From 23bb524973d67a783911980d25460833f03befc3 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Wed, 13 May 2026 15:21:31 -0700 Subject: [PATCH] Spill oversized PreToolUse additionalContext (#22529) # Why `PreToolUse.additionalContext` became model-visible after #20692, but the hook-output spilling path from #21069 never picked up that newer lane. As a result, oversized `PreToolUse` context could bypass the truncation/spill treatment that already applies to the other hook outputs Codex forwards to the model. # What - Run `PreToolUseOutcome.additional_contexts` through `maybe_spill_texts(...)` - Add an integration test proving a large `PreToolUse.additionalContext` is replaced with a truncated preview plus spill-file pointer, while the full text is preserved on disk. --- codex-rs/core/tests/suite/hooks.rs | 61 ++++++++++++++++++++++++++++++ codex-rs/hooks/src/engine/mod.rs | 11 ++++-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index 635aeaf488..f059998052 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -1064,6 +1064,67 @@ async fn session_start_hook_spills_large_additional_context() -> Result<()> { Ok(()) } +#[tokio::test] +async fn pre_tool_use_hook_spills_large_additional_context() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let call_id = "pretooluse-shell-command-large-context"; + let command = "printf pre-tool-output".to_string(); + let args = serde_json::json!({ "command": command }); + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + core_test_support::responses::ev_function_call( + call_id, + "shell_command", + &serde_json::to_string(&args)?, + ), + ev_completed("resp-1"), + ]), + sse(vec![ + ev_response_created("resp-2"), + ev_assistant_message("msg-1", "pre hook context observed"), + ev_completed("resp-2"), + ]), + ], + ) + .await; + let additional_context = "remember the pre tool reef ".repeat(800); + + let mut builder = test_codex() + .with_pre_build_hook({ + let additional_context = additional_context.clone(); + move |home| { + if let Err(error) = + write_pre_tool_use_hook(home, Some("^Bash$"), "context", &additional_context) + { + panic!("failed to write pre tool use hook test fixture: {error}"); + } + } + }) + .with_config(trust_discovered_hooks); + let test = builder.build(&server).await?; + + test.submit_turn("run the shell command with large pre hook context") + .await?; + + let requests = responses.requests(); + assert_eq!(requests.len(), 2); + let developer_messages = requests[1].message_input_texts("developer"); + let developer_message = developer_messages + .iter() + .find(|message| spilled_hook_output_path(message).is_some()) + .context("spilled developer hook message")?; + assert!(developer_message.contains("tokens truncated")); + let path = spilled_hook_output_path(developer_message).context("spill path")?; + assert_eq!(fs::read_to_string(path)?, additional_context); + + Ok(()) +} + #[tokio::test] async fn stop_hook_spills_large_continuation_prompt() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/hooks/src/engine/mod.rs b/codex-rs/hooks/src/engine/mod.rs index 441c24314c..8a4a11a5d4 100644 --- a/codex-rs/hooks/src/engine/mod.rs +++ b/codex-rs/hooks/src/engine/mod.rs @@ -4,8 +4,6 @@ pub(crate) mod dispatcher; pub(crate) mod output_parser; pub(crate) mod schema_loader; -use std::collections::HashMap; - use crate::events::compact::PostCompactRequest; use crate::events::compact::PreCompactOutcome; use crate::events::compact::PreCompactRequest; @@ -32,6 +30,7 @@ use codex_protocol::protocol::HookRunSummary; use codex_protocol::protocol::HookSource; use codex_protocol::protocol::HookTrustStatus; use codex_utils_absolute_path::AbsolutePathBuf; +use std::collections::HashMap; #[derive(Debug, Clone)] pub(crate) struct CommandShell { @@ -180,7 +179,13 @@ impl ClaudeHooksEngine { } pub(crate) async fn run_pre_tool_use(&self, request: PreToolUseRequest) -> PreToolUseOutcome { - crate::events::pre_tool_use::run(&self.handlers, &self.shell, request).await + let session_id = request.session_id; + let mut outcome = + crate::events::pre_tool_use::run(&self.handlers, &self.shell, request).await; + outcome.additional_contexts = self + .maybe_spill_texts(session_id, outcome.additional_contexts) + .await; + outcome } pub(crate) async fn run_permission_request(