PreToolUse updatedInput resolution depends on handler completion timing
What variant of Codex are you using?
Codex CLI/TUI hooks on current main (49025589b0216b876b1a6a20977536c7d55cdb8b).
What issue are you seeing?
When more than one matching PreToolUse command hook returns updatedInput, the selected rewrite depends on handler completion timing rather than configured order.
The dispatcher runs matching handlers concurrently with FuturesUnordered and records completion order:
[https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/engine/dispatcher.rs#L90-L116](<https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/engine/dispatcher.rs#L90-L116>)
PreToolUse then explicitly selects the rewrite from the handler that finished last:
[https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/events/pre_tool_use.rs#L151-L168](<https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/events/pre_tool_use.rs#L151-L168>)
The existing unit test last_completed_updated_input_wins codifies this behavior:
[https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/events/pre_tool_use.rs#L414-L440](<https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/events/pre_tool_use.rs#L414-L440>)
This makes the effective tool input timing-dependent. Identical configuration and model output can execute different commands when handler latency changes because of scheduling, cold starts, I/O, or load.
Minimal reproduction
Create two handlers:
# hook_a.py
import json
import time
time.sleep(0.2)
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {"command": "echo from-hook-a"},
}
}))
# hook_b.py
import json
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {"command": "echo from-hook-b"},
}
}))
Register both in the same configured order:
{
"hooks": {
"PreToolUse": [{
"matcher": "^Bash$",
"hooks": [
{"type": "command", "command": "python hook_a.py", "timeout": 5},
{"type": "command", "command": "python hook_b.py", "timeout": 5}
]
}]
}
}
Trigger a Bash/shell tool call. A finishes last, so the effective command is echo from-hook-a. Move the sleep(0.2) from A to B without changing hook configuration order; the effective command becomes echo from-hook-b.
This is particularly risky for policy middleware. For example, one handler may remove unsafe arguments while another normalizes a path; completion-order replacement can silently discard either rewrite.
Expected behavior
Multiple rewrites should have deterministic semantics unrelated to wall-clock completion timing.
Preferred contract:
- execute rewriting handlers in configured order;
- pass each accepted
updatedInputas the next handler'stool_input; - preserve blocking dominance: once a handler denies the operation, no rewrite is executed;
- return the fully composed input after the final handler.
If sequential composition is not desired, deterministic configured-order precedence or explicit rejection of competing rewrites would still be safer than completion-order selection.
Suggested acceptance tests
- swapping artificial handler delays does not change the final tool input;
- configured order determines rewrite composition;
- the second handler receives the first handler's rewritten input;
- a deny result prevents execution regardless of other rewrites;
- invalid rewrites cannot erase a previously valid security transformation;
- reporting order and execution semantics remain deterministic.
Related but distinct: openai/codex#18491 requests updatedInput support and broader tool coverage; it does not define conflict or composition semantics for multiple rewriting handlers. openai/codex#15266 concerns ordering between different lifecycle events, not multiple handlers for one event.
I can prepare a focused implementation and regression tests if maintainers confirm the intended composition contract and invite a PR.
1 Comment
Reconfirmed against the exact official
rust-v0.146.0tag (e363b08c9175ac1cbe5893615dd2cb9ddf95043b).The upstream
codex-hookstestevents::pre_tool_use::tests::last_completed_updated_input_winspasses on that tag. The dispatcher still assignscompletion_orderfromFuturesUnordered, andlatest_updated_inputselects the rewrite with the greatest completion order.This confirms that
updatedInputresolution remains dependent on handler completion timing in 0.146.0.