Repository navigation
fix(O165): hooks fail closed on throw; fatal Stop veto; blocked prompt exits 9 - #157
Conversation
# Conflicts: # CHANGELOG.md # src/cli/hooks.ts
# Conflicts: # CHANGELOG.md # README.en.md # README.md
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b57ede65fe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| inputBlockedReason = reason; | ||
| process.stderr.write(`[hooks] ${reason}\n`); | ||
| } else if (outputFormat === 'text') { | ||
| throw err; | ||
| } else { | ||
| writeStructured(formatHeadlessThrownError(state, err)); |
There was a problem hiding this comment.
Preserve structured errors and exit 9 for rejected prompts
When an input guardrail rejects a prompt in json or stream-json mode, this branch consumes the exception without calling formatHeadlessThrownError. Because rejection happens before streamChat yields any events, neither finalResult nor state.lastError is populated, so the subsequent exit-code assignment is skipped. Reproducing with a rejecting agent produces empty JSON output (or only the stream initialization event) and exit 0, incorrectly signaling success to automation. Emit the structured error result and set exit 9 for this branch as well as printing the text-mode conclusion.
Useful? React with 👍 / 👎.
| inputBlockedReason = reason; | ||
| process.stderr.write(`[hooks] ${reason}\n`); | ||
| } else if (outputFormat === 'text') { | ||
| throw err; |
There was a problem hiding this comment.
Stop execution after an input guardrail rejects the prompt
With MOSS_GOAL_VERIFY_LOOP=1 and a failing MOSS_GOAL_VERIFY_CMD, swallowing the input rejection here allows execution to reach the verification loop below. That loop executes the command and recursively calls runOneShot with a synthetic verification-failure prompt, dropping the rejected original prompt. A content-based UserPromptSubmit policy can therefore reject the user's request but approve the synthetic continuation, allowing a model turn anyway. This is a regression from text mode's previous immediate throw; terminate the rejected run before verification commands or continuation-producing Stop hooks can run.
Useful? React with 👍 / 👎.
# Conflicts: # CHANGELOG.md
test(O165): add the regression spec missing from #157
O165 (P1): a hook that throws must block
On main, an exception on a blocking-event path never failed closed:
PreToolUse / PermissionRequest bubbled up as tool or run errors, and
runStopHookscaught the exception and returned{ blocked: false }— thethrow silently approved the stop (an O88-era test even pinned that).
Fix
[hooks]notice:PreToolUse blocks the call, PermissionRequest denies, UserPromptSubmit
rejects the prompt, Stop vetoes (
src/cli/hooks.tstry/catch wraps).StopHookResult.fatal: true— the REPL's forced continuation turn (3 call sites) is skipped (no
further model call), headless ends Incomplete (exit 9); oneshot's own
outer catch also fails closed now.
-pwith a blocked UserPromptSubmit hook exits 9 (was 8): the errorcode becomes
INPUT_GUARDRAIL_DENIED→ExitCode.INCOMPLETE, and the runends with the stable 「被拦」/
Blockedlast line (via a newinputBlockedoutcome input) instead of a rethrown error with a stack.JSON output keeps the error event with exit 9.
Tests
test/o165-hook-throw-blocks.spec.mjs(8): the four gates (throwing-hookobject / throwing lifecycle runner) incl. zh copy and the
fatalveto;INPUT_GUARDRAIL_DENIED→ 9 mapping; process e2e — blockedUserPromptSubmit exit 9 with 「被拦」 line, notes.txt untouched, no
tool round-trip (the model never ran), zh + en; allow control exit 0.
hooks-fail-closed.spec.mjs: the O88 "internal error stays fail-open" pinis updated to the new fail-closed contract (25/25), like fix(o152): block exec-capable reads; L3 auto-restore; exit 9 #136 did for
o103's intentional semantics change.
sdk-contract 6/6 (the new ErrorCode member is additive; snapshot
unchanged).
Revert-red (local, macOS)
blocked:falseagain + PreToolUse catch rethrowsHead
b57ede65includes main at9d455d94(#155).npm run checkgreen.