Repository navigation
fix: refuse a hook-rewritten call on a backend that would run the original - #371
Merged
Merged
Conversation
…ginal A tool_call hook can rewrite a call's input before it is approved. Only some backends run the rewritten input. codex and the Gemini CLI answer an approval with yes/no and run the input they proposed, and qwen-code assigns the new args but executes the invocation it built from the original ones. On those, the rewrite was shown (and approved) while the original was what codex/gemini/qwen would run. - New provider capability `appliesToolInputRewrite` (absent = false, so a new backend fails closed). Declared true for claude (the SDK runs the permission result's updatedInput), the tool-loop backends (openai, gemini, llama.cpp), and pi; explicitly false, with the reason, for codex, the Gemini CLI, and qwen. - Session refuses such a call before the rewrite reaches any display or parks: the tool call ends cancelled with the reason, an info message says why, and it is audited as session.hook_rewrite_refused. The same guard covers an approver's form answers (session.patch_refused). - pi's bridge merged updatedInput key by key, so a rewrite that dropped a key left it in what ran. It now replaces the input. - docs/hooks.md and the session.approvals protocol note say which backends apply a rewrite. Closes #367 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Security review of the #367 change: - An approver's form answers refused on a backend that can't apply them left the tool card at "executing" and the call audited as approved: the refusal ran after both. It now runs right after the approval wait, before the status, card, or audit move. - A hook that returns the input unchanged is not a rewrite; it no longer refuses the call on codex / the Gemini CLI / qwen. - The rewrite check uses the backend that asked, captured before any await, not whatever the session's provider is by then. - pi bridge: own-key checks, and never assign `__proto__`. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Collaborator
Author
|
pr-shepherd: CI is green on 2444c37 (iteration 1, no auto-fixes needed). daemon-check, web-check, Socket Security: Project Report and Socket Security: Pull Request Alerts all passed. The branch is up to date with main. Ready for human review. |
Collaborator
Author
|
@highflame-oracle review |
rsharath
approved these changes
Oct 11, 2026
Collaborator
Author
|
@highflame-oracle review |
saucam
added a commit
that referenced
this pull request
Oct 11, 2026
Brings in #371 (refuse hook-rewritten calls on backends that run their own input). One conflict, in MockSessionProvider: both sides added a test field (appliesToolInputRewrite, ungatedToolIds) — kept both. The session.approvals note no longer lists #368 as a known limit; it is fixed on this branch (#374). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
A
tool_callhook can rewrite a tool call before it is approved, for example to remove a dangerous part of a shell command. Only some backends actually run the rewritten version.codex, the Gemini CLI and qwen run the input they originally proposed, whatever codeoid answers. On those, the rewrite was shown and approved, but the original was what the backend would run.
Now:
Verification
Ran on a real daemon (
codeoid start --local), with a hook that rewrites every shell call toecho SAFEand an agent asked torm a.txt:codeoid approveshowedecho SAFE. After approving, the file was still there andecho SAFEran.session.hook_rewrite_refused.Found while reproducing the bug on
main: codex currently rejects every approval reply codeoid sends, because codeoid uses outdated decision words. So onmain, approved codex commands fail outright rather than running the original. That's filed as #369.This PR is still needed. Once #369 is fixed, codex really will run whatever it proposed, and without this change a hook's rewrite would be shown and then not applied.
Tests:
constructor/__proto__are handled safely, and deny still blocks.Not run: no end-to-end regression tier; codeoid has none.
Review:
__proto__.Follow-ups found along the way:
tool_callhooks don't run for calls the SDK approves by itself, notably skill-granted shell commands.Closes #367
For agents: implementation detail
AgentProvider.appliesToolInputRewrite?: boolean; absent counts as false.ClaudeProvider,OpenAIProvider,GeminiProvider,PiProvider.CodexProvider(the reply carries only a decision),GeminiAcpProvider(it only selects an option),QwenProvider.toolCall.request.args = updatedInput, but_executeToolCallBodyrunsscheduledCall.invocation, which was built from the original args. OnlysetArgsInternalrebuilds it, and the SDK confirmation path never calls it.StatelessSessionProviderforwards the inner provider's value.MockSessionProviderdefaults to true and is mutable for tests.makeCanUseToolFn):updatedInputand the current provider lacks the capability, the call is denied before#applyHookInputMutation. The tool message is resolvedcancelledwith the reason, ahook.rewrite_refusedinfo message is sent, and the audit recordssession.hook_rewrite_refused.session.patch_refused.updatedInputreplacesevent.input(keys missing from it are deleted). That's safe becauseupdatedInputis always the full input: the hook rewrite, or the approver patch merged onto it. Thefake-pifixture mirrors this.docs/hooks.mdand thesession.approvalsprotocol note list which backends apply a rewrite.🤖 Generated with Claude Code