Files
msd-core/docs/features/hooks-declare-their-crash-policy.md
Jakub Zych 6cfa0c55d2 refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity
Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes,
cline, codebuddy and pi end to end: capability descriptors, installer branches
and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters,
hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi
migrations, Kimi payload normalization in the hook guards, dead hostBehaviors
vocabulary, launcher home probes, fixtures, runtime-specific tests and the
prose that presented them as supported.

Installer output for the six kept runtimes is byte-identical to before the
prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and
read-injection-scanner are left in place pending a decision.
2026-10-06 20:02:40 +02:00

58 lines
3.2 KiB
Markdown

---
id: 3911
title: Hooks Declare Their Crash Policy
group: v1.7.0 Features
---
**Purpose:** Give every shipped enforcement hook (`hooks/*.js`, `hooks/*.sh`) a
named, auditable termination vocabulary instead of a bare `process.exit(N)`
scattered per file — and make a hook's fail-open/fail-closed choice a
declaration a reviewer can see, rather than an inference from which literal
integer follows `process.exit(` in its outer `catch`.
**What changed (ADR-3889 Phase 7, #3911):**
- `hooks/lib/hook-exit.js` (hand-written) exposes `allow(payload)` → exit 0,
`deny(payload, stderrPayload?)` → exit 2, and `crash(onCrash, payload)`,
which dispatches to `allow`/`deny` per a `HOOK_ON_CRASH` policy the caller
must supply — `crash()` has no default policy, so a hook cannot fail open
by omission.
- Every one of the 19 enforcement hooks under `hooks/*.js` now declares
`const ON_CRASH = HOOK_ON_CRASH.ALLOW` (or `DENY`) once, with a
hook-specific comment naming why, and calls `crash(ON_CRASH, payload)` from
its outer catch instead of a bare `process.exit(0)` / `process.exit(2)`. No
hook's effective exit code changed — this is a naming-and-declaration
migration, not a behavior change.
- `hooks/lib/cli-exit.js` and `hooks/lib/exit-code-registry.js` are new,
generated, git-tracked copies of the exit-code seam (`src/cli-exit.cts` /
`msd-core/bin/shared/exit-codes.json`), so a shipped hook can terminate
correctly on a raw, unbuilt clone without depending on `msd-core/bin/lib/`
tsc output. Generated by `scripts/gen-hooks-cli-exit.cjs` and
`scripts/gen-exit-code-registry.cjs`, both `--check`ed by
`npm run lint:generated-sync`.
- `terminateNow` gained an optional third argument, `stderrPayload`, so a
deny can send a full JSON body to stdout and a distinct plain-text reason
to stderr — needed because `msd-write-guard.js` (a host's native hook bus
may read stderr verbatim back to the model) always sent only the bare reason
string on fd 2. The two streams are now written in independent try/catch
blocks: previously a payload that failed to serialize on fd 1 aborted
before fd 2 ever wrote, producing a deny with an empty stderr reason.
- Two hooks are deliberately **not** migrated to `deny()`:
`msd-read-injection-scanner.js` (PostToolUse — its harness reads the block
decision from the JSON response body, not the exit code) and
`msd-cursor-subagent-start.js` (follows Cursor's own `subagentStart`
protocol, which reads `permission: "deny"` from the JSON body at exit 0).
Both still use `allow()`/`crash()` for their no-op and crash paths.
- `msd-phase-boundary.sh`, `msd-session-state.sh`, and
`msd-validate-commit.sh` gained `set -euo pipefail`, and
`msd-validate-commit.sh`'s three swallow-and-pass sites (the opt-in config
read, JSON command extraction, and the `isGitSubcommand` classifier) now
distinguish a genuine negative from "could not run" — on the latter they
emit a stderr diagnostic and exit 0 instead of silently allowing every
commit (#3838).
See [Declare a hook's crash policy](../how-to/declare-a-hook-crash-policy.md)
for the full how-to, and
[ADR-3889](../adr/3889-process-exit-contract.md) for the exit-code registry
this vocabulary is layered over.