Recording
Starting, stopping, and pausing the daemon, checking status, and the lifecycle of stored data.
Recording runs as a background daemon (a launchd agent), controlled with these commands:
| Command | Effect |
|---|---|
zanei start |
Register with launchd and start background recording |
zanei stop |
Stop recording and unregister from launchd; stored data is kept |
zanei pause --for 30m |
Pause; without --for, pauses indefinitely until resume |
zanei resume |
Resume from pause |
zanei status |
Check daemon state, event counts, and store info |
Starting
zanei start
At startup, the recorder requests required missing Accessibility and Input Monitoring permissions once. A background start exits with code 3 if the recorder heartbeat reports missing permissions, but the daemon remains running in a degraded state. Grant them, run zanei stop && zanei start, and verify with zanei doctor (see the permissions guide).
Running in the foreground
Two ways to observe what’s happening without daemonizing:
zanei start --foreground # same configuration as the daemon, in the foreground (dev/debug)
zanei record --stream # stream raw events as NDJSON to stdout
record is for piping and experimentation; use --out events.jsonl to write to a file. For everyday recording, use start.
Checking status
zanei status
zanei status --json # for agents and scripts
The following is an abridged example. The CLI reference defines the complete JSON shape and null conditions.
{
"running": true,
"paused": false,
"since": "2026-08-16T08:00:00Z",
"uptime_s": 3600,
"events_captured": 12345,
"last_event_ts": "2026-08-16T08:59:58Z",
"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
}
When there is no daemon to act on (status, stop, etc.), the exit code is 4.
Diagnosing recorder problems
Run zanei doctor first. Its COLLECTOR HEALTH section reports the current evidence-based state; zanei doctor --json exposes the same information under health. The CLI reference defines every state and field.
Read current degradation and cumulative failures separately:
health.degradedindoctor --jsonanddegradedinstatus --jsondescribe only a problem that is current. Human output prints the same map belowCOLLECTOR HEALTHorDEGRADED. Chrome and AX diagnostic reasons use stablephaseandkindterms and, when available, the underlying operation and numeric error code. When AX has more than one unresolved failure site, its reason also reportsunresolved_sites; recovery at one PID and operation does not hide another unresolved site.collector_failuresis a persisted, monotonically increasing count of failures by collector. Human output prints it belowCOLLECTOR FAILURES. A nonzero count means failures happened in the past; it does not by itself mean the collector is still unhealthy.
While fresh recorder status from the current owner is available, a current reason is removed only when the collector reports actual recovery. A reason retained after an unexpected collector exit is also removed after the restarted collector runs stably for 60 seconds. If the recorder stops, its status becomes stale, or the persisted recorder instance no longer matches the current owner, recorder-reported diagnostics are removed from the degraded map because current evidence is unavailable, not because recovery occurred. Entries synthesized while reading the store, such as retired_store, remain regardless of whether the recorder is running. Interpret the absence of recorder-reported diagnostics using the canonical CLI definitions for recorder health and status fields, especially health.state (stopped, stale, or suspected_unavailable, as applicable) and store_write_state. Recovery does not reset collector_failures, so no recorder-reported diagnostics and a nonzero cumulative count is normal.
For a background recorder, launchd writes persistent diagnostic output beside the selected store:
| File | Default path |
|---|---|
| Standard output | ~/.local/state/zanei/store.sqlite.daemon.stdout.log |
| Standard error | ~/.local/state/zanei/store.sqlite.daemon.stderr.log |
With a custom store path, the names are <store>.daemon.stdout.log and <store>.daemon.stderr.log. Both files are regular files restricted to the current user (0600). Zanei does not rotate them automatically. They contain recorder diagnostics only, not captured event payloads or captured text content.
Zanei creates a missing store directory with mode 0700 and refuses to start the launchd recorder unless the store and log directory is safe. The directory must be owned by the current user and must not be group- or world-writable. Its ancestors must be owned by root or the current user; a group- or world-writable ancestor also needs the sticky bit. Extended ACL entries that grant access, and ACL entries Zanei cannot safely interpret, are rejected.
The startup error names the unsafe directory. Remove group/world write access if it is not intentional. If the error identifies an extended ACL and that ACL is not required, remove it from the named directory:
chmod go-w /path/to/directory-reported-by-zanei
chmod -N /path/to/directory-reported-by-zanei
Then run zanei start again. If the directory is intentionally shared, choose an owner-only store directory instead of weakening this check.
Pausing and resuming
pause turns recording off temporarily, for example during a meeting or a screen share:
zanei pause --for 30m # resumes automatically after 30 minutes
zanei pause # paused until you resume
zanei resume
Data lifecycle
- Captured events live only in the local SQLite store at
~/.local/state/zanei/store.sqlite(override with--store). - Events older than 48 hours are purged at startup and periodically, and are excluded from reads (
retention_hours, see configuration). - To delete manually, use
purge:
zanei purge --before 24h # delete events older than 24 hours
zanei purge --all # delete everything (with a confirmation prompt)