# Telemetry

> What anonymous usage data agentjail collects, how it stays anonymous, and how to turn it off with one command or an environment variable.

agentjail collects **anonymous, opt-out** usage statistics to help us decide what
to improve. This page documents exactly what is and isn't sent, how to see it, and
how to turn it off. The source of truth for the data contract is
[`internal/telemetry/event.go`](https://github.com/LuD1161/agentjail/blob/main/internal/telemetry/event.go)
in the repo: every field below is built there and nowhere else.

## TL;DR

- **Anonymous.** Tied to a random ID generated once on your machine, never your
  name, hostname, username, IP, or any hardware fingerprint.
- **No payloads, ever.** We never send file paths, command text, repo names,
  environment contents, MCP server names, or policy contents.
- **Off automatically in CI** (`CI=true` and common CI variables).
- **Opt out anytime:**

```sh
agentjail telemetry disable
# or, per-shell / in CI / in a Dockerfile:
export AGENTJAIL_SEND_ANONYMOUS_USAGE_STATS=false
```

## Controlling telemetry

| Command | What it does |
| --- | --- |
| `agentjail telemetry status` | Show whether it's on/off and *why* (env var, CI, config, or default), plus your anonymous ID |
| `agentjail telemetry disable` | Turn it off (persisted) |
| `agentjail telemetry enable` | Turn it back on |
| `agentjail telemetry view` | Print the exact JSON currently queued to be sent |
| `agentjail telemetry reset` | Generate a new anonymous ID and clear the queue |

**Resolution order** (first match wins): the `AGENTJAIL_SEND_ANONYMOUS_USAGE_STATS`
environment variable → CI auto-detection → your `~/.agentjail/telemetry.json`
setting → default (on). The variable accepts `false`/`0`/`no` to disable and
`true`/`1`/`yes` to enable.

## Exactly what is sent

Every event carries a random `distinct_id` (your anonymous ID), a per-event
`$insert_id` (used only to de-duplicate retried sends), and the `agentjail_version`.

**Lifecycle events** (`install`, `uninstall`, `update`) are sent **immediately and
synchronously** when they happen, without waiting for the daemon's batched flush,
so they're captured even if the daemon never runs. Everything else is spooled locally
and delivered by the daemon.

### `install`

Emitted once, immediately after a successful install: `os`, `arch`, optional
`install_method` (`curl`/`brew`), optional `agents` (the agent enums whose hooks
were wired), and `agents_detected` (how many supported agents were found).

### `uninstall`

Emitted immediately on teardown (full or single-agent), **before** any state is
removed: `os`, `arch`, and optional `agents` (which agents were unhooked; omitted
for a full teardown).

### `update`

Emitted when you run `agentjail update`: `from_version`, `to_version`, `os`, `arch`.

### `session_start`

Emitted when the daemon starts: `os`, `arch`, and optional `install_method`.

### `feature_used`

Emitted when you run a CLI command: a `command` enum (unknown commands collapse to
`other`, so your arguments are never recorded) and an optional `agents` enum
(`claude`/`codex`/`cursor`).

### `decision_rollup`

Aggregated policy decisions for a window (not per decision): `action_counts` such as
`{"allow":120,"deny":3,"ask":1}`, and `rule_counts` keyed by **rule ID** such as
`command_policy/rm_rf`, never the matched path or command. Also includes optional
`rule_action_counts` (keyed by `"action|rule_id"`), `tool_counts` (decisions per tool
name), `agent_counts` (decisions per agent identity), and `spool_dropped` (events
dropped if the local queue overflowed). Custom rule IDs appear verbatim; see "A note
on custom rule names" below.

### `perf_rollup`

Aggregated daemon performance: `eval_p50_ms`, `eval_p95_ms`, and `restarts`.

### `policy_config`

Your configuration at daemon start and on reload: `custom_rule_count` (a count; rule
contents are never sent) and `disabled_rules` (your disabled-rules list verbatim,
which may include custom rule IDs).

### `fail_open`

Emitted when the hook fails open (can't reach or parse a daemon decision and allows
the action rather than blocking): `os` and a `reason` enum (`dial-daemon`,
`read-response`, `parse-response`, `read-stdin`, `parse-input`, `other`), never the
tool input.

### `heartbeat`

Best-effort, at most once per ~24h on a CLI run: `os`, `latest_version` (the latest
release seen), and `update_available`.

### `feedback`

Only when you run `agentjail feedback` (never automatic): the `message` you typed,
an optional `contact` you typed, and `os`. Sent even if usage telemetry is disabled.

A real `decision_rollup`, the entirety of what leaves your machine for that event:

```json
{
  "event": "decision_rollup",
  "properties": {
    "distinct_id": "3f9c2a7e-8b1d-4c5a-9e2f-1a2b3c4d5e6f",
    "$insert_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "agentjail_version": "1.1.0",
    "action_counts": { "allow": 120, "deny": 3, "ask": 1 },
    "rule_counts": { "command_policy/rm_rf": 3, "file_policy/ssh_key_read": 1 }
  }
}
```

## What is never sent

File paths, command text, repository names or URLs, environment variable contents,
MCP server names, policy file contents, IP addresses, hostnames, usernames,
timestamps finer than the day, or any hardware identifier.

This isn't enforced by review alone: telemetry events are built from fixed structs,
never by serializing arbitrary data, so a payload **cannot** be attached by accident.

## A note on custom rule names

We **do** send the IDs of your custom rules (e.g. `custom/<name>/<rule>`) in
`rule_counts` and your `disabled_rules` list, so we can understand what kinds of
custom rules people build. We do **not** send the contents of any rule, nor the
paths or commands they match. If you'd rather not share custom rule IDs, name your
rules generically, or turn telemetry off with `agentjail telemetry disable`. Run
`agentjail telemetry view` to see exactly what's queued before anything is sent.

## Sending feedback

```sh
agentjail feedback "the publish guard fires too often"
# or run it with no message and it prompts you, plus an optional contact
```

`agentjail feedback` sends a one-off `feedback` event with your message, an optional
contact you may provide, your OS, version, and the random anonymous ID. Because it's
an explicit action you took, it sends even if usage telemetry is disabled, and it
never includes anything beyond what you typed plus version and OS. If the build has
no backend configured, it prints a GitHub issue link instead.

## How it's delivered

Most events are aggregated locally and delivered by the daemon in a single batched
HTTPS request. The daemon flushes shortly after startup (a couple of minutes) and
then roughly every 6 hours, plus on graceful shutdown. CLI commands only write a
local event to `~/.agentjail/telemetry-spool.jsonl`, which the daemon picks up on
its next flush.

The exception is **lifecycle events** (`install`, `uninstall`, `update`) and
`feedback`: the CLI sends these **immediately and synchronously** rather than waiting
for the daemon flush, since they may happen when no daemon is running. If you're
offline or a send fails, spooled events stay queued and are retried later; the local
queue is capped so it can't grow without bound.

## Backend

We currently use **PostHog** (US cloud) as our analytics backend. This may change;
the data we send, listed above, will not. The telemetry host is included in
agentjail's default network allowlist so agentjail doesn't block its own
telemetry; you're free to remove it.
