vigiles API reference
    Preparing search index...

    Interface HookProtocol

    interface HookProtocol {
        blockExitCode: number;
        condition?: HookConditionSupport;
        denyDecisionValues: readonly string[];
        eventEnvVars: readonly string[];
        haltsTurnField?: string;
        injectableEvents: readonly string[];
        matcherStyle?: "exact" | "regex";
        name: string;
        mergeRegistrations(
            existing: Record<string, unknown>,
            compiled: Readonly<Record<string, readonly unknown[]>>,
            managedBy: string,
        ): Record<string, unknown>;
        registration(
            on: string,
            matcher: string | undefined,
            command: string,
        ): { hooks: Readonly<Record<string, readonly unknown[]>> };
    }
    Index

    Properties

    blockExitCode: number

    Exit code a hook process uses to block/deny a tool call (Claude Code: 2).

    condition?: HookConditionSupport

    How this harness decides whether a hook that declares a CONDITION runs at all — Claude Code's per-action if field. Optional (additive, non-breaking); absent ⇒ the harness has no such feature and every hook is unconditional, which is what every consumer got before this existed.

    🔴 IT IS A PORT FIELD BECAUSE THE SYNTAX IS THE HARNESS'S, NOT BECAUSE THE IDEA IS. "A hook may only run on matching calls" is neutral and modelled in core/hook-condition.ts; "Bash(git push *--force*)" is Claude Code's permission-rule grammar, so the matcher lives in its adapter. Reading it here is what stops verifyGuardrail reporting a conditional guard as blocking things its condition means it never sees — see core/hook-condition.ts for the measured false green that put this field on the port.

    denyDecisionValues: readonly string[]

    decision / permissionDecision values that mean "deny" the tool call.

    eventEnvVars: readonly string[]

    Env vars a synthesized hook event carries beyond the JSON on stdin (Claude Code passes the event on stdin only; Codex adds session_id/cwd/PLUGIN_ROOT/…).

    haltsTurnField?: string

    The boolean stdout field whose false value HALTS THE WHOLE TURN, if the harness has one (Claude Code: "continue"). Distinct from a deny: a deny refuses one tool call, this stops the iteration and hands stopReason back to the agent as text — so authors reach for it exactly when they want to explain themselves, and a guard written that way still prevents the action.

    Optional (additive, non-breaking) and per-harness on purpose. It is DOCUMENTED for Claude Code and UNVERIFIED for Codex, whose protocol notes only record the shared exit-2 / decision / permissionDecision model — so Codex leaves it unset rather than inheriting a claim nobody measured. Read by decideHook; absent ⇒ no field halts the turn on this harness.

    injectableEvents: readonly string[]

    The events whose hook can inject developer context into the agent by printing { hookSpecificOutput: { hookEventName, additionalContext } } on stdout. The inject shape is shared across Claude Code and Codex (so the runtime emits it once, not per-harness); the genuinely per-harness fact is which events honor it — and encoding it here is what makes "this harness can deliver an inject hook" a TESTED contract instead of an assumption. Both Claude Code and Codex support the main lifecycle events (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse); beyond that they DIVERGE, which is why this is a port and not a core constant: Claude Code also honors Stop (measured 2026-09-15 on 2.1.273, headless), Codex instead honors SubagentStart. An empty list means the harness cannot inject context from a hook at all. Verified for Codex against the official hooks docs (developers.openai.com/codex/hooks). The conformance kit asserts a shell-hook harness declares a non-empty set.

    ⚠️ This comment previously asserted that "a few (Stop, SubagentStop, PreCompact) carry no context on EITHER" harness. For Claude Code's Stop that was wrong, and the cost was structural rather than cosmetic: an unmeasured claim in a doc-comment became the runtime's emit gate, so three react hooks were reported undeliverable-by-vocabulary when the harness would have delivered them. A per-harness fact belongs in the adapter WITH its measurement; the residue (SubagentStop, PreCompact) stays unclaimed here rather than re-asserted. *

    Superseded by HarnessDialect.eventCapabilities (honours: "inject"), which is asserted to reproduce this list exactly. It lives on the DIALECT rather than here because three sibling event facts already did, and because the browser-side engine is handed a dialect and never a protocol. Kept for third-party adapters; goes in the next major.

    matcherStyle?: "exact" | "regex"

    How a tool matcher is written in the emitted hooks block — "exact" (Claude Code: the tool name / A|B alternation) or "regex" (Codex: an anchored regex ^(A|B)$). Optional (additive, non-breaking) — absent ⇒ "exact". Used by compileHookProgram when rendering the settings block.

    name: string

    Stable identifier, e.g. "claude-code".

    Methods

    • Merge a compiled hook's registrations into an already-parsed settings object, idempotently: entries this hook file manages are REPLACED, every other command — including the user's own hand-written hooks sharing a matcher block — is preserved.

      🔴 compiled IS THE COMPILER'S CANONICAL BLOCK, NOT registration's OUTPUT, and the asymmetry is load-bearing enough to state rather than leave to be discovered. compileHookProgram always produces the nested {matcher?, hooks:[{type, command}]} form (CompiledHooks — "the CC-shaped structured block a compiled hook program carries"), and each implementation converts to its own shape on the way in; Codex's flattens with toTomlEntries. registration goes the other way: it produces the NATIVE shape, for the settings block a human pastes. Feeding this method a registration result is a type-level no-op and a run-time TypeError on the flat side — found by the property test below, which asserted it.

      managedBy is the canonical hook-source reference whose commands this merge owns; a compiled block whose commands do not mention it is APPENDED, not replaced, which is what keeps a user's own hooks intact.

      On HookProtocol because it is the same SHAPE question registration answers, from the other direction: the CC shape nests several commands under one matcher (so the granularity has to be the command), the Codex shape carries one command per entry (so entry- and command-granularity coincide). Typed structurally here, because the concrete entry types are the application layer's.

      ⚠️ There is no registrations(config) INVERSE yet — reading a third shape would need one, and that is deferred until a third shape exists. What exists now is: write one (registration), and merge into a parsed object (this).

      Parameters

      • existing: Record<string, unknown>
      • compiled: Readonly<Record<string, readonly unknown[]>>
      • managedBy: string

      Returns Record<string, unknown>

    • The config fragment that registers ONE command on ONE event, in this harness's native settings SHAPE. Claude Code nests {matcher, hooks: [{type: "command", command}]}; Codex is flat {matcher, command}.

      🔴 THE SHAPE HALF OF WHAT settingsFormat WAS STANDING IN FOR, and it does not belong with the encoding. PluginLayout.settingsFormat was a "json" | "toml" enum that three call sites read as "which entry shape", which is neither what its name says nor what its values mean — a TOML harness with CC-shaped entries, or a JSON harness with flat ones, were both expressible and both would have been read wrong. A constructor cannot be wrong about its own shape.

      Reading already tolerates both shapes (core/hook-normalize.ts); this makes WRITING symmetric.

      Parameters

      • on: string
      • matcher: string | undefined
      • command: string

      Returns { hooks: Readonly<Record<string, readonly unknown[]>> }