mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Support PreToolUse updatedInput rewrites (#20527)
## Why
`PreToolUse` already exposes `updatedInput` in its hook output schema,
but Codex currently rejects it instead of applying the rewrite. That
leaves hook authors unable to make the documented pre-execution
adjustment to a tool call before it runs.
## What
- Accept `updatedInput` from `PreToolUse` hooks when paired with
`permissionDecision: "allow"`.
- Apply the rewritten input before dispatch so the tool executes the
updated payload, not the original one.
- Preserve the stable hook-facing compatibility shapes that
participating tool handlers expose:
- Bash-like tools (`shell`, `container.exec`, `local_shell`,
`shell_command`, `exec_command`) use `{ "command": ... }`.
- `apply_patch` exposes its patch body through the same command-shaped
hook contract.
- MCP tools expose their JSON argument object directly.
- Keep each participating tool handler responsible for translating
hook-facing `updatedInput` back into its concrete invocation shape.
## Verification
Direct Bash-like rewrite coverage:
- `pre_tool_use_rewrites_shell_before_execution`
- `pre_tool_use_rewrites_container_exec_before_execution`
- `pre_tool_use_rewrites_local_shell_before_execution`
- `pre_tool_use_rewrites_shell_command_before_execution`
- `pre_tool_use_rewrites_exec_command_before_execution`
These cases assert that each supported Bash-like surface runs only the
rewritten command while the hook still observes the original `{
"command": ... }` input.
`pre_tool_use_rewrites_apply_patch_before_execution`
- Model emits one patch.
- Hook swaps in a different patch.
- Asserts only the rewritten file is created, and the hook saw the
original patch.
`pre_tool_use_rewrites_code_mode_nested_exec_command_before_execution`
- Model runs one nested shell command from code mode.
- Hook rewrites it.
- Asserts only the rewritten command runs, and the hook saw the original
nested input.
`pre_tool_use_rewrites_mcp_tool_before_execution`
- Model calls the RMCP echo tool.
- Hook rewrites the MCP arguments.
- Asserts the MCP server receives and returns the rewritten message, not
the original one.
This commit is contained in:
committed by
GitHub
Unverified
parent
17ed5ad0b0
commit
d08906a944
@@ -44,6 +44,11 @@ pub(crate) struct HookRuntimeOutcome {
|
||||
pub additional_contexts: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) enum PreToolUseHookResult {
|
||||
Continue { updated_input: Option<Value> },
|
||||
Blocked(String),
|
||||
}
|
||||
|
||||
pub(crate) enum PendingInputHookDisposition {
|
||||
Accepted(Box<PendingInputRecord>),
|
||||
Blocked { additional_contexts: Vec<String> },
|
||||
@@ -141,7 +146,7 @@ pub(crate) async fn run_pre_tool_use_hooks(
|
||||
tool_use_id: String,
|
||||
tool_name: &HookToolName,
|
||||
tool_input: &Value,
|
||||
) -> Option<String> {
|
||||
) -> PreToolUseHookResult {
|
||||
let request = PreToolUseRequest {
|
||||
session_id: sess.conversation_id,
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
@@ -163,25 +168,32 @@ pub(crate) async fn run_pre_tool_use_hooks(
|
||||
should_block,
|
||||
block_reason,
|
||||
additional_contexts,
|
||||
updated_input,
|
||||
} = hooks.run_pre_tool_use(request).await;
|
||||
emit_hook_completed_events(sess, turn_context, hook_events).await;
|
||||
record_additional_contexts(sess, turn_context, additional_contexts).await;
|
||||
|
||||
if should_block {
|
||||
block_reason.map(|reason| {
|
||||
if (tool_name.name() == "Bash" || tool_name.name() == "apply_patch")
|
||||
&& let Some(command) = tool_input.get("command").and_then(Value::as_str)
|
||||
{
|
||||
format!("Command blocked by PreToolUse hook: {reason}. Command: {command}")
|
||||
} else {
|
||||
format!(
|
||||
"Tool call blocked by PreToolUse hook: {reason}. Tool: {}",
|
||||
tool_name.name()
|
||||
)
|
||||
}
|
||||
})
|
||||
if !should_block {
|
||||
return PreToolUseHookResult::Continue { updated_input };
|
||||
}
|
||||
|
||||
let Some(reason) = block_reason else {
|
||||
return PreToolUseHookResult::Continue {
|
||||
updated_input: None,
|
||||
};
|
||||
};
|
||||
|
||||
if (tool_name.name() == "Bash" || tool_name.name() == "apply_patch")
|
||||
&& let Some(command) = tool_input.get("command").and_then(Value::as_str)
|
||||
{
|
||||
PreToolUseHookResult::Blocked(format!(
|
||||
"Command blocked by PreToolUse hook: {reason}. Command: {command}"
|
||||
))
|
||||
} else {
|
||||
None
|
||||
PreToolUseHookResult::Blocked(format!(
|
||||
"Tool call blocked by PreToolUse hook: {reason}. Tool: {}",
|
||||
tool_name.name()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user