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

Configuration

The config.toml schema — capture sources, filters, output, and retention.

Configuration lives in a single TOML file:

~/.config/zanei/config.toml

Override the path per invocation with the global --config <path> flag. Helper commands:

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

zanei config init creates missing parent directories and writes the complete template shown below. It never overwrites an existing file; if the destination exists, it reports that path and exits with code 1. Use the global --config <path> option to initialize a non-default location.

Use zanei config set for capture.text_content, capture.content_snapshot, output.batch_interval_s, and output.retention_hours. Use zanei filter for filter lists and config edit for other arrays.

Full example (defaults)

[capture]
sources = ["app", "window", "ui", "input", "browser"]
text_content = false            # content capture is an explicit opt-in
content_snapshot = false        # frontmost-window Accessibility text is a separate opt-in

[filter]                        # capture-time filters: the privacy boundary (applied before the store)
exclude_apps = ["1Password", "Keychain Access"]   # deny list (bundle_id recommended); defaults ship built-in
include_only_apps = []          # non-empty switches to allow-list mode
exclude_websites = []           # browser URL events/text bodies; dot-boundary host suffix match
include_only_websites = []      # allow list for browser URL events/text bodies
redactors = ["credit_card", "token"]              # limited pattern-based scrubbing; email is opt-in

[filter.text_content]           # typed/copied bodies only; events and non-content facts remain
exclude_apps = ["com.apple.Safari", "org.mozilla.firefox", "com.brave.Browser",
                "com.microsoft.edgemac", "com.vivaldi.Vivaldi", "company.thebrowser.Browser"]
include_only_apps = []
exclude_websites = []
include_only_websites = []

[filter.content_snapshot]       # content.snapshot events only
exclude_apps = ["com.apple.Safari", "org.mozilla.firefox", "com.brave.Browser",
                "com.microsoft.edgemac", "com.vivaldi.Vivaldi", "company.thebrowser.Browser"]
include_only_apps = []
exclude_websites = []
include_only_websites = []

[output]
batch_interval_s = 5
retention_hours = 48            # short-lived retention by default

There is no egress-related setting; external transmission does not exist as a feature.

Validation

Validation runs after omitted settings are filled with their defaults. Only the 18 keys below and the optional [filter.capture_policy] table, in their declared sections, are accepted; any other key makes the file invalid. config init writes all 18 options and never writes [filter.capture_policy]. A file that violates a rule is not accepted: every command that loads it, including config show and config edit, exits with code 1. Invalid values given to config set or filter are usage errors (code 2) and nothing is saved. config set accepts only scalar keys (capture.text_content, capture.content_snapshot, output.batch_interval_s, output.retention_hours); array keys are edited with filter or by hand.

Key Accepted values
capture.sources Zero or more of app, window, ui, input, browser; no duplicates
capture.text_content TOML boolean; config set accepts exactly true or false
capture.content_snapshot TOML boolean; config set accepts exactly true or false. Enabling it shows the current scope and asks [y/N]; see the CLI reference
filter.exclude_apps, filter.include_only_apps Strings; each non-empty, no leading or trailing whitespace, unique within the list ignoring case. filter ... add of a value already present (including a case-only variant) is a no-op (code 0)
filter.exclude_websites, filter.include_only_websites Domain names, same non-empty / whitespace / uniqueness rules. At most one trailing dot; without it, at most 253 bytes total, each dot-separated label 1–63 bytes of ASCII letters, digits, or -, starting and ending with a letter or digit. A value that is itself a public suffix in the bundled snapshot (for example com) is accepted, but filter ... add prints a warning unless --quiet is set
filter.text_content.exclude_apps, filter.text_content.include_only_apps, filter.content_snapshot.exclude_apps, filter.content_snapshot.include_only_apps The same app-list rules. Both exclude_apps lists default to Safari, Firefox, Brave, Edge, Vivaldi, and Arc bundle IDs shown in the full example; the other app lists default to []
filter.text_content.exclude_websites, filter.text_content.include_only_websites, filter.content_snapshot.exclude_websites, filter.content_snapshot.include_only_websites The same domain-list rules; all four default to []
filter.redactors Zero or more of email, credit_card, token; no duplicates. email is available but is not enabled by default
output.batch_interval_s, output.retention_hours Unsigned integer (u64) greater than zero
[filter.capture_policy] Optional and absent by default. When the table is present, browser and ide and every key inside them are required; allowed_apps is optional. See [filter.capture_policy]

[capture]

Key Default Description
sources ["app", "window", "ui", "input", "browser"] Which event families to capture. Without browser, browser Automation is required for eligible content snapshots or when text_content is enabled with ui or input; window alone does not require it (see permissions)
text_content false Capture typed/field content (ui.value, input.key, clipboard contents). Off by default; see the privacy model
content_snapshot false Capture Accessibility text shown in the frontmost window as content.snapshot. Independent from sources and text_content; even with window absent from sources, this opt-in requires Accessibility and can record snapshots. Restart required

[filter]

Capture-time filters apply before writing. There are three scopes: [filter] for all events, [filter.text_content] for typed/copied bodies, and [filter.content_snapshot] for snapshot events. For each app or website axis, a non-empty include_only_* list selects only mode; otherwise the scope uses exclude mode. exclude_* always wins.

The evaluation order is built-in exclusions, [filter] apps and websites, [filter.text_content], then [filter.content_snapshot]. Falling outside the text-content scope keeps the event and facts but sets body fields to null, as if capture.text_content were false in that window. Falling outside the snapshot scope prevents content.snapshot from being created. Website rules apply to Chrome and Safari. See the filters guide; manage lists with zanei filter rather than editing by hand.

Key Default Description
exclude_apps built-in defaults Deny list of apps (bundle_id recommended; display names allowed)
include_only_apps [] Allow list; non-empty switches to allow-list mode
exclude_websites [] Deny list of URL hosts for browser URL events and text-content bodies. Dot-boundary suffix match with no Public Suffix List handling: example.com covers api.example.com but not evil-example.com; a bare com would match every .com host
include_only_websites [] Allow list of URL hosts for browser URL events and text-content bodies
redactors ["credit_card", "token"] Pattern-based redaction rules applied to captured values. email remains available as an opt-in rule

The nested scopes use the same four list names. Their defaults and behavior are shown above. The daemon watches all three filter scopes at roughly 2-second intervals, so changes apply within a few seconds with no daemon restart.

[filter.capture_policy]

An optional table for an application that embeds the recorder. It is written by that application or by hand: config init never writes it, and config set and filter never touch it. A configuration without it behaves exactly as described above, and adding it does not change the meaning of any key above.

App selection is the same for everyone. Which applications are recorded is decided by filter.exclude_apps and filter.include_only_apps — the lists managed with zanei filter exclude-app and zanei filter only-app. capture_policy adds only what those lists cannot express: browser URL rules and IDE file rules.

allowed_apps is optional and exists for a policy that must pin its own explicit list of application display names. Omit it and the two filter lists are the only app gate; include it and it is an additional allow list applied after them, so an empty allowed_apps records nothing.

Given an event whose app already passed the [filter] app lists, evaluation runs in this order:

  1. Built-in exclusions (password managers and credential stores) deny the app. They cannot be lifted by any configuration.
  2. allowed_apps, when present, must contain the app’s display name (compared case-insensitively after trimming surrounding whitespace); otherwise the event is denied.
  3. Google Chrome and Safari are decided by browser and nothing else.
  4. Cursor, Visual Studio Code, and Code are decided by ide.
  5. Every other app is allowed.

The [filter] and [filter.text_content] / [filter.content_snapshot] website rules still apply to Chrome and Safari after browser allows a URL. Like the rest of [filter], changes are picked up by the daemon’s config watch within a few seconds; no restart is needed.

Key Required Description
allowed_apps no Application display names. Absent leaves app selection to filter.exclude_apps / filter.include_only_apps; [] denies every app. Each entry non-empty, no leading or trailing whitespace, unique ignoring case
browser.mode yes off denies Chrome and Safari outright, all_sites allows any resolved URL, rules consults the lists below
browser.default_policy yes allow or block for a resolved URL that matches no rule under mode = "rules"
browser.on_url_unavailable yes allow or block when no http/https URL with a host could be bound to the window
browser.block_auth yes Block URLs whose path contains a representative sign-in, sign-up, OAuth, or password-reset segment. It is a URL pattern, not detection of every authentication screen: /docs/oauth matches too
browser.block_payments yes Block URLs whose path contains checkout, payment, payments, billing, subscription, or subscriptions
browser.allow_list, browser.block_list yes Arrays of { host, path_prefix, match_subdomains }. block_list wins over allow_list and over mode = "all_sites". host must already be canonical — the exact form URL.hostname produces, so lowercase ASCII/IDNA (xn--r8jz45g.xn--zckzah, not 例え.テスト), no port, no trailing dot, IPv6 in brackets. path_prefix is a literal, case-sensitive prefix with no surrounding whitespace
ide.block_env_files yes Block capture in Cursor, Visual Studio Code, and Code when the window title names a .env file. .env.example and similar are not blocked
ide.on_file_name_unavailable yes allow or block when no file name can be read from the window title

Written by an embedding application that lets the user exclude specific apps, where the browser is restricted to two sites and IDE .env files are never captured:

[filter]
exclude_apps = ["1Password", "Keychain Access", "com.tinyspeck.slackmacgap"]
include_only_apps = []

[filter.capture_policy]                 # no allowed_apps: exclude_apps above is the app gate

[filter.capture_policy.browser]
mode = "rules"
default_policy = "block"
on_url_unavailable = "block"
block_auth = true
block_payments = true
allow_list = [
  { host = "github.com", path_prefix = "", match_subdomains = false },
  { host = "example.com", path_prefix = "/docs", match_subdomains = true },
]
block_list = []

[filter.capture_policy.ide]
block_env_files = true
on_file_name_unavailable = "block"

The same policy in only mode — the user picked the two apps to record:

[filter]
include_only_apps = ["com.apple.Notes", "com.google.Chrome"]

And the pinned form, for a policy that must carry its own allow list instead of using the filter lists:

[filter.capture_policy]
allowed_apps = ["Notes", "Google Chrome"]

[output]

Key Default Description
batch_interval_s 5 Interval for flushing coalesced events and SQLite batches
retention_hours 48 Events older than this are purged at startup and periodically, and excluded from reads

zanei record emits NDJSON as events are recorded. The daemon writes to the single local SQLite store; neither output behavior nor the store backend is configurable.

Change propagation

  • [filter], [filter.text_content], and [filter.content_snapshot] changes are picked up by the daemon’s config watch (roughly 2-second intervals) and apply within a few seconds; no restart is needed.
  • output.retention_hours is picked up by the daemon without a restart and immediately purges events outside the new window. Other changes (e.g. capture.sources, capture.content_snapshot, and output.batch_interval_s) take effect after a daemon restart: zanei stop && zanei start.
Path Purpose Override
~/.config/zanei/config.toml Configuration --config <path>
~/.local/state/zanei/store.sqlite Event store --store <path>

The event store is encrypted. Its key is generated on the recorder’s first start and kept in your login Keychain; there is no configuration key for encryption. ZANEI_STORE_KEY_FILE=<path> makes every Zanei process read the key from that file instead. It is a development override for builds from source, described in the CLI reference.

Was this page helpful?