# shell-hooks Shell side of the terminal collector (spec §1.4). **Live as of v0.2.** These snippets belong in the dotfiles repo and are sourced by the user's `.zshrc`; they are kept here so the privacy contract lives next to the code it constrains (and so the differential secret-typing test can drive the real file). ## The one rule **Aggregate-only.** The hooks emit counts, durations, and exit codes. They never read, store, or transmit the content of a command or a keystroke. - No input tap, no `CGEventTap`, no PTY sniffing. - Never reference the `zle` line buffer or capture argv. - What leaves the shell is a number, flushed on `precmd`. v0.2 ships the `zle` keypress counter: a widget wraps `self-insert`, does `(( _SIGNALD_KEYS++ ))`, then calls the built-in insert. It receives the key in the editor and discards it — the character is never assigned to a variable that outlives the widget and never leaves the shell. On each `precmd` the hook appends one count record — ` `, numbers only — to `$SIGNALD_SPOOL`. The session id is the shell's pid, so several shells can share one spool and `signald` still derives each shell's rate separately. `signald` consumes the spool on every tick (renames it aside, reads it, deletes it), so it never grows. This contract is enforced by the forbidden-symbol scan **and** the differential secret-typing test in `crates/signal-schema/tests/privacy_invariant.rs` (plus the full-pipeline gate in `crates/signald/tests/`), which drive *this file* with a planted secret and fail the build if it — in any encoding — reaches the spool, the wire, or SQLite. ## Files - `signald-hooks.zsh` — the `zle` keystroke counter + `precmd` count flush (aggregate-only).