Ambient system companions over one privacy-preserving signal daemon (aggregate-only, no keystroke content): a git-driven terminal garden and IOKit hardware collectors. ambient daemon macos privacy terminal

shell-hooks/signald-hooks.zsh

60 lines · 2731 bytes

 1# signald-hooks.zsh — terminal collector, shell side (spec §1.4).
 2#
 3# A small sourced script that lives in the dotfiles repo and is sourced by the
 4# user's .zshrc. It talks to signald by appending newline-delimited AGGREGATE
 5# COUNT records to a spool file that signald reads. It holds no history.
 6#
 7# ============================ PRIVACY CONTRACT ============================
 8# AGGREGATE-ONLY. This script emits COUNTS, DURATIONS, and EXIT CODES.
 9# It NEVER reads, stores, or transmits the content of a command or a keystroke.
10#
11#   - No input tap (none of the global event-tap / HID keyboard APIs). No
12#     PTY sniffing.
13#   - The keypress counter is a zle widget that increments a NUMBER and then
14#     calls the built-in insert. It receives the key in the editor and discards
15#     it; the character is never assigned to a variable that outlives the widget
16#     and never leaves the shell. What leaves is a count.
17#   - It NEVER references the zle line buffer (the BUFFER/LBUFFER/RBUFFER zle
18#     parameters) and NEVER captures argv. The forbidden-symbol CI scan
19#     (crates/signal-schema/tests/privacy_invariant.rs) fails the build if it
20#     ever does. That test — plus the differential secret-typing test — drives
21#     THIS FILE with a planted secret and asserts the secret never reaches the
22#     spool, the wire, or SQLite.
23#
24# Spool record format (all fields are NUMBERS, space-separated):
25#
26#     <epoch_ms> <keys_since_last_flush> <session_seconds>
27#
28# One record is appended on each precmd (i.e. after each command line). There is
29# no field capable of carrying typed content.
30# =========================================================================
31
32zmodload zsh/datetime 2>/dev/null
33
34# Spool the daemon reads. Override SIGNALD_SPOOL to point elsewhere.
35: ${SIGNALD_SPOOL:=${XDG_RUNTIME_DIR:-$HOME/.local/state/signald}/terminal.spool}
36
37# --- session start: a timestamp only ---
38typeset -g _SIGNALD_SESSION_START=${EPOCHSECONDS:-0}
39typeset -g _SIGNALD_KEYS=0
40
41# Keypress counter: increment a number, then perform the normal insert. The key
42# is handled by `.self-insert` and is never captured here. No BUFFER/LBUFFER.
43_signald_self_insert() {
44  (( _SIGNALD_KEYS++ ))
45  zle .self-insert
46}
47zle -N self-insert _signald_self_insert
48
49# precmd: the previous command finished. Append ONE aggregate record (numbers
50# only) and reset the per-flush key counter.
51_signald_precmd() {
52  local now_ms=$(( ${EPOCHREALTIME:-$EPOCHSECONDS} * 1000 ))
53  local session=$(( ${EPOCHSECONDS:-0} - _SIGNALD_SESSION_START ))
54  mkdir -p ${SIGNALD_SPOOL:h} 2>/dev/null
55  print -r -- "${now_ms%.*} ${_SIGNALD_KEYS} ${session}" >> $SIGNALD_SPOOL
56  _SIGNALD_KEYS=0
57}
58
59autoload -Uz add-zsh-hook
60add-zsh-hook precmd _signald_precmd