ReadonlyblockOptional ReadonlyconditionHow 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.
Readonlydenydecision / permissionDecision values that mean "deny" the tool call.
ReadonlyeventEnv 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/…).
Optional ReadonlyhaltsThe 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.
ReadonlyinjectableThe 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.
Optional ReadonlymatcherHow 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.
ReadonlynameStable identifier, e.g. "claude-code".
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).
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.
Exit code a hook process uses to block/deny a tool call (Claude Code: 2).