mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix(core): emit hooks for apply_patch edits (#18391)
Fixes https://github.com/openai/codex/issues/16732. ## Why `apply_patch` is Codex's primary file edit path, but it was not emitting `PreToolUse` or `PostToolUse` hook events. That meant hook-based policy, auditing, and write coordination could observe shell commands while missing the actual file mutation performed by `apply_patch`. The issue also exposed that the hook runtime serialized command hook payloads with `tool_name: "Bash"` unconditionally. Even if `apply_patch` supplied hook payloads, hooks would either fail to match it directly or receive misleading stdin that identified the edit as a Bash tool call. ## What Changed - Added `PreToolUse` and `PostToolUse` payload support to `ApplyPatchHandler`. - Exposed the raw patch body as `tool_input.command` for both JSON/function and freeform `apply_patch` calls. - Taught tool hook payloads to carry a handler-supplied hook-facing `tool_name`. - Preserved existing shell compatibility by continuing to emit `Bash` for shell-like tools. - Serialized the selected hook `tool_name` into hook stdin instead of hardcoding `Bash`. - Relaxed the generated hook command input schema so `tool_name` can represent tools other than `Bash`. ## Verification Added focused handler coverage for: - JSON/function `apply_patch` calls producing a `PreToolUse` payload. - Freeform `apply_patch` calls producing a `PreToolUse` payload. - Successful `apply_patch` output producing a `PostToolUse` payload. - Shell and `exec_command` handlers continuing to expose `Bash`. Added end-to-end hook coverage for: - A `PreToolUse` hook matching `^apply_patch$` blocking the patch before the target file is created. - A `PostToolUse` hook matching `^apply_patch$` receiving the patch input and tool response, then adding context to the follow-up model request. - Non-participating tools such as the plan tool continuing not to emit `PreToolUse`/`PostToolUse` hook events. Also validated manually with a live `codex exec` smoke test using an isolated temp workspace and temp `CODEX_HOME`. The smoke test confirmed that a real `apply_patch` edit emits `PreToolUse`/`PostToolUse` with `tool_name: "apply_patch"`, a shell command still emits `tool_name: "Bash"`, and a denying `PreToolUse` hook prevents the blocked patch file from being created.
This commit is contained in:
@@ -27,6 +27,18 @@ pub(crate) fn select_handlers(
|
||||
event_name: HookEventName,
|
||||
matcher_input: Option<&str>,
|
||||
) -> Vec<ConfiguredHandler> {
|
||||
let matcher_inputs = matcher_input.into_iter().collect::<Vec<_>>();
|
||||
select_handlers_for_matcher_inputs(handlers, event_name, &matcher_inputs)
|
||||
}
|
||||
|
||||
pub(crate) fn select_handlers_for_matcher_inputs(
|
||||
handlers: &[ConfiguredHandler],
|
||||
event_name: HookEventName,
|
||||
matcher_inputs: &[&str],
|
||||
) -> Vec<ConfiguredHandler> {
|
||||
// Check each configured handler once, even when several compatibility names
|
||||
// match the same regex. A hook like `apply_patch|Write|Edit` should run a
|
||||
// single time for one tool call, not once per matching alias.
|
||||
handlers
|
||||
.iter()
|
||||
.filter(|handler| handler.event_name == event_name)
|
||||
@@ -35,7 +47,13 @@ pub(crate) fn select_handlers(
|
||||
| HookEventName::PermissionRequest
|
||||
| HookEventName::PostToolUse
|
||||
| HookEventName::SessionStart => {
|
||||
matches_matcher(handler.matcher.as_deref(), matcher_input)
|
||||
if matcher_inputs.is_empty() {
|
||||
matches_matcher(handler.matcher.as_deref(), /*input*/ None)
|
||||
} else {
|
||||
matcher_inputs
|
||||
.iter()
|
||||
.any(|input| matches_matcher(handler.matcher.as_deref(), Some(input)))
|
||||
}
|
||||
}
|
||||
HookEventName::UserPromptSubmit | HookEventName::Stop => true,
|
||||
})
|
||||
@@ -128,6 +146,7 @@ mod tests {
|
||||
|
||||
use super::ConfiguredHandler;
|
||||
use super::select_handlers;
|
||||
use super::select_handlers_for_matcher_inputs;
|
||||
|
||||
fn make_handler(
|
||||
event_name: HookEventName,
|
||||
@@ -282,6 +301,51 @@ mod tests {
|
||||
assert_eq!(selected_bash.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_tool_use_aliases_match_once_per_handler() {
|
||||
let handlers = vec![
|
||||
make_handler(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^apply_patch$"),
|
||||
"echo apply_patch",
|
||||
/*display_order*/ 0,
|
||||
),
|
||||
make_handler(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^Write$"),
|
||||
"echo write",
|
||||
/*display_order*/ 1,
|
||||
),
|
||||
make_handler(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^Edit$"),
|
||||
"echo edit",
|
||||
/*display_order*/ 2,
|
||||
),
|
||||
make_handler(
|
||||
HookEventName::PreToolUse,
|
||||
Some("apply_patch|Write|Edit"),
|
||||
"echo combined",
|
||||
/*display_order*/ 3,
|
||||
),
|
||||
];
|
||||
|
||||
let selected = select_handlers_for_matcher_inputs(
|
||||
&handlers,
|
||||
HookEventName::PreToolUse,
|
||||
&["apply_patch", "Write", "Edit"],
|
||||
);
|
||||
|
||||
assert_eq!(selected.len(), 4);
|
||||
assert_eq!(
|
||||
selected
|
||||
.iter()
|
||||
.map(|handler| handler.display_order)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![0, 1, 2, 3],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_prompt_submit_ignores_matcher() {
|
||||
let handlers = vec![
|
||||
|
||||
Reference in New Issue
Block a user