Hooks per runtime

What this layer does

Phase 3 wires push --auto and pull --auto into every runtime's session-lifecycle so Joe never thinks about sync. Stop hook fires push, SessionStart hook fires pull. Runtimes without native hooks (Gemini, OpenClaw) get covered by a 5-minute cron tick.

Files involved

snappy-os doctor self-test at 30 minutes past every 6 hours.

settings.json to ~/.claude/_backups/settings-<ts>.json before every change.

if any local OR remote change since last tick.

per-runtime instruction files when CLAUDE.md changes.

Canonical hook order on Claude Code Stop

hooks.Stop = [auto-regen-skills.sh, sync-runtimes.ts, push.sh --auto]

bin/wire-hooks.js enforces this order. Any pre-existing entries in the wrong order get re-ordered after backup. Push runs LAST so any PID rewrite or runtime regen lands in the same upload.

Per-runtime mechanism

RuntimeMechanismStopSessionStart
Claude CodeNativeappend push --auto to canonical Stop arraynew SessionStart entry runs pull --auto
Codex CLINativeappend push --autoappend pull --auto to existing SessionStart matcher
Gemini CLICroncovered by cron-tick.shcovered
OpenClawCroncovered by cron-tick.shcovered
Cursorn/a (rules-file only)n/an/a
Windsurfn/an/an/a

Idempotency

Re-running bootstrap N times must leave hook arrays the same length. wire-hooks.js dedupes by exact command string match. Smoke step C.1 / step 14 asserts:

LEN1=$(npx snappy-os && jq '.hooks.Stop | length' ~/.claude/settings.json)
LEN2=$(npx snappy-os && jq '.hooks.Stop | length' ~/.claude/settings.json)
[ "$LEN1" = "$LEN2" ] || echo FAIL

Operational gotchas

filename includes the timestamp so multiple runs leave a trail.

append, do not overwrite, the matcher's command list.

catches up — sync is idempotent, never additive.

short-circuits within 60s. A stop hook firing twice in a session produces one push + one debounced row.

How to verify it's working

array in the documented order.

state/log/sync-events.ndjson with trigger: "stop-hook".

snappy-os doctor entries.

jq '.hooks.Stop | length' ~/.claude/settings.json unchanged.