Skip to content
Zanei
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.degraded in doctor --json and degraded in status --json describe only a problem that is current. Human output prints the same map below COLLECTOR HEALTH or DEGRADED. Chrome and AX diagnostic reasons use stable phase and kind terms and, when available, the underlying operation and numeric error code. When AX has more than one unresolved failure site, its reason also reports unresolved_sites; recovery at one PID and operation does not hide another unresolved site.
  • collector_failures is a persisted, monotonically increasing count of failures by collector. Human output prints it below COLLECTOR 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)

Was this page helpful?