## Public Exports

Exports enumerated from `grep -E '^export ' extension/src/hooks/*.ts` (top-level files only; `handlers/` sub-directory is covered under Handler Invariants below).

### `resolve-state.ts`
- `sameWorkingDir(a, b)` — compare canonical realpaths for hook cwd matching
- `selectScannedStateFile(stateFiles, cwd)` — choose the best matching state file from a scanned list
- `resolveStateFile(dataDir)` — resolve a single authoritative state file path
- `loadActiveState(stateFile)` — load and validate a `State` object from disk
- `approve()` — emit the hook approve response to stdout

### `dispatch.ts`
CLI entry point — no named exports. Spawns the appropriate handler subprocess based on hook event type.

---

## Handler Invariants

Summary of security-critical invariants for each handler. Full detail is in the `## Trap Doors` entries below; cross-references are noted per handler.

### `handlers/config-protection.ts`
- **Bash config-guard**: glob, brace (`{}`), bracket (`[]`), and `?` patterns in shell commands must be matched against protected config basenames to prevent bypass. See trap-door entry for `handlers/config-protection.ts` (glob/brace invariant).
- **`PROTECTED_WRITE_GLOBS` state-file write blocking**: `Write`/`Edit` tools and bash output-redirects (`>`, `>>`, `tee`, `cp`, `mv`, `rsync`) targeting state files are blocked unless `state.flags.allow_state_writes_reason` is set. See trap-door entry for `handlers/config-protection.ts` (R-WSRC-3).
- **R-WSRC-GR git-verb blocker** (`detectProhibitedGitVerb`): `git reset`, `switch`, `stash`, `rebase`, `pull`, `push`, `checkout-with-ref`, `commit --amend`, and `fetch --prune` are blocked in worker Bash. See trap-door entry for `handlers/config-protection.ts` (R-WSRC-GR).
- **Chained-command segmentation**: the worker-forbidden-op detectors (`detectProhibitedGitVerb`, `isBashInvokingInstallSh`, `extractNodeTestPath`) evaluate every `splitShellSegments` segment, not just the leading token — `cd sub && git reset` cannot bypass them. `splitShellSegments` treats `&&`, `||`, `|`, `&`, `;`, AND an unquoted newline as boundaries, so `git status\ngit reset --hard` is segmented too. See trap-door entries for `handlers/config-protection.ts` (chained-command detection) and (newline segmentation).

### `handlers/stop-hook.ts`
- **tmux passthrough APPROVE**: when `state.tmux_mode === true`, the hook approves unconditionally so launcher conversations do not block tmux-owned loops. See trap-door entry for `src/hooks/handlers/stop-hook.ts` (tmux passthrough).
- **Idle-backoff progression**: after 3 consecutive degenerate wait-pattern manager turns, nudges are suppressed until state mtime / worker artifact mtime / liveness changes or the bounded fallback timer fires. See trap-door entry for `stop-hook.ts (idle backoff)`.
- **Update-cadence conversion**: `update_check_interval_hours` is multiplied by `3600` before rate-limit comparison — not divided. See trap-door entry for `stop-hook.ts (update cadence)`.
- **`Decision` type**: always `'approve' | 'block'` — the string `'allow'` is never a valid decision value. See trap-door entries and `Decision` type at `handlers/stop-hook.ts:66`.

### `handlers/tsc-gate.ts`
- Gate fires **only on git-commit-class Bash commands** (`isGitCommitCommand`), evaluated across every top-level shell segment so chained `git add … && git commit` still triggers it. Non-commit commands are passed through without a tsc check.
- `allow_tsc_failed_reason` override is **consumed on the next clean commit** — the flag auto-clears via `tsc_gate_override_consumed`.
- See trap-door entry for `handlers/tsc-gate.ts`.

---

## Trap Doors

- `../bin/log-commit.ts` — INVARIANT: every successful `PostToolUse` in an active session clears `last-tool-error.json` before commit parsing. BREAKS: old tool failures poison later worker spawns with stale retry-circuit guidance. ENFORCE: `extension/tests/log-commit.test.js`. PATTERN_SHAPE: PostToolUse hook resolves same-cwd active session and leaves `last-tool-error.json` untouched.
- `handlers/config-protection.ts` — INVARIANT: Bash config guards must match shell-expanded protected basenames across `*`, `?`, `[]`, and `{}` patterns. BREAKS: glob, bracket, or brace commands can bypass protected-file blocking. ENFORCE: `extension/tests/config-protection.test.js`. PATTERN_SHAPE: `tool_name==='Bash'` token with shell pattern chars matching a protected config basename.
- `handlers/config-protection.ts` (R-WSRC-3) — INVARIANT: PreToolUse hook MUST block `Write`/`Edit` tool calls AND bash output-redirects targeting `**/state.json*`, `**/circuit_breaker.json*`, `**/pipeline-status.json*`, `~/.claude/pickle-rick/**`, and `pickle_settings.json*` UNLESS `state.flags.allow_state_writes_reason` (or `allow_settings_writes_reason` for settings) is a non-empty trimmed string (which emits `state_write_override_used` per bypass). Hook fails open on scanner crash per dispatch.js contract. BREAKS: workers retain ad-hoc write access to runtime state files, enabling R-WSRC corruption class. ENFORCE: `extension/tests/config-protection-state-files.test.js`. PATTERN_SHAPE: `PROTECTED_WRITE_GLOBS` constant in source MUST include the named globs; `PROTECTED_BASH_CANDIDATES` extended scanner MUST match `>`, `>>`, `tee`, `cp`, `mv`, `rsync` targets against the same globs.
- `handlers/config-protection.ts` (R-WSRC-3 home-expansion) — INVARIANT: `isInsideRuntimeRoot` calls `expandLeadingHome` before `path.resolve` so `~`/`$HOME` write targets under `~/.claude/pickle-rick/**` are contained. BREAKS: `echo x > ~/.claude/pickle-rick/extension/bin/mux-runner.js` slips the guard because `path.resolve('~/...')` yields `<cwd>/~/...`. ENFORCE: `extension/tests/config-protection-state-files.test.js`. PATTERN_SHAPE: `path.resolve(expandLeadingHome(filePath))` in `isInsideRuntimeRoot`.
- `handlers/config-protection.ts` (chained-command detection) — INVARIANT: `detectProhibitedGitVerb`, `isBashInvokingInstallSh`, and `extractNodeTestPath` MUST evaluate every `splitShellSegments(command)` segment, not just the leading executable token. BREAKS: `cd sub && git reset`, `git status && git push`, `cd x && bash install.sh`, and `cd x && node --test <expensive>` slip the worker-forbidden-op guards (the CLAUDE.md-mandated `cd <subdir> && git <verb>` pattern makes chaining the common case). `splitShellSegments` is quote-aware so a commit message containing `&& git reset` is not mis-segmented. ENFORCE: `extension/tests/hooks/config-protection-git-boundary.test.js`. PATTERN_SHAPE: `splitShellSegments(` consumed by all three leading-command detectors; no detector reads `parseFirstShellWord(command)`/`tokens[0]` of the raw unsegmented command.
- `handlers/config-protection.ts` (R-WSRC-GR option-arg) — INVARIANT: `findGitVerb` MUST skip a space-separated arg-consuming git global option AND its value token (`ARG_CONSUMING_GIT_GLOBAL_OPTIONS`: `-C`, `-c`, `--git-dir`, `--work-tree`, `--namespace`, `--super-prefix`, `--exec-path`) before reading the verb. BREAKS: `git -C <path> reset --hard` reads the `-C` value as the verb → `null` → the destructive reset slips the R-WSRC-GR guard (proven null pre-fix; same option-arg handling already lives in tsc-gate.ts:segmentIsGitCommit). ENFORCE: `extension/tests/hooks/config-protection-git-boundary.test.js`. PATTERN_SHAPE: `ARG_CONSUMING_GIT_GLOBAL_OPTIONS.has(rest[i])` skip-with-value in `findGitVerb`.
- `handlers/config-protection.ts` (quoted-token parity) — INVARIANT: the R-WSRC-GR git-verb chain MUST tokenize each segment quote-aware via `tokenizeGitCommand` (strips matching surrounding quotes) in BOTH `parseFirstShellWord` (the gate) and `findGitVerb` (the verb read). BREAKS: a bare `split(/\s+/)` reads `"git"`/`"reset"` with quotes attached, so `git "reset" --hard` and `"git" reset --hard` (the shell runs both as `git reset --hard`) slip the guard. Mirrors tsc-gate.ts:segmentIsGitCommit which already strips quotes. ENFORCE: `extension/tests/hooks/config-protection-git-boundary.test.js`. PATTERN_SHAPE: `tokenizeGitCommand(` used by both `parseFirstShellWord` and `findGitVerb`; no `split(/\\s+/)` in either.
- `handlers/config-protection.ts` (newline segmentation) — INVARIANT: `splitShellSegments` MUST treat an unquoted newline as a segment boundary (`'\n'` ∈ `SHELL_SEGMENT_SEPARATORS`; the tokenizer matches `\n` before `\S+`). BREAKS: `git status\ngit reset --hard`, `git add\n…\ngit reset`, `cd x\nbash install.sh`, and `cd x\nnode --test <expensive>` collapse to one segment and slip the worker-forbidden-op guards (a worker emits sequential commands one per line). Quoted multi-line commit messages stay one token. ENFORCE: `extension/tests/hooks/config-protection-git-boundary.test.js`. PATTERN_SHAPE: `'\n'` present in `SHELL_SEGMENT_SEPARATORS` AND `|\n|` alternative in the `splitShellSegments` tokenizer regex.
- `handlers/tsc-gate.ts` — INVARIANT: only git-commit-class Bash commands trigger the gate; blocking failures emit `tsc_gate_failed`, override bypasses emit `tsc_gate_override_used`, and clean flagged commits auto-clear `allow_tsc_failed_reason` with `tsc_gate_override_consumed` via `StateManager.update`. BREAKS: broken-tsc commits slip through or the manager override persists past the next clean commit. ENFORCE: `extension/tests/tsc-gate.test.js`. PATTERN_SHAPE: `isGitCommitCommand`
- `handlers/tsc-gate.ts` (chained-command detection) — INVARIANT: `isGitCommitCommand` MUST test every `splitTopLevelSegments` segment, not just the cd-stripped leading command, so `git add -A && git commit` (the CLAUDE.md-canonical commit form) still triggers the gate. BREAKS: chained add+commit skips the R-WACT tsc backstop, letting broken-TS commits through. ENFORCE: `extension/tests/tsc-gate.test.js`. PATTERN_SHAPE: `splitTopLevelSegments(command).some(segmentIsGitCommit)` in `isGitCommitCommand`.
- `handlers/tsc-gate.ts` (option-arg parity) — INVARIANT: `segmentIsGitCommit` MUST skip a space-separated arg-consuming git global option AND its value token (`ARG_CONSUMING_GIT_GLOBAL_OPTIONS`: `-c`, `-C`, `--git-dir`, `--work-tree`, `--namespace`, `--super-prefix`, `--exec-path`) plus the `=`-glued forms before reading the subcommand. BREAKS: `git --namespace ns commit` / `--super-prefix sp/ commit` / `--exec-path /p commit` read the option VALUE as the subcommand → classified non-commit → the R-WACT tsc gate is SKIPPED for a broken-TS commit (proven; parity gap vs config-protection.ts:findGitVerb which already covers all 7). ENFORCE: `extension/tests/tsc-gate.test.js`. PATTERN_SHAPE: `ARG_CONSUMING_GIT_GLOBAL_OPTIONS.has(token)` skip-with-value (`index += 2`) in `segmentIsGitCommit`.
- `handlers/tsc-gate.ts` (boolean-flag parity) — INVARIANT: `segmentIsGitCommit` MUST skip ANY leading `-`-prefixed flag before reading the subcommand (a non-arg-consuming git global boolean option `--no-pager`/`-p`/`--paginate`/`--no-optional-locks`/`--bare`, or an `=`-glued arg-option), after first consuming an `ARG_CONSUMING_GIT_GLOBAL_OPTIONS` value. BREAKS: `git --no-pager commit` reads the boolean flag as the subcommand → classified non-commit → the R-WACT tsc gate is SKIPPED for a broken-TS commit (proven; findGitVerb skips ALL flags so this was a one-sided parity gap). ENFORCE: `extension/tests/tsc-gate.test.js`. PATTERN_SHAPE: generic `if (token.startsWith('-')) { index += 1; continue; }` after the `ARG_CONSUMING_GIT_GLOBAL_OPTIONS` skip in `segmentIsGitCommit`, with no per-option `startsWith('--git-dir=')` allowlist.
- `handlers/tsc-gate.ts` (env-prefix parity) — INVARIANT: `segmentIsGitCommit` MUST skip leading `NAME=value` env-var assignments before reading the `gh`/`git` token, mirroring `findGitVerb`/`parseFirstShellWord`. BREAKS: `GIT_COMMITTER_DATE=… git commit` reads the assignment as tokens[0], fails the `=== 'git'` check, classifies non-commit → R-WACT tsc gate SKIPPED for a broken-TS commit (one-sided parity gap; config-protection catches `GIT_DIR=x git reset`). ENFORCE: `extension/tests/tsc-gate.test.js`. PATTERN_SHAPE: `while (gitIdx < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[gitIdx])) gitIdx++;` before the `gh`/`git` token check in `segmentIsGitCommit`.
- `handlers/tsc-gate.ts` (newline segmentation) — INVARIANT: `splitTopLevelSegments` MUST treat an unquoted newline as a segment boundary (`'\n'` ∈ `SHELL_SEGMENT_SEPARATORS`; the tokenizer matches `\n` before `\S+`). BREAKS: `git add -A\ngit commit` collapses to one segment whose subcommand reads `add`, skipping the R-WACT tsc gate so a broken-TS commit slips through (a worker emits sequential commands one per line). Quoted multi-line commit messages stay one token. ENFORCE: `extension/tests/tsc-gate.test.js`. PATTERN_SHAPE: `'\n'` present in `SHELL_SEGMENT_SEPARATORS` AND `|\n|` alternative in the `splitTopLevelSegments` tokenizer regex.
- `handlers/config-protection.ts` (R-WSRC-3 redirect tokenizer) — INVARIANT: `tokenizeBashCommand` isolates every output-redirect-to-file operator BEFORE the general `>` pass so the dest is never glued: `>|` (clobber-override) and `>&<filename>` (the `&>`-equivalent dup-to-file write) both normalize to ` > `. The `>&` normalization uses negative lookahead `(?![\d-])` so it fires ONLY on the file-write form (`>&state.json`, `>& state.json`) and leaves fd-dup/close untouched (`2>&1`, `>&2`, `>&-`, `1>&2` must NOT be treated as writes or legitimate redirections false-block). BREAKS: a glued redirect operator hides the protected basename and bypasses the state-write guard; dropping the `(?![\d-])` lookahead false-blocks ubiquitous fd-dup redirections. ENFORCE: `extension/tests/config-protection-state-files.test.js`. PATTERN_SHAPE: `.replace(/>\|/g, ' > ')` AND `.replace(/>&(?![\d-])/g, ' > ')` both present before the `(^|[^>])>` pass in `tokenizeBashCommand`.
- `resolve-state.ts` — INVARIANT: hook cwd matching must compare canonical realpaths via the `sameWorkingDir` helper, not raw `path.resolve()` strings. BREAKS: `/var` vs `/private/var` aliases make active sessions look foreign, so config-protection/tool-error fail open and stop-hook exits early. ENFORCE: `extension/tests/resolve-state.test.js`, `extension/tests/stop-hook.test.js`. PATTERN_SHAPE: `sameWorkingDir(` on hook/session lookup paths.
- `resolve-state.ts` (lookup precedence) — INVARIANT: the exported helpers compose a strict order — `selectScannedStateFile` → `resolveStateFile` → `loadActiveState` — and `loadActiveState` is the single authority feeding the hook `approve` decision shape. Stale-map / dead-pid / wrong-cwd / inactive sessions must demote BELOW a live same-cwd fallback before `approve` returns. BREAKS: skipping any helper makes config-protection / tsc-gate / stop-hook fail open or attach to dead owners. ENFORCE: `extension/tests/resolve-state.test.js`, `extension/tests/config-protection.test.js`, `extension/tests/stop-hook.test.js`. PATTERN_SHAPE: `selectScannedStateFile|resolveStateFile|loadActiveState|approve`.
