Hooks: Stop hook systemMessage is forced to `warning:` — no info/notice severity for end-of-turn notes

Resolved 💬 4 comments Opened Jun 24, 2026 by kfastino Closed Jul 21, 2026
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

What variant of Codex are you using?

CLI

What version of Codex are you using?

codex-cli 0.141.0

What happened?

A Stop hook's systemMessage is always rendered in the TUI with the warning: prefix, even when the message is purely informational. There is no info/notice severity available to an end-of-turn hook, so passive status lines (cost/usage summaries, routing info, "all checks passed", etc.) are surfaced as warnings.

Example — this Stop hook output:

{ "systemMessage": "Auto-routing savings this session: ~$0.94 (vs <reference-model>)" }

renders as:

• Stop hook (completed)
  warning: Auto-routing savings this session: ~$0.94 (vs <reference-model>)

Why this isn't fixable from the hook side

In codex-rs/hooks/src/events/stop.rs, parse_completed maps systemMessage unconditionally to HookOutputEntryKind::Warning:

if let Some(system_message) = parsed.universal.system_message {
    entries.push(HookOutputEntry {
        kind: HookOutputEntryKind::Warning,   // forced
        text: system_message,
    });
}

For a Stop hook the only reachable entry kinds are Warning (from systemMessage), Stop (from continue:false + stopReason), Feedback (from decision:"block"), and Error. The neutral HookOutputEntryKind::HookContext (hook context: prefix, added in #17266) is never produced for Stop, and hookSpecificOutput.additionalContext isn't an accepted Stop field (deny_unknown_fields).

The available alternatives don't work as passive notes:

  • decision:"block" + reason → renders as feedback: but force-continues the turn (injects a new user prompt).
  • exit code 2 + stderr → also treated as a turn continuation.
  • plain-text stdout → invalid for Stop.

So systemMessage → warning: is the only passive, user-visible channel for an end-of-turn hook, and it's hardwired to the warning label.

What I expected

A way for an end-of-turn (and ideally any) hook to surface a non-warning informational note. Options that would solve it:

  1. Add an info/notice entry kind (e.g. rendered as note: / info:) selectable from hook output — for example an optional severity on systemMessage, or a dedicated field.
  2. Allow Stop hooks to emit a neutral hook context:-style entry that is shown to the user (not injected into the model turn).

Why it matters

Stop hooks are a natural place for end-of-turn informational summaries (usage/cost, routing decisions, lint/test status). Today every one of these reads as a warning, which trains users to ignore the prefix and makes legitimately useful notes feel alarming.

Related (but distinct)

  • #25403 (quiet mode) and #15497 / #19383 — about suppressing hook output, not relabeling an info note.
  • #21753 (Claude hook parity) and #19385 (additionalContext in PreToolUse) — adjacent, but neither requests an info-severity user-visible entry for Stop.

View original on GitHub ↗

4 Comments

Nicolas0315 · 2 months ago

I put up a patch on my fork here:

https://github.com/openai/codex/compare/main...Nicolas0315:codex:codex/hook-system-message-severity?expand=1

Commit: 1656e5b8d fix(hooks): allow neutral stop hook context

What it changes:

  • Stop and SubagentStop hook outputs now accept hookSpecificOutput.additionalContext.
  • That value renders through the existing neutral HookOutputEntryKind::Context path (hook context:), not as a warning.
  • systemMessage remains a warning, preserving the current semantics for warning-like output.
  • The neutral context is not added to continuation/model input.
  • Generated output schemas now document the event-specific hookSpecificOutput shape for Stop/SubagentStop.

Verification:

  • Local rtx4090 workspace:
  • just fmt
  • just test -p codex-hooks hook_specific_output_event_names_are_event_specific_in_output_schemas
  • just test -p codex-hooks (123 passed)
  • cargo check --manifest-path codex-rs\Cargo.toml -p codex-hooks
  • git diff --check
  • SSH rtx4090:
  • cargo test --quiet --manifest-path codex-rs/Cargo.toml -p codex-hooks (123 passed)
  • git diff --check
  • SSH cyberagent-mac-m5max:
  • fresh clone from openai/codex, git apply --check, git apply
  • cargo test --quiet --manifest-path codex-rs/Cargo.toml -p codex-hooks (123 passed)
  • git diff --check
  • SSH rtx5060ti:
  • fresh clone + git apply --check + git apply passed
  • Rust test is blocked by the node's existing Windows linker/MSVC issue (link.exe treats object files as extra operands; Cargo exits 101)
  • SSH nicolas2025:
  • fresh clone + git apply --check + git apply + git diff --check passed
  • Rust test blocked because /c/Users/ogosh/work/.cargo/bin/cargo and rustc are zero-byte files
  • SSH nicolas-macbook-pro:
  • fresh clone + git apply --check + git apply passed; changed files are the expected five hook/schema files
  • Cargo is not installed on PATH; installed Git also lacks/does not accept git diff --check

I tried to create the upstream PR directly, but GitHub rejected it with:
GraphQL: Nicolas0315 does not have the correct permissions to execute CreatePullRequest.

jarfa · 1 month ago

Same problem, and before seeing @Nicolas0315 's PR I made my own solution: https://github.com/jarfa/codex/pull/1

If y'all care to invite the PR into openai/codex or to just make your own solution, by all means.

nos1609 · 1 month ago

This also affects the IDE extension, and it is broader than Stop.

Environment

  • codex-cli 0.144.5
  • VS Code extension openai.chatgpt 26.715.31925 on a Linux remote host
  • Codex source main at 0fb559f0f6e231a88ac02ea002d3ecd248e2b515

Reproduction

A successful hook emits:

{
  "continue": true,
  "systemMessage": "hook completed (UserPromptSubmit)\nwrote: <hook-state>/session.jsonl"
}

Hook instrumentation confirms that the handler ran successfully and the app-server reports the run as Completed, but the VS Code conversation shows no prominent user-visible notification.

In contrast, a hook startup failure is immediately visible:

python3: can't open file '<plugin-cache>/<version>/hooks/hook.py': [Errno 2] No such file or directory

Source-level reason

HookUniversalOutputWire has deny_unknown_fields and exposes only systemMessage for a universal user-facing message. Successful systemMessage output is unconditionally converted to HookOutputEntryKind::Warning across events such as UserPromptSubmit, PreCompact, and PostCompact.

The VS Code conversation renderer promotes error, feedback, and stop entries to its visible top-level list, but keeps warning only inside the per-run details. Therefore a successful systemMessage cannot produce the same visible notification as the error above.

The hook cannot work around this safely:

  • failing deliberately makes the run Failed;
  • exit code 2 / blocking feedback changes control flow for prompt and stop hooks;
  • additionalContext is model context, not a user notification;
  • plain stdout is either invalid or not a stable cross-surface display channel.

Requested contract

Please add a universal, non-blocking informational output channel for successful hooks, for example:

{
  "continue": true,
  "notificationMessage": "wrote: <path>"
}

or an info / notice severity on systemMessage.

Required semantics:

  • the hook run remains Completed;
  • it does not stop, block, or continue the turn;
  • it is not injected into model context;
  • app-server exposes it as a user-facing display entry;
  • CLI/TUI/Desktop/IDE render it consistently;
  • existing systemMessage warning behavior may remain backward compatible.

This is the same missing neutral-message primitive described in this issue, with an additional cross-surface impact: in the IDE, the warning is not merely mislabeled; it is effectively invisible unless the user opens hook run details.

etraut-openai contributor · 1 month ago

Current main commit 70a9235 explicitly addresses the issue and renders passive hook messages as <HookEvent> (<status>) says: rather than warnings.