Event reference
The event taxonomy, the JSON envelope shared by all events, and per-browser URL capture support.
Every event Tadoru 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; 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 (field edits, …) — content only when text_content is on |
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 deliberately 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 — content itself is in element.value, opt-in |
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.
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 | typed string, opt-in only | 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 never 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 |
element | 1s debounce | only the final value is emitted |
Shortcuts and ui.click never coalesce (one action = one event). Coalescing buffers are always flushed on pause, stop, and batch flush — no events are lost.
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 (after redaction) |
input.key.text |
always null | recorded (after redaction) |
input.key.combo (shortcuts) |
recorded | recorded |
clipboard.*.text / size_bytes |
always null | recorded (after redaction) |
ui.value.value_len |
recorded | recorded |
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. The reason is a privacy guarantee: Chromium exposes each window’s mode (normal / incognito) deterministically via AppleScript, which lets Tadoru make a hard promise — Incognito windows are always excluded from URL capture, with no configuration knob needed.
| 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 deliberately 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, but only if a genuinely reliable private-mode detection mechanism exists.
Two layers of data
- Raw event layer — complete and machine-readable; what
query,record, andexportreturn. - Timeline layer — human/LLM-readable; session splitting, dedup, coalescing, token-budgeted serialization. What
timelineandget_timelinereturn.
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.