Skip to content

fix(O165): hooks fail closed on throw; fatal Stop veto; blocked prompt exits 9 - #157

Merged
QiaolongLi1201 merged 5 commits into
mainfrom
fix/o165-hook-throw
Oct 18, 2026
Merged

QiaolongLi1201 merged 5 commits into
mainfrom
fix/o165-hook-throw

Conversation

@QiaolongLi1201

Copy link
Copy Markdown
Collaborator

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
runStopHooks caught the exception and returned { blocked: false } — the
throw silently approved the stop (an O88-era test even pinned that).

Fix

  • All four gates fail closed on a throw with a visible [hooks] notice:
    PreToolUse blocks the call, PermissionRequest denies, UserPromptSubmit
    rejects the prompt, Stop vetoes (src/cli/hooks.ts try/catch wraps).
  • A throwing Stop hook is a terminal veto: 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.
  • -p with a blocked UserPromptSubmit hook exits 9 (was 8): the error
    code becomes INPUT_GUARDRAIL_DENIED → ExitCode.INCOMPLETE, and the run
    ends with the stable 「被拦」/Blocked last line (via a new
    inputBlocked outcome 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-hook
    object / throwing lifecycle runner) incl. zh copy and the fatal veto;
    INPUT_GUARDRAIL_DENIED → 9 mapping; process e2e — blocked
    UserPromptSubmit 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" pin
    is 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.
  • Merged head: o165 8/8, o157 16/16, o121 46/46, hooks-fail-closed 25/25,
    sdk-contract 6/6 (the new ErrorCode member is additive; snapshot
    unchanged).

Revert-red (local, macOS)

Mutation Result
runStopHooks catch → blocked:false again + PreToolUse catch rethrows 7 red
exit map back to TOOL_EXECUTION (8) 3 red (mapping unit + both e2e)
head with fix 8/8

Head b57ede65 includes main at 9d455d94 (#155). npm run check green.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T17:58:55.722228Z b57ede6 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/cli/oneshot.ts
Comment on lines +587 to +592
inputBlockedReason = reason;
process.stderr.write(`[hooks] ${reason}\n`);
} else if (outputFormat === 'text') {
throw err;
} else {
writeStructured(formatHeadlessThrownError(state, err));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/cli/oneshot.ts
Comment on lines +587 to +590
inputBlockedReason = reason;
process.stderr.write(`[hooks] ${reason}\n`);
} else if (outputFormat === 'text') {
throw err;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@QiaolongLi1201
QiaolongLi1201 merged commit 5f1153c into main Oct 18, 2026
13 checks passed
QiaolongLi1201 added a commit that referenced this pull request Oct 18, 2026
test(O165): add the regression spec missing from #157
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant