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.
This commit is contained in:
Abhinav
2026-05-13 15:21:31 -07:00
committed by GitHub
parent 7c57a59f51
commit 23bb524973
2 changed files with 69 additions and 3 deletions

View File

@@ -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(()));

View File

@@ -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(