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