Reject any import/require outside vigiles/hook + textual escape hatches
(eval / Function / dynamic import). The hook's capabilities are then EXACTLY the
sanctioned API — it cannot reach child_process, fs, or the network. Returns
the offending specifiers/constructs (empty = clean). (Probe: a regex scan; a
production impl walks the TS AST.)
vigiles/hook— the closed vocabulary for authoring a compiled hook.A hook today is opaque shell (
bash guard.sh): un-analyzable, and the author hand-writes the fragile parts (exit code, JSON field, agrepmatcher) that cause the #1 verified hook pain — FALSE CONFIDENCE (a guardrail that looks like it blocks but silently doesn't). Invert it: author a pure typed function(event) => Decisionagainst THIS surface, andvigiles compileemits the harness protocol for you. The whole false-confidence class becomes UNREPRESENTABLE — you never write the exit code or the field.The roles, each with its own output type so a category mistake is a
tscerror, not a silent no-op:defineHook/defineFileGate— a gate returns aDecision(allow/deny/ask);denyis the only thing that blocks.definePromptGate— a prompt gate (UserPromptSubmit) sees the prompt TEXT and maydenyto block it (a security filter).defineStopGate— a stop gate (Stop/SubagentStop) maydenyto keep the agent going (gate-until-tests-pass).defineInject— an inject returns anInjection(context text); it has nodeny, so "block on a SessionStart hook" won't compile.defineReact— a react (PostToolUse) returns aReaction; it sees the tool RESPONSE, itsrun(cmd)is effect-classified at construction, and it can't block (the tool already ran).Every gate takes a
mode:enforce(default, blocks) orobserve(the shadow/rollout mode — records what it WOULD block, never blocks).observeis harness-neutral (it just exits 0 + writes a local record).Matching is AST-backed (
command.runs("git push", { force })), so it catchescd x && git push -fthat the nativeBash(git:*)glob misses.A gate may also decide on EXTERNAL STATE by declaring
needs(e.g.needs: ["git.branch"]): the trusted runtime gathers those read-only facts and hands them in ase.ctx— the hook still does zero I/O, and reading an undeclared fact is atscerror. Built-ins:git.branch/git.isDirty/git.root/cwd/os.platform/env.isCI. For a one-off off-catalog fact, the lightweight opt-out is an inlineprovide(name, cmd)(read-only) ordangerously(name, cmd)(the loud escape) right inneeds. Seeresearch/hook-context-providers.md.⚠️ Honest scope: compile/verify fix the hook's AUTHORING + LOGIC. They do NOT change DELIVERY — Claude Code's own subagent-bypass (#34692) means a PreToolUse hook (compiled or hand-written) does not fire for a subagent's tool calls. A gate is a strong default, never an unbypassable wall. See
docs/compiled-hooks.md.