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

3.2 KiB

id, title, group
id title group
3911 Hooks Declare Their Crash Policy 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 --checked 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 for the full how-to, and ADR-3889 for the exit-code registry this vocabulary is layered over.