Hooks: Stop hook systemMessage is forced to `warning:` — no info/notice severity for end-of-turn notes
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 asfeedback: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:
- Add an info/notice entry kind (e.g. rendered as
note:/info:) selectable from hook output — for example an optional severity onsystemMessage, or a dedicated field. - Allow
Stophooks to emit a neutralhook 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 (
additionalContextinPreToolUse) — adjacent, but neither requests an info-severity user-visible entry forStop.
4 Comments
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 contextWhat it changes:
StopandSubagentStophook outputs now accepthookSpecificOutput.additionalContext.HookOutputEntryKind::Contextpath (hook context:), not as a warning.systemMessageremains a warning, preserving the current semantics for warning-like output.hookSpecificOutputshape for Stop/SubagentStop.Verification:
rtx4090workspace:just fmtjust test -p codex-hooks hook_specific_output_event_names_are_event_specific_in_output_schemasjust test -p codex-hooks(123 passed)cargo check --manifest-path codex-rs\Cargo.toml -p codex-hooksgit diff --checkrtx4090:cargo test --quiet --manifest-path codex-rs/Cargo.toml -p codex-hooks(123 passed)git diff --checkcyberagent-mac-m5max:openai/codex,git apply --check,git applycargo test --quiet --manifest-path codex-rs/Cargo.toml -p codex-hooks(123 passed)git diff --checkrtx5060ti:git apply --check+git applypassedlink.exetreats object files as extra operands; Cargo exits 101)nicolas2025:git apply --check+git apply+git diff --checkpassed/c/Users/ogosh/work/.cargo/bin/cargoandrustcare zero-byte filesnicolas-macbook-pro:git apply --check+git applypassed; changed files are the expected five hook/schema filesgit diff --checkI tried to create the upstream PR directly, but GitHub rejected it with:
GraphQL: Nicolas0315 does not have the correct permissions to execute CreatePullRequest.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/codexor to just make your own solution, by all means.This also affects the IDE extension, and it is broader than
Stop.Environment
codex-cli 0.144.5openai.chatgpt 26.715.31925on a Linux remote hostmainat0fb559f0f6e231a88ac02ea002d3ecd248e2b515Reproduction
A successful hook emits:
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:
Source-level reason
HookUniversalOutputWirehasdeny_unknown_fieldsand exposes onlysystemMessagefor a universal user-facing message. SuccessfulsystemMessageoutput is unconditionally converted toHookOutputEntryKind::Warningacross events such asUserPromptSubmit,PreCompact, andPostCompact.The VS Code conversation renderer promotes
error,feedback, andstopentries to its visible top-level list, but keepswarningonly inside the per-run details. Therefore a successfulsystemMessagecannot produce the same visible notification as the error above.The hook cannot work around this safely:
Failed;additionalContextis model context, not a user notification;Requested contract
Please add a universal, non-blocking informational output channel for successful hooks, for example:
or an
info/noticeseverity onsystemMessage.Required semantics:
Completed;systemMessagewarning 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.
Current main commit 70a9235 explicitly addresses the issue and renders passive hook messages as <HookEvent> (<status>) says: rather than warnings.