Skip to content
Zanei
English
Esc
navigateopen⌘Jpreview
On this page

Event reference

The event taxonomy, the JSON envelope shared by all events, and per-browser URL capture support.

Every event Zanei records — regardless of source — shares one OS-independent JSON envelope. The stable schema is the core asset of the project: capture backends may change per OS, but the shape your tools consume does not.

Event envelope

One event per line (NDJSON) in raw outputs (query --format jsonl, record, export):

{
  "v": 1,
  "id": "evt_01J...",
  "ts": "2026-08-16T12:34:56.789Z",
  "mono_ns": 128374651234,
  "source": "macos.ax",
  "type": "window.focus",
  "app":    { "name": "Safari", "bundle_id": "com.apple.Safari", "pid": 501 },
  "window": { "title": "Design doc", "id": 42 },
  "element":{ "role": "AXButton", "title": "Send", "value": null },
  "data":   { },
  "redaction": { "applied": true, "rules": ["email"] }
}
Field Description
v Envelope schema version (currently 1)
id ULID-based event ID, unique and time-sortable
ts Wall-clock timestamp (RFC3339, millisecond precision)
mono_ns Monotonic clock in nanoseconds — reliable ordering even across wall-clock changes
source Capture backend, e.g. macos.ax, macos.workspace, macos.eventtap, macos.applescript
type Event type (see the taxonomy)
app App name, bundle ID, and PID
window Window title and ID (when applicable)
element UI element role/title/value (when applicable; value is limited to allowlisted known-safe non-text controls, unknown elements have value: null, and secure fields never appear)
data Type-specific payload, e.g. url / tab_title / mode for browser.navigate
redaction Whether redaction rules (email / credit_card / token) were applied to this event, and which. Secure fields leave no redaction trace — they are excluded before capture

This envelope is also published as a machine-readable JSON Schema at /schema/event.schema.json — the single contract all surfaces share, and the file the Rust core’s types are tested against.

Event taxonomy

The current event types. The Permission column shows what must be granted for the type to be captured (see the permissions guide).

Type Source What it records Permission
app.activate macos.workspace Frontmost app changed None
app.launch / app.terminate macos.workspace App started / quit None
window.focus macos.ax Focused window changed Accessibility
window.title macos.ax Window title changed Accessibility
ui.focus macos.ax Focused UI element changed Accessibility
ui.click macos.ax Click on a UI element Accessibility (+ Input Monitoring)
ui.value macos.ax Element value changed; newly added input is opt-in and never includes the full free-text value Accessibility
input.key macos.eventtap Key / shortcut. Default: fact of typing + field type; content is opt-in Input Monitoring
input.scroll macos.eventtap Scrolling Input Monitoring
browser.navigate macos.applescript URL / tab change. Chrome only, currently; Incognito always excluded Automation
clipboard.copy / clipboard.paste macos.eventtap Clipboard use; content is opt-in Input Monitoring

In --types filters, a trailing wildcard selects a whole family: browser.*, ui.*, and so on.

type is an open set, not a closed enum: future platforms and versions may add types without a version bump, and consumers must skip unknown types rather than error.

Per-type payloads

What each type carries in data, and whether the envelope’s window / element are present (✓) or null (—). The normative definitions live in the JSON Schema as per-type conditions.

Type window element data
app.activate prev_bundle_id — previously frontmost app, null on first event
app.launch / app.terminate empty
window.focus empty (the focused window is in the envelope)
window.title prev_title — the new title is in the envelope
ui.focus field_kind
ui.click button (left/right/other), click_count
ui.value field_kind, value_len, text. text is the newly added authorized difference, or null; element.value is never recorded for free-text or unknown fields
input.key kind, modifiers, count, combo, text, field_kind (details below)
input.scroll direction, amount, count — coalesced totals
browser.navigate url, tab_title, mode (always "normal"), transition (navigate/tab_switch/null)
clipboard.copy content_kind (text/image/file/other), size_bytes, text
clipboard.paste same as copy, plus field_kind of the paste target

field_kind classifies the focused input field: text / search / url / email / number / other, or null when focus is not on a text-like element. There is no password value — secure fields are excluded before an event exists.

ui.value in detail

For a free-text field, data.text contains only the newly added difference after an authorized input. Authorization requires a keystroke or paste from the same app, for the same focused-element generation, with the value-change notification timestamp no more than 3 seconds after that input. One authorization can permit at most one emission and is consumed when used. text_content must also be enabled. The field’s complete value is never stored in element.value.

A value change without valid authorization, including an app mismatch, focused-element generation mismatch, or expired authorization, is not recorded as text: data.text is null and only value_len describes the resulting value. Deletion likewise has text: null. Voice input has no keystroke trigger, so its text is not recorded.

input.key in detail

The exact shape of “fact of typing + field type by default, content opt-in”:

kind Meaning combo text Coalesced
text Printable typing null characters produced directly by a keyboard-layout input source, opt-in only; null for keyboard input modes and when the input-source type is unknown or unavailable yes
shortcut Modifier combo (cmd+s, …) always recorded null no
navigation Arrows, PageUp/Down, Home/End, Tab null null yes
delete Backspace / Delete null null yes
other Esc, F-keys, media keys null null no

A shortcut is an action, not content — so combo is recorded without opt-in. It is a primary timeline ingredient (saves, commits, tab switches).

Coalescing guarantees

Events are coalesced before they reach the store, so consumers do not see raw key-repeat or scroll floods:

Events Grouped by Window Result
input.key (text/navigation/delete) app + window + field_kind + kind gap ≤ 2s one event; count summed, text concatenated (opt-in)
input.scroll app + window + direction gap ≤ 1s amount and count summed
window.title window 500ms debounce only the final title is emitted
ui.value focused element 1s collector debounce one event for the final value; text is the added difference between that value and the pre-input baseline

Shortcuts and ui.click do not coalesce (one action = one event). Core coalescing buffers are flushed on pause, stop, and batch flush. The collector-side ui.value buffer is flushed on focus change and collector stop.

What text_content changes

Only content (what you typed or copied) is opt-in. Facts of use (shortcuts, clicks, URLs, titles) are recorded by default:

Field Default (false) Opt-in (true)
element.value always null recorded only for allowlisted known-safe non-text controls (such as buttons, checkboxes, radio buttons, sliders, pop-up buttons, menu items, tabs, and short static text); null for free-text, secure, and unknown elements
input.key.text always null characters produced directly by a keyboard-layout input source (after redaction); null for keyboard input modes and when the input-source type is unknown or unavailable
input.key.combo (shortcuts) recorded recorded
clipboard.*.text / size_bytes always null recorded (after redaction)
ui.value.value_len recorded recorded
ui.value.data.text always null newly added authorized input difference (after redaction); null unless the same app and focused-element generation receive a value-change notification within 3 seconds of the input; authorization is consumed on emission
window.title / tab_title / url recorded (after redaction) recorded

Schema versioning

  • Additive changes (new fields, new types, new sources) do not bump v. Consumers must ignore unknown fields and skip unknown types.
  • Breaking changes (removal, rename, meaning change) bump v, with a store migration to match.

Browser URL capture

browser.navigate (URL capture) currently supports Chrome only. Chromium exposes each window’s mode (normal / incognito) deterministically via AppleScript, so Incognito windows can be excluded from URL capture without a configuration knob.

Browser URL capture (current) Private windows
Google Chrome ✅ Supported mode detects Incognito deterministically → always excluded
Brave / Edge / Vivaldi (Chromium family) ❌ Not supported — the collector only launches for Chrome’s bundle ID No URL capture, so no private-window handling applies
Safari ❌ Not supported No private-mode property; menu-diff heuristics are brittle, so not attempted
Firefox ❌ Not supported AppleScript has no URL API
Arc ❌ Not supported (unverified) Chromium-based but limited AppleScript support

Safari/Firefox support may come later if a reliable private-mode detection mechanism exists.

Two layers of data

  • Raw event layer — complete and machine-readable; what query, record, and export return.
  • Timeline layer — human/LLM-readable; session splitting, dedup, coalescing, token-budgeted serialization. What timeline and get_timeline return.

Timeline sessions carry event_ids back-references to the underlying raw events. They are included only in the JSON format (--format json), and are dropped when the output must be coarsened to fit the token budget.

Was this page helpful?