mcp_tool Stop hook fails open when MCP server is missing or cannot start
What version of Codex CLI is running?
codex-cli 0.149.0
What subscription do you have?
ChatGPT Pro
Which model were you using?
Default OpenAI model selected by Codex CLI.
What platform is your computer?
Linux 7.0.0-29-generic x86_64 x86_64 (Linux Mint 22.3)
What terminal emulator and version are you using?
Kitty on Linux; reproduced with non-interactive codex exec --ephemeral.
Codex doctor report
Redacted summary: overallStatus ok; Codex 0.149.0; installation and update target consistent; config loaded; MCP configuration locally consistent; provider HTTP and WebSocket checks pass; state databases healthy. The full report contains only local-path details and does not show a failing prerequisite.
What issue are you seeing?
An mcp_tool Stop hook works when its configured stdio MCP server is available, but fails open when that MCP server is unavailable.
Positive control on 0.149.0: the Stop hook successfully invoked two MCP tools in sequence and the second tool wrote a valid terminal-seal fixture. This confirms that the MCP hook executor is wired in this release.
Negative controls:
- The Stop hook references an MCP server name that is not registered.
- The referenced MCP server is registered, but its command points to a nonexistent executable and cannot start.
In both cases Codex emitted the requested agent message followed by turn.completed. There was no hook failure event in JSONL output and the Stop hook did not block completion.
This makes an MCP-backed validation, compliance, or release gate fail open precisely when its validator is unavailable.
What steps can reproduce the bug?
- Enable the stable hooks feature.
- Configure a Stop hook similar to:
{
"hooks": {
"Stop": [
{
"matcher": ".*",
"hooks": [
{
"type": "mcp_tool",
"server": "repro_validator",
"tool": "validate",
"input": {},
"timeout": 20
}
]
}
]
}
}
- First register a working stdio server named
repro_validatorwhosevalidatetool returns a successful Stop-hook response. Run:
codex exec --ephemeral --skip-git-repo-check --dangerously-bypass-hook-trust --json "Reply exactly POSITIVE_CONTROL"
Confirm the MCP tool is invoked. This positive control passes on 0.149.0.
- Remove
repro_validatorfrom the MCP configuration while leaving the hook enabled. Run the same command. The output still ends withturn.completed.
- Register
repro_validatoragain with a nonexistent command, for example/__missing_mcp_server__, and run the same command. The output again ends withturn.completed.
Observed negative-control shape:
{"type":"item.completed","item":{"type":"agent_message","text":"..."}}
{"type":"turn.completed","usage":{}}
No successful MCP validation occurred in either negative control.
What is the expected behavior?
If an enabled mcp_tool Stop hook cannot resolve, start, or call its MCP server, Stop should fail closed. Codex should block completion or return a non-success terminal result that callers cannot mistake for a completed turn. JSONL output should identify the failed hook and MCP failure.
Additional information
The same fixture was also exercised with ten validator fault-injection cases; all validator-side cases pass. The remaining issue is specifically runtime behavior when the MCP hook dependency is missing or cannot start.
This is reproducible after a clean Canary rollback: the test hook and MCP registration were removed, and no production configuration or data is involved.
2 Comments
I reproduced both negative controls on fresh
main:HookCompletedwithStop / Failed(unknown MCP server), followed byTurnComplete.HookCompletedwithStop / Failed(server is not connected), again followed byTurnComplete.The MCP failure is therefore detected correctly.
mcp_runnerconverts it intoHandlerRunResult.error, and the Stop parser recordsHookRunStatus::Failed. However, a failed result carries no control effect, aggregation only considers explicitStoppedorBlockedresults, andcodex exec --jsoncurrently discardsHookStartedandHookCompletednotifications.This seems like two separate concerns:
hook.startedandhook.completedevents in exec JSONL without changing existing behavior.failureMode = "continue" | "failTurn", retainingcontinueas the compatibility default and applying it consistently to command and MCP handlers.I think
failTurnis preferable to translating infrastructure failure intoblock, because blocking re-enters the model loop while the validator remains unavailable. Async and executor-scoped hooks should remain non-controlling.Does this match the intended hook contract, and is there a preferred configuration surface? I’d be happy to prepare the implementation if the team wants an external patch.
Thanks for reproducing both negative controls on fresh main. Your
findings match what we observed: the MCP failure is visible in the
hook lifecycle, but it has no control effect, so the turn can still
complete without the required validator having run.
The proposed opt-in
failureMode = continue | failTurnforsynchronous local hooks fits our use case well.
Our Stop hook invokes a Validator MCP as a completion gate. The
required invariant is:
For that reason, an unknown MCP or an unstartable registered MCP must
not fall through to normal
TurnComplete. We agree thatfailTurnisa better representation of this infrastructure failure than
translating it into
Blocked: the validator was unavailable, so thereis no meaningful model action that can resolve the condition, and
re-entering the model loop could create retries without restoring the
missing validation authority.
Keeping
continueas the compatibility default also seemsappropriate, while allowing explicitly configured synchronous local
command/MCP hooks to be controlling. Keeping asynchronous or
executor-scoped hooks non-controlling is consistent with the boundary
we need.
One distinction we would want to preserve is between:
transport/execution failure — which should trigger
failTurn; andresult — which should continue to use the validator’s structured
result/blocking contract.
Structured
hook.started/hook.completedJSONL events would alsobe useful for independently verifying which path occurred.
So, from our Stop Hook → Validator use case, this direction addresses
the current gap. We would be happy to test the implementation against
our two negative controls and the normal validator-success path, but
we are not making any assumptions about implementation or merge
decisions.