hooks: emit Bash PostToolUse when exec_command completes via write_stdin (#18888)

Fixes #16246.

## Why

`exec_command` already emits `PreToolUse`, but long-running unified exec
commands that finish on a later `write_stdin` poll could miss the
matching `PostToolUse`. That left the Bash hook lifecycle inconsistent,
broke expectations around `tool_use_id` and `tool_input.command`, and
meant `PostToolUse` block/replacement feedback could fail to replace the
final session output before it reached model context.

This keeps the fix scoped to the `exec_command` / `write_stdin`
lifecycle. Broader non-Bash hook expansion is still out of scope here
and remains tracked separately in #16732.

## What changed

- Compute and store `PostToolUsePayload` while handlers still have
access to their concrete output type, and carry `tool_use_id` through
that payload.
- Preserve the original hook-facing `exec_command` string through
unified exec state (`ExecCommandRequest`, `ProcessEntry`,
`PreparedProcessHandles`, and `ExecCommandToolOutput`) via
`hook_command`, and remove the now-unused `session_command` output
metadata.
- Emit exactly one Bash `PostToolUse` for long-running `exec_command`
sessions when a later `write_stdin` poll observes final completion,
using the original `exec_command` call id and hook-facing command.
- Keep one-shot `exec_command` behavior aligned with the same payload
construction, including interactive completions that return a final
result directly.
- Apply `PostToolUse` block/replacement feedback before the final
`write_stdin` completion output is sent back to the model.
- Keep `write_stdin` itself out of `PreToolUse` matching so it continues
to act as transport/polling for the original Bash tool call.
- Restore plain matcher behavior for tool-name matchers such as `Bash`
and `Edit|Write`, while still treating patterns with regex characters
(for example `mcp__.*`) as regexes.
- Add unit coverage for unified exec payload construction and parallel
session separation, plus a core integration regression that verifies a
blocked `PostToolUse` replaces the final `write_stdin` output in model
context.

## Testing

- `cargo test -p codex-hooks`
- `cargo test -p codex-core post_tool_use_payload`
- `cargo test -p codex-core
post_tool_use_blocks_when_exec_session_completes_via_write_stdin`
This commit is contained in:
Andrei Eternal
2026-04-22 17:14:22 -07:00
committed by GitHub
Unverified
parent 6ca038bbd1
commit eed0e07825
15 changed files with 345 additions and 78 deletions
+1 -1
View File
@@ -148,7 +148,7 @@ struct ProcessEntry {
process: Arc<UnifiedExecProcess>,
call_id: String,
process_id: i32,
command: Vec<String>,
hook_command: String,
tty: bool,
network_approval_id: Option<String>,
session: Weak<Session>,
+2 -2
View File
@@ -113,7 +113,7 @@ async fn exec_command_with_tty(
process: Arc::clone(&process),
call_id: context.call_id.clone(),
process_id,
command: command.clone(),
hook_command: cmd.to_string(),
tty,
network_approval_id: None,
session: Arc::downgrade(session),
@@ -165,7 +165,7 @@ async fn exec_command_with_tty(
process_id: response_process_id,
exit_code,
original_token_count: Some(approx_token_count(&text)),
session_command: Some(command),
hook_command: Some(cmd.to_string()),
})
}
@@ -169,7 +169,7 @@ struct PreparedProcessHandles {
output_closed_notify: Arc<Notify>,
cancellation_token: CancellationToken,
pause_state: Option<watch::Receiver<bool>>,
command: Vec<String>,
hook_command: String,
process_id: i32,
tty: bool,
}
@@ -279,6 +279,7 @@ impl UnifiedExecProcessManager {
Arc::clone(&process),
context,
&request.command,
request.hook_command.clone(),
cwd.clone(),
start,
request.process_id,
@@ -398,7 +399,7 @@ impl UnifiedExecProcessManager {
process_id: response_process_id,
exit_code,
original_token_count: Some(original_token_count),
session_command: Some(request.command.clone()),
hook_command: Some(request.hook_command.clone()),
};
Ok(response)
@@ -418,7 +419,7 @@ impl UnifiedExecProcessManager {
output_closed_notify,
cancellation_token,
pause_state,
command: session_command,
hook_command,
process_id,
tty,
..
@@ -517,7 +518,7 @@ impl UnifiedExecProcessManager {
process_id,
exit_code,
original_token_count: Some(original_token_count),
session_command: Some(session_command.clone()),
hook_command: Some(hook_command),
};
Ok(response)
@@ -585,7 +586,7 @@ impl UnifiedExecProcessManager {
output_closed_notify,
cancellation_token,
pause_state,
command: entry.command.clone(),
hook_command: entry.hook_command.clone(),
process_id: entry.process_id,
tty: entry.tty,
})
@@ -597,6 +598,7 @@ impl UnifiedExecProcessManager {
process: Arc<UnifiedExecProcess>,
context: &UnifiedExecContext,
command: &[String],
hook_command: String,
cwd: AbsolutePathBuf,
started_at: Instant,
process_id: i32,
@@ -608,7 +610,7 @@ impl UnifiedExecProcessManager {
process: Arc::clone(&process),
call_id: context.call_id.clone(),
process_id,
command: command.to_vec(),
hook_command,
tty,
network_approval_id,
session: Arc::downgrade(&context.session),