CLI reference
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 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 |
Diagnose required macOS permissions; guide granting |
start |
Start background recording (launchd) |
stop |
Stop recording and unregister (data kept) |
pause / resume |
Temporarily suspend / resume recording |
status |
Daemon state, event counts, store info |
record |
Foreground capture to stdout/file (debug & piping) |
query |
Retrieve raw events with filters |
timeline |
LLM-ready, token-budgeted timeline |
export |
Bulk dump of raw events |
purge |
Manually delete stored events (destructive) |
filter |
Manage capture-time allow/deny lists |
config |
Initialize, show, locate, or edit configuration |
mcp |
Run the stdio MCP server |
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, 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.
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_monitoringcounts as required only when the configuration capturesinput.*; in configurations that don’t, its absence still yieldsok.
--json output:
{
"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
explains that CLI tools do not appear in macOS permission lists automatically and shows how to use
zanei doctor --fix, the + button, and Command-Shift-G (or a single ~) to add the exact
path of the running executable. 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.
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.
zanei stop
pause
Temporarily suspends recording without unregistering the daemon.
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.
zanei resume
status
Reports daemon state, capture configuration, and store statistics.
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.
{
"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 for everyday recording.
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.
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).
timeline
Splits raw events into sessions, deduplicates, coalesces, and serializes an LLM-ready summary within a token budget. The command agents call most.
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):
{
"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).
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.
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. Equivalent to editing [filter] in config.toml, with added validation and deduplication.
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 showalso displays the built-in default exclusions (1Password,Keychain Access, …). - Changes apply from the next event — no daemon restart.
- Adding a single
only-appentry switches to allow-list mode;filter showstates the active mode.
config
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 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:
Restart recording with `zanei stop && zanei start` for this to take effect.
Schema: see the configuration reference.
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.
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.
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.
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.