# signald-hooks.zsh โ€” terminal collector, shell side (spec ยง1.4). # # A small sourced script that lives in the dotfiles repo and is sourced by the # user's .zshrc. It talks to signald by appending newline-delimited AGGREGATE # COUNT records to a spool file that signald reads. It holds no history. # # ============================ PRIVACY CONTRACT ============================ # AGGREGATE-ONLY. This script emits COUNTS, DURATIONS, and EXIT CODES. # It NEVER reads, stores, or transmits the content of a command or a keystroke. # # - No input tap (none of the global event-tap / HID keyboard APIs). No # PTY sniffing. # - The keypress counter is a zle widget that increments a NUMBER and 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. What leaves is a count. # - It NEVER references the zle line buffer (the BUFFER/LBUFFER/RBUFFER zle # parameters) and NEVER captures argv. The forbidden-symbol CI scan # (crates/signal-schema/tests/privacy_invariant.rs) fails the build if it # ever does. That test โ€” plus the differential secret-typing test โ€” drives # THIS FILE with a planted secret and asserts the secret never reaches the # spool, the wire, or SQLite. # # Spool record format (all fields are NUMBERS, space-separated): # # # # One record is appended on each precmd (i.e. after each command line). There is # no field capable of carrying typed content. session_id is this shell's pid, # so the daemon keeps each shell's rate separate when several append to the # same spool. The daemon consumes the spool (renames it aside and deletes it), # which is why every write opens the file afresh with >>. # ========================================================================= zmodload zsh/datetime 2>/dev/null # Spool the daemon reads. Override SIGNALD_SPOOL to point elsewhere. : ${SIGNALD_SPOOL:=${XDG_RUNTIME_DIR:-$HOME/.local/state/signald}/terminal.spool} # --- session start: a timestamp only --- typeset -g _SIGNALD_SESSION_START=${EPOCHSECONDS:-0} typeset -g _SIGNALD_KEYS=0 # Keypress counter: increment a number, then perform the normal insert. The key # is handled by `.self-insert` and is never captured here. No BUFFER/LBUFFER. _signald_self_insert() { (( _SIGNALD_KEYS++ )) zle .self-insert } zle -N self-insert _signald_self_insert # precmd: the previous command finished. Append ONE aggregate record (numbers # only) and reset the per-flush key counter. _signald_precmd() { local now_ms=$(( ${EPOCHREALTIME:-$EPOCHSECONDS} * 1000 )) local session=$(( ${EPOCHSECONDS:-0} - _SIGNALD_SESSION_START )) mkdir -p ${SIGNALD_SPOOL:h} 2>/dev/null print -r -- "${now_ms%.*} ${_SIGNALD_KEYS} ${session} $$" >> $SIGNALD_SPOOL _SIGNALD_KEYS=0 } autoload -Uz add-zsh-hook add-zsh-hook precmd _signald_precmd