Spill large hook outputs from context (#21069)

## Why

Large hook outputs can enter model-visible context through hook-specific
paths such as `additionalContext` and `Stop` continuation prompts.
Without a dedicated cap, one hook can inject a large blob directly into
conversation history instead of leaving a bounded preview for the model
and preserving the full text elsewhere.

## What

- spill hook text once it exceeds a fixed `2_500`-token budget,
preserving the full output on disk and leaving a head/tail preview plus
saved path in context
- add shared hook-output spilling under
`CODEX_HOME/hook_outputs/<thread_id>/<uuid>.txt`
- apply the cap to both `additionalContext`, `feedback_message`, and
`Stop` continuation fragments
This commit is contained in:
Abhinav
2026-05-05 05:03:18 +00:00
committed by GitHub
parent 33d24b0df5
commit dca105cf99
7 changed files with 439 additions and 13 deletions
+65 -12
View File
@@ -6,14 +6,6 @@ pub(crate) mod schema_loader;
use std::collections::HashMap;
use codex_config::ConfigLayerStack;
use codex_plugin::PluginHookSource;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookHandlerType;
use codex_protocol::protocol::HookRunSummary;
use codex_protocol::protocol::HookSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::events::permission_request::PermissionRequestOutcome;
use crate::events::permission_request::PermissionRequestRequest;
use crate::events::post_tool_use::PostToolUseOutcome;
@@ -26,6 +18,15 @@ use crate::events::stop::StopOutcome;
use crate::events::stop::StopRequest;
use crate::events::user_prompt_submit::UserPromptSubmitOutcome;
use crate::events::user_prompt_submit::UserPromptSubmitRequest;
use crate::output_spill::HookOutputSpiller;
use codex_config::ConfigLayerStack;
use codex_plugin::PluginHookSource;
use codex_protocol::ThreadId;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookHandlerType;
use codex_protocol::protocol::HookRunSummary;
use codex_protocol::protocol::HookSource;
use codex_utils_absolute_path::AbsolutePathBuf;
#[derive(Debug, Clone)]
pub(crate) struct CommandShell {
@@ -90,6 +91,7 @@ pub(crate) struct ClaudeHooksEngine {
handlers: Vec<ConfiguredHandler>,
warnings: Vec<String>,
shell: CommandShell,
output_spiller: HookOutputSpiller,
}
impl ClaudeHooksEngine {
@@ -105,6 +107,7 @@ impl ClaudeHooksEngine {
handlers: Vec::new(),
warnings: Vec::new(),
shell,
output_spiller: HookOutputSpiller::new(),
};
}
@@ -118,6 +121,7 @@ impl ClaudeHooksEngine {
handlers: discovered.handlers,
warnings: discovered.warnings,
shell,
output_spiller: HookOutputSpiller::new(),
}
}
@@ -155,7 +159,13 @@ impl ClaudeHooksEngine {
request: SessionStartRequest,
turn_id: Option<String>,
) -> SessionStartOutcome {
crate::events::session_start::run(&self.handlers, &self.shell, request, turn_id).await
let session_id = request.session_id;
let mut outcome =
crate::events::session_start::run(&self.handlers, &self.shell, request, turn_id).await;
outcome.additional_contexts = self
.maybe_spill_texts(session_id, outcome.additional_contexts)
.await;
outcome
}
pub(crate) async fn run_pre_tool_use(&self, request: PreToolUseRequest) -> PreToolUseOutcome {
@@ -173,7 +183,16 @@ impl ClaudeHooksEngine {
&self,
request: PostToolUseRequest,
) -> PostToolUseOutcome {
crate::events::post_tool_use::run(&self.handlers, &self.shell, request).await
let session_id = request.session_id;
let mut outcome =
crate::events::post_tool_use::run(&self.handlers, &self.shell, request).await;
outcome.additional_contexts = self
.maybe_spill_texts(session_id, outcome.additional_contexts)
.await;
outcome.feedback_message = self
.maybe_spill_text(session_id, outcome.feedback_message)
.await;
outcome
}
pub(crate) fn preview_user_prompt_submit(
@@ -187,7 +206,13 @@ impl ClaudeHooksEngine {
&self,
request: UserPromptSubmitRequest,
) -> UserPromptSubmitOutcome {
crate::events::user_prompt_submit::run(&self.handlers, &self.shell, request).await
let session_id = request.session_id;
let mut outcome =
crate::events::user_prompt_submit::run(&self.handlers, &self.shell, request).await;
outcome.additional_contexts = self
.maybe_spill_texts(session_id, outcome.additional_contexts)
.await;
outcome
}
pub(crate) fn preview_stop(&self, request: &StopRequest) -> Vec<HookRunSummary> {
@@ -195,7 +220,35 @@ impl ClaudeHooksEngine {
}
pub(crate) async fn run_stop(&self, request: StopRequest) -> StopOutcome {
crate::events::stop::run(&self.handlers, &self.shell, request).await
let session_id = request.session_id;
let mut outcome = crate::events::stop::run(&self.handlers, &self.shell, request).await;
outcome.continuation_fragments = self
.maybe_spill_prompt_fragments(session_id, outcome.continuation_fragments)
.await;
outcome
}
async fn maybe_spill_texts(&self, session_id: ThreadId, texts: Vec<String>) -> Vec<String> {
self.output_spiller
.maybe_spill_texts(session_id, texts)
.await
}
async fn maybe_spill_text(&self, session_id: ThreadId, text: Option<String>) -> Option<String> {
match text {
Some(text) => Some(self.output_spiller.maybe_spill_text(session_id, text).await),
None => None,
}
}
async fn maybe_spill_prompt_fragments(
&self,
session_id: ThreadId,
fragments: Vec<codex_protocol::items::HookPromptFragment>,
) -> Vec<codex_protocol::items::HookPromptFragment> {
self.output_spiller
.maybe_spill_prompt_fragments(session_id, fragments)
.await
}
}