Skip to content

Stop hook invalid JSON error is too opaque for unsupported fields #18887

Open

Description

@SYRS-AI

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:

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-L353
    • StopCommandOutputWire has #[serde(deny_unknown_fields)]
    • fields are only the flattened universal output, decision, and reason
  • codex-rs/hooks/src/engine/output_parser.rs#L215-L232
    • parse_stop() deserializes directly into StopCommandOutputWire
  • codex-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:

  1. Surface the serde unknown-field error directly, e.g. unsupported field hookSpecificOutput for Stop.
  2. Add event-aware validation diagnostics before returning the generic parse error.
  3. Optionally, if intended, support hookSpecificOutput.additionalContext on Stop too 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.

Activity

  1. added
    enhancementNew feature or request
    CLIIssues related to the Codex CLI
    hooksIssues related to event hooks
    on Apr 21, 2026
  2. oxysoft commented on May 8, 2026

    @oxysoft

    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.

  3. francisco-perez-sorrosal commented on May 10, 2026

    @francisco-perez-sorrosal

    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 on stdout or JSON in the documented shape:

    {
      "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "additionalContext": "..."
      }
    }

    What I hit was a cross-tool adapter case where the hook returned this instead:

    {
      "additionalContext": "..."
    }

    On codex-cli 0.130.0 on macOS, that produced:

    UserPromptSubmit hook (failed)
    error: hook returned invalid user prompt submit JSON output
    

    That message was hard to act on because:

    • the payload was valid JSON
    • the hook exited successfully
    • the actual problem was only that the JSON shape was not the event contract Codex expected

    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 UserPromptSubmit
    • or expected plain text stdout or hookSpecificOutput.additionalContext

    A docs-side improvement could also help, even if behavior stays exactly as-is: an explicit note that for UserPromptSubmit, top-level JSON additionalContext is not a supported form, even though plain text stdout and hookSpecificOutput.additionalContext are.

    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: #19385 for another hook-event parity / docs-clarity case.

  4. dustingregg1 commented on Jul 16, 2026

    @dustingregg1

    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-cli 0.144.4 (standalone Windows installer), Windows 11 Pro (build 26200)
    • Hooks registered in ~/.codex/hooks.json, each as a quoted absolute-path .cmd wrapper with a commandWindows field (the invocation form that works reliably on Windows)

    Symptom
    Every turn ends with:

    Stop hook (failed)
      error: hook returned invalid stop hook JSON output
    

    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 {} (hex 7B 7D) to stdout via process.stdout.write in node (no shell echo involved), and (4) exits 0.

    • Manual pipe test (payload | cmd /c "<wrapper>.cmd"): log line written, stdin captured, stdout is exactly {}, exit 0.
    • Real turn-end, across multiple fresh sessions (full restart, hook trust re-approved, hooks.json verified to point at the wrapper, trusted hash recorded in config.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 + commandWindows pattern, 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

    1. The error string conflates "hook returned invalid JSON" with what appears to be a pre-execution failure inside the Stop pipeline. Per Hook failure messages should identify the failing hook and expose diagnostics #27052, surfacing spawn/exec failures distinctly from output-validation failures would have saved several rounds of schema guessing.
    2. Stop-event execution itself appears broken on this Windows build. If useful I can provide the full hooks.json, the instrumented wrapper, and debug logs.

    Minimal repro sketch (Windows)

    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"C:/Users/<user>/.codex/hooks/stop-test.cmd\"",
            "timeout": 10,
            "commandWindows": "\"C:/Users/<user>/.codex/hooks/stop-test.cmd\""
          }
        ]
      }
    ]

    stop-test.cmd:

    @ECHO OFF
    @ECHO invoked >> "C:\Users\<user>\.codex\hooks\stop-test.log"
    @ECHO {}
    @EXIT /B 0
    

    Observed: "invalid stop hook JSON output" at every turn end, and stop-test.log is never created.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthooksIssues related to event hooks

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions