Stop hook invalid JSON error is too opaque for unsupported fields
Summary
When a Stop hook returns JSON with an unsupported field such as hookSpecificOutput, Codex reports only a generic parse failure like:
hook returned invalid stop hook JSON output
That made a real integration failure much harder to diagnose than it needed to be. The hook was returning a shape that is accepted on other events (SessionStart, UserPromptSubmit, PostToolUse) but not on Stop.
Why this matters
In a multi-event hook integration, it is natural to reuse the same output builder across events. If one event has stricter schema rules than the others, a field-level diagnostic is much more actionable than a generic JSON-invalid error.
In my case, the hook JSON itself was valid JSON and the hook exited 0; the problem was only that Stop rejected an extra field that other hook events commonly use.
Current documented behavior
The public hooks docs say Stop expects JSON on stdout and that it supports the common output fields. The docs do not show hookSpecificOutput for Stop:
- https://developers.openai.com/codex/hooks
Stopsection: plain text is invalid;decision/reasonare the documented continuation shape.
Current source behavior
At current main (6f6997758a6936b522c046191ae3cae4293e23e9 when I checked), the hooks schema for Stop is strict and does not include hookSpecificOutput:
codex-rs/hooks/src/schema.rs#L340-L353StopCommandOutputWirehas#[serde(deny_unknown_fields)]- fields are only the flattened universal output,
decision, andreason codex-rs/hooks/src/engine/output_parser.rs#L215-L232parse_stop()deserializes directly intoStopCommandOutputWirecodex-rs/hooks/src/events/stop.rs- when parsing fails, Codex surfaces
hook returned invalid stop hook JSON output
Minimal example
This is valid JSON, but because it includes hookSpecificOutput, it is rejected by Stop:
{
"decision": "block",
"reason": "Run one more pass.",
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": "Queue DB is source of truth."
}
}
Requested improvement
At minimum, please make the error specific enough to identify the actual contract violation, for example by reporting the unexpected field name/path instead of only saying the whole payload is invalid.
Any of these would help:
- Surface the serde unknown-field error directly, e.g. unsupported field
hookSpecificOutputforStop. - Add event-aware validation diagnostics before returning the generic parse error.
- Optionally, if intended, support
hookSpecificOutput.additionalContextonStoptoo for parity with other events.
I am not claiming the current parser is wrong per the documented schema. The issue is that the runtime diagnostic is too opaque for a common integration mistake.
Environment
- Codex CLI version:
0.122.0 - Platform: macOS
- Hooks feature enabled with
features.codex_hooks=true
Additional context
I found this while debugging a Codex-only Stop hook failure in a cross-tool bridge. The local integration bug was easy to fix once I traced the schema, but that took longer than it should have because the runtime error did not reveal that the JSON was structurally valid and only had an unsupported field.
3 Comments
Indexed this hook ticket in the umbrella tracker: #21753
Goal: collect the scattered Codex hook requests and bugs into one parity matrix for Full Claude Code Hook Parity (29+), while preserving this issue as the detailed thread for its specific behavior.
I ran into the same diagnostics problem on a different hook event:
UserPromptSubmit.After reading the current hooks docs, I understand that this is not a parser bug. For
UserPromptSubmit, Codex accepts either plain text onstdoutor JSON in the documented shape:What I hit was a cross-tool adapter case where the hook returned this instead:
On
codex-cli 0.130.0on macOS, that produced:That message was hard to act on because:
Once I understood the contract, the local fix was straightforward. The expensive part was the diagnosis.
I think this strengthens the request in this issue for more specific hook-output diagnostics. For example, something along the lines of:
unsupported top-level field "additionalContext" for UserPromptSubmitexpected plain text stdout or hookSpecificOutput.additionalContextA docs-side improvement could also help, even if behavior stays exactly as-is: an explicit note that for
UserPromptSubmit, top-level JSONadditionalContextis not a supported form, even though plain text stdout andhookSpecificOutput.additionalContextare.I am not asking for broader compatibility here unless that already fits the roadmap. Better event-aware error messages, or a small clarification in the hooks docs, would already make this class of integration mistake much easier to debug.
Related:
#19385for another hook-event parity / docs-clarity case.Adding a data point from Windows that may be a stronger variant of this issue: in my case the opaque "invalid stop hook JSON output" error fires for a Stop hook whose command is never executed at all, so no output shape can satisfy the validator.
Environment
~/.codex/hooks.json, each as a quoted absolute-path.cmdwrapper with acommandWindowsfield (the invocation form that works reliably on Windows)Symptom
Every turn ends with:
Evidence that the registered command never runs
I replaced the Stop hook target with an instrumented wrapper that (1) appends a timestamped line to a debug log on every invocation, (2) captures stdin to a file, (3) writes bare
{}(hex7B 7D) to stdout viaprocess.stdout.writein node (no shell echo involved), and (4) exits 0.payload | cmd /c "<wrapper>.cmd"): log line written, stdin captured, stdout is exactly{}, exit 0.hooks.jsonverified to point at the wrapper, trusted hash recorded inconfig.toml): the error above is reported and the debug log receives zero entries. The command is never spawned.Meanwhile the identical registration form (same
.cmd+commandWindowspattern, same file) reports "completed" on all nine other events I have registered: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, PostCompact, SubagentStart, SubagentStop, PermissionRequest. Only Stop fails.Before instrumenting, I iterated output shapes as if it were a schema problem: plain text,
{"continue":true}(listed as a common output field for Stop in the hooks docs), and bare{}all produced the identical error, which is consistent with the output never being read rather than rejected.Two asks
hooks.json, the instrumented wrapper, and debug logs.Minimal repro sketch (Windows)
stop-test.cmd:Observed: "invalid stop hook JSON output" at every turn end, and
stop-test.logis never created.