---
title: CLI reference
description: Every zanei command, flag, shared convention, and exit code.
---

All functionality lives in a single binary, `zanei` — daemon control, permission diagnostics, data retrieval, filter management, agent setup, and the MCP server. The CLI and the [MCP server](/reference/mcp) are thin wrappers over the same core and store, and share the conventions on this page.

- Signed, notarized single Rust binary; no external runtime.
- Strictly local: the CLI performs no network egress of any kind.
- Apart from the destructive operations (`stop`, `purge`), commands have minimal side effects and are safe for agents to call.

## Commands at a glance

| Command | Summary |
| --- | --- |
| [`doctor`](#doctor) | Diagnose required macOS permissions; guide granting |
| [`start`](#start) | Start background recording (launchd) |
| [`stop`](#stop) | Stop recording and unregister (data kept) |
| [`pause`](#pause) / [`resume`](#resume) | Temporarily suspend / resume recording |
| [`status`](#status) | Daemon state, event counts, store info |
| [`record`](#record) | Foreground capture to stdout/file (debug & piping) |
| [`query`](#query) | Retrieve raw events with filters |
| [`timeline`](#timeline) | LLM-ready, token-budgeted timeline |
| [`export`](#export) | Bulk dump of raw events |
| [`purge`](#purge) | Manually delete stored events (destructive) |
| [`filter`](#filter) | Manage capture-time allow/deny lists |
| [`config`](#config) | Initialize, show, locate, or edit configuration |
| [`mcp`](#mcp) | Run the stdio MCP server |
| [`setup`](#setup) | Configure agent instructions and MCP integration |

## Global options

Valid for every subcommand.

| Flag | Description |
| --- | --- |
| `--config <path>` | Override config file path (default `~/.config/zanei/config.toml`) |
| `--store <path>` | Override store path (default `~/.local/state/zanei/store.sqlite`) |
| `--json` | Machine-readable JSON for diagnostic/state commands (shortcut for `--format json`) |
| `-q, --quiet` | Suppress progress output and notices |
| `-v, --verbose` | Verbose logging to stderr |
| `--version` / `--help` | Version / help |

## Shared conventions

### Time expressions

Values accepted by `--since` / `--until`:

| Form | Example | Meaning |
| --- | --- | --- |
| Relative | `15m` `2h` `1d` `1w` | Looking back from now; units `s` `m` `h` `d` `w` |
| Absolute | `2026-08-16T09:00:00Z` | RFC3339 timestamp |
| Keyword | `now` | Current moment (mainly for `--until`) |

`--until` defaults to `now`. `--since` defaults per command: `timeline` = `1h`, `query` = `15m`, `export` = `24h`.

### Event types

`--types` takes a comma-separated list of [event types](/reference/events), with trailing wildcards allowed (`browser.*`).

### Output formats

| Value | Commands | Description |
| --- | --- | --- |
| `jsonl` | `query`, `export`, `record` | One raw event per line (machine-readable) |
| `json` | `query`, `export`, `timeline` | Single JSON document; for `timeline`, the structured timeline |
| `md` | `timeline` | LLM-ready Markdown (default) |
| `table` | `query`, `status`, `doctor` | Human-readable table |

### Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Success |
| `1` | General error |
| `2` | Usage error (invalid arguments) |
| `3` | **Missing permissions** (required TCC permission not granted) — a dedicated code so agents and scripts can detect it |
| `4` | No daemon (nothing to act on for `status`, `stop`, ...) |

---

## `doctor`

Diagnoses the granted state of required TCC permissions (Accessibility / Input Monitoring / Automation) against the configured `capture.sources`.

```bash
zanei doctor            # human-readable; shows granting steps and the System Settings pane if anything is missing
zanei doctor --fix      # opens the relevant System Settings pane (granting is still a user action)
zanei doctor --json     # machine-readable, for agents
```

| Flag | Description |
| --- | --- |
| `--fix` | Open the System Settings pane for each missing permission |
| `--json` | Machine-readable output |

- Exits with **code 3** if any required permission is missing.
- `input_monitoring` counts as required only when the configuration captures `input.*`; in configurations that don't, its absence still yields `ok`.

`--json` output:

```json
{
  "ok": false,
  "capture_sources": ["app", "window", "ui", "input", "browser"],
  "permissions": {
    "accessibility":     { "status": "granted" },
    "input_monitoring":  { "status": "denied", "required_for": ["input.key", "input.scroll", "ui.click"] },
    "automation":        { "per_app": { "com.google.Chrome": "not_determined" } }
  },
  "missing_required": ["input_monitoring"],
  "settings_pane": "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"
}
```

`status` values: `granted` / `denied` / `not_determined`.

Human-readable output always ends with the next action. When a required permission is denied, it
walks through granting it: run `zanei start` so the recorder asks macOS for the permission, switch
the `zanei` row ON if the pane lists it, and only otherwise use `zanei doctor --fix`, the `+`
button, and Command-Shift-G (or a single `~`) to add the exact path of the running executable.
Every report notes that macOS does not always list launchd-started command-line tools in those
panes, so `doctor` — not the list — is the authority. When Chrome Automation is `not_determined`, no advance
action is needed: macOS asks the first time Zanei contacts Chrome. An unsigned or ad-hoc-signed
build also produces a warning that rebuilding will reset granted permissions.

## `start`

Registers Zanei with launchd and starts background recording.

```bash
zanei start              # start background recording
zanei start --foreground # run in the foreground instead (dev/debug)
```

| Flag | Description |
| --- | --- |
| `--foreground` | Don't daemonize; run in the foreground |

If missing permissions are detected, recording does not start and the command exits with **code 3**, explaining what to grant.

After a background start succeeds, the confirmation states that Zanei is registered as a
launchd background item. A macOS notification or an entry under **Login Items & Extensions** is
expected behavior.

## `stop`

Stops recording and unregisters from launchd. **Not destructive** — stored data is kept. Exits with **code 4** if there is no daemon to stop.

```bash
zanei stop
```

## `pause`

Temporarily suspends recording without unregistering the daemon.

```bash
zanei pause --for 30m   # auto-resume after 30 minutes
zanei pause             # paused until `resume`
```

| Flag | Description |
| --- | --- |
| `--for <TIME>` | Duration; omit to pause indefinitely |

## `resume`

Resumes recording after a pause.

```bash
zanei resume
```

## `status`

Reports daemon state, capture configuration, and store statistics.

```bash
zanei status
zanei status --json
```

Human-readable output shows `TEXT CONTENT      on (opt-in)` when enabled and
`TEXT CONTENT      off (opt-in: zanei config set capture.text_content true)` when disabled, so the
privacy-sensitive opt-in state and its explicit activation command are visible without requesting JSON.

```json
{
  "running": true,
  "paused": false,
  "since": "2026-08-16T08:00:00Z",
  "uptime_s": 3600,
  "events_captured": 12345,
  "events_dropped": 2,
  "last_event_ts": "2026-08-16T08:59:58Z",
  "degraded": { "eventtap": "1 degraded collector operation observed" },
  "store": { "path": "~/.local/state/zanei/store.sqlite", "size_bytes": 5242880, "retention_hours": 48, "oldest_event_ts": "2026-08-14T09:00:00Z" },
  "capture": { "sources": ["app", "window", "ui", "input", "browser"], "text_content": false },
  "permissions_ok": true
}
```

| Field | Meaning |
| --- | --- |
| `events_dropped` | Cumulative events dropped by collectors or the recording pipeline |
| `degraded` | Collector or runtime degradation by component; empty when no degradation is known |
| `permissions_ok` | The running recorder's own permission result while its heartbeat is fresh; a local probe when no recorder is running |

## `record`

Foreground capture: streams raw events as NDJSON to stdout (or a file) without daemonizing. Intended for piping and experiments — use [`start`](#start) for everyday recording.

```bash
zanei record --stream                 # NDJSON to stdout
zanei record --out events.jsonl       # to a file
```

| Flag | Description |
| --- | --- |
| `--stream` | Stream events to stdout as they occur |
| `--out <FILE>` | Write to a file instead |
| `--format jsonl` | Output format (NDJSON) |

## `query`

Retrieves raw events matching the given conditions. Machine-readable, structured output.

```bash
zanei query --since 15m --types browser.navigate,app.activate
zanei query --since 2h --app Safari --format json --limit 500
```

| Flag | Description |
| --- | --- |
| `--since` / `--until` | Time range (default `--since 15m`) |
| `--types <TYPE,...>` | Event-type filter (comma-separated; `browser.*` wildcards allowed) |
| `--app <NAME>` / `--bundle-id <ID>` | Filter by app name / bundle ID |
| `--limit <N>` | Max events (default 500) |
| `--format` | `jsonl` (default) / `json` / `table` |

These are query-time filters — they narrow reads, not what gets recorded (see [filters](/guides/filters)).

## `timeline`

Splits raw events into sessions, deduplicates, coalesces, and serializes an LLM-ready summary within a token budget. **The command agents call most.**

```bash
zanei timeline --since 1h --format md --token-budget 4000
zanei timeline --since 30m --format json --granularity fine
```

| Flag | Description |
| --- | --- |
| `--since` / `--until` | Time range (default `--since 1h`) |
| `--format` | `md` (default; LLM-ready Markdown) / `json` (structured) |
| `--token-budget <N>` | Approximate token cap (default 4000); content is coarsened to fit |
| `--granularity` | `coarse` (default; per session) / `fine` (per interaction) |

`--format json` structure (abridged):

```json
{
  "range": { "since": "2026-08-16T08:00:00Z", "until": "2026-08-16T09:00:00Z" },
  "token_estimate": 3810,
  "truncated": false,
  "sessions": [
    {
      "start": "2026-08-16T08:12:00Z", "end": "2026-08-16T08:31:00Z",
      "app": "Safari", "title_summary": "Reviewing PR #42",
      "activities": ["Viewed 3 files on GitHub", "Wrote 2 comments"],
      "event_ids": ["evt_01J...", "evt_01J..."]
    }
  ]
}
```

## `export`

Dumps every raw event in range (backup / external processing).

```bash
zanei export --since 24h --format jsonl --out dump.jsonl
```

| Flag | Description |
| --- | --- |
| `--since` / `--until` | Time range (default `--since 24h`) |
| `--format` | `jsonl` / `json` |
| `--out <FILE>` | Output file |

## `purge`

Deletes stored events manually, independent of retention-based auto-purge. **Destructive.**

```bash
zanei purge --before 24h   # delete events older than 24 hours
zanei purge --all          # delete everything (confirmation prompt; suppress with --quiet)
```

| Flag | Description |
| --- | --- |
| `--before <TIME>` | Delete events older than this |
| `--all` | Delete everything (asks for confirmation) |

## `filter`

Manages the capture-time allow/deny lists — apps and sites discarded **before** they are written to the store. Semantics and matching rules are covered in the [filters guide](/guides/filters). Equivalent to editing `[filter]` in `config.toml`, with added validation and deduplication.

```bash
zanei filter show                                      # all lists and the active mode
zanei filter exclude-app add com.1password.1password
zanei filter exclude-app remove com.1password.1password
zanei filter only-app add com.apple.Safari
zanei filter exclude-site add example.com
zanei filter only-site add github.com
```

| Subcommand | `[filter]` key | Meaning |
| --- | --- | --- |
| `exclude-app (add\|remove) <BUNDLE_ID or NAME>` | `exclude_apps` | Deny list (everything else is captured) |
| `only-app (add\|remove) <BUNDLE_ID or NAME>` | `include_only_apps` | Allow list (when set, only listed apps are captured) |
| `exclude-site (add\|remove) <DOMAIN>` | `exclude_websites` | Deny list for `browser.*` URL hosts |
| `only-site (add\|remove) <DOMAIN>` | `include_only_websites` | Allow list for `browser.*` URL hosts |

- Apps: specify by `BUNDLE_ID` (recommended) or display name. `filter show` also displays the built-in default exclusions (`1Password`, `Keychain Access`, ...).
- Changes apply from the next event — no daemon restart.
- Adding a single `only-app` entry switches to allow-list mode; `filter show` states the active mode.

## `config`

```bash
zanei config init   # create a fully commented configuration template
zanei config path   # print the config file path
zanei config show   # print the effective configuration (defaults merged)
zanei config edit   # open the config in $EDITOR
zanei config set capture.text_content true
```

`config init` writes a complete, commented template containing every supported option and its
current default. By default it creates `~/.config/zanei/config.toml`; the global
`--config <path>` flag selects a different destination. Missing parent directories are created. On
success, the command prints the created path and exits with code 0. If the destination already
exists, it leaves the file unchanged, reports the existing path, and exits with code 1.

`config set <DOTTED_KEY> <VALUE>` validates and saves one scalar setting. Supported keys and values:

| Key | Accepted values |
| --- | --- |
| `capture.text_content` | `true`, `false` |
| `output.mode` | `stream`, `batch`, `both` |
| `output.batch_interval_s` | Unsigned integer greater than zero |
| `output.store` | `sqlite`, `jsonl` |
| `output.retention_hours` | Unsigned integer greater than zero |

Array settings are not accepted: use [`zanei filter`](#filter) for filter lists, or
`zanei config edit` for other arrays. Unknown keys, array keys, and invalid values exit with
code 2 without saving. If the recording daemon is running, a successful `capture.*` or `output.*`
change prints:

```text
Restart recording with `zanei stop && zanei start` for this to take effect.
```

Schema: see the [configuration reference](/reference/config).

## `mcp`

Runs the stdio MCP server — JSON-RPC on stdin/stdout, zero network surface. Started by MCP clients (Claude Code, Codex, opencode, Claude Desktop, ...), not usually by hand.

```bash
zanei mcp [--store <PATH>]
```

- **Read-only view** over the store; independent from the recording daemon.
- Exposed tools: `get_timeline` / `query_events` / `get_status` — see the [MCP reference](/reference/mcp).

## `setup`

Configures Zanei instructions and MCP integration for the target agent. Agents with native skill support and Claude Desktop retain their file-based setup; opencode and pi only print manual setup instructions and do not modify files.

```bash
zanei setup --agent claude            # SKILL.md into Claude Code's skill location + MCP registration
zanei setup --agent codex --scope user
zanei setup --agent hermes            # skill into ~/.hermes/skills/ + MCP in ~/.hermes/config.yaml
zanei setup --agent pi                # print derived README instructions; no MCP or file writes
zanei setup --agent claude-desktop    # MCP registration in claude_desktop_config.json
zanei setup --agent opencode          # print AGENTS.md snippet + MCP JSON; no file writes
```

| Flag | Description |
| --- | --- |
| `--agent` | `claude` / `codex` / `opencode` / `hermes` / `pi` / `claude-desktop` |
| `--scope` | `project` (default; current repository) / `user` (account-wide). It has no effect on the manual opencode/pi output |
| `--print` | Preview planned file changes without writing. opencode and pi do not write files, with or without this flag |

Per-agent behavior and setup output: see [agent setup](/agents/setup).
