[FEATURE] PostToolUse hooks: allow `updatedToolOutput` for built-in tools (context budget recovery)
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Tool results consume around 60% of context tokens in agentic Claude Code sessions. I've audited sessions across diverse workloads (coordinator agents, SDK-spawned bots, interactive CLI, subagent swarms) — every session had >49% tool result ratio, worst hit 73.6%.
Bash is the core problem. The same command produces wildly different output sizes depending on runtime state:
Command Best Case Worst Case Ratio
──────────────────── ────────── ────────── ─────
git status 5 tok 5,491 tok 1098x
git diff 20 tok 6,211 tok 311x
tail -100 <logfile> 200 tok 6,215 tok 31x
curl <api-endpoint> 50 tok 4,000 tok 80x
npm install 30 tok 3,500 tok 117x
docker logs 10 tok 5,000 tok 500x
PreToolUse can't solve this. For Read, the hook checks file size and injects limit/offset - input predicts output. For Bash, the hook sees the command string but is blind to output size. Pattern-matching known-verbose commands (git status → git status -s) becomes a whack-a-mole game that risks losing critical info (pre-commit errors, push rejections) and can never cover the long tail.
Claude Code's built-in BASH_MAX_OUTPUT_LENGTH (30K chars) is a blunt head+tail cap - still 10-25x more than semantic compression could achieve, and completely blind to signal vs noise.
The fundamental gap: PreToolUse can gate what goes in but is blind to what comes out. For Bash, input does not predict output. The only clean solution is post-execution result modification.
Proposed Solution
Extend updatedMCPToolOutput to work for all tools (not just MCP), or add a parallel updatedToolOutput field in PostToolUse hook output:
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"updatedToolOutput": "<compressed result string>"
}
}
When present, Claude Code replaces the tool result in conversation context with this value instead of the original tool_response. The original result is still available to the hook via stdin.
Built-in tool results follow known schemas. A hook using updatedToolOutput is responsible for preserving schema invariants - the same responsibility that updatedMCPToolOutput hooks bear for MCP tools. Tool-side state tracking (Read's file-read history, Write's success status) occurs at execution time, before the result enters context, so replacing the result string does not affect internal bookkeeping.
Implementation scope: The hook input already contains tool_response. The replacement mechanism already exists for MCP tools (updatedMCPToolOutput). The change extends that code path to built-in tools.
Alternative Solutions
I've exhausted every available mechanism:
| Approach | Result |
|----------|--------|
| PreToolUse updatedInput | Works for Read (inject limit/offset). Fails for Bash - can't predict output size from command string. |
| PostToolUse additionalContext | Adds tokens alongside the original result. Makes pollution worse. |
| PostToolUse updatedMCPToolOutput | Exactly the mechanism needed - but only works for MCP tools. |
| CLAUDE.md prompt discipline | Probabilistic - Claude follows ~70% of the time. |
| BASH_MAX_OUTPUT_LENGTH | 30K char blunt cap. Still 10-25x too large, semantically blind. |
| CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS | Global cap. Can't adapt per-call. |
| RTK (third-party PreToolUse rewriter) | Returns permissionDecision: "allow" on every rewritten command, bypassing Claude Code's permission system and safety hooks (rtk-ai/rtk#260). Incompatible with production security guardrails. |
Priority
High - Significant impact on productivity
Feature Category
Configuration and settings
Use Case Example
Scenario: I run a coordinator agent that orchestrates subagents via Claude Code SDK.
- Agent runs
git statuson a repo with 200+ untracked files → 5,491 tokens dumped into context - Agent only needed to know "branch is clean, 203 untracked files" → ~50 tokens of actual signal
- A PostToolUse hook would see the full result, compress it to a summary, and return ~200 tokens
- Savings: 94% per call. Across a session with 257 Bash calls, this recovers ~35% of total context budget
Another scenario:
- Agent runs
tail -100 app.logduring debugging → 6,215 tokens of log output - A PostToolUse hook keeps error lines + last 10 lines → ~500 tokens
- Without this feature, I can't even know the output will be large until after execution — PreToolUse is blind to it
This cannot be solved via PreToolUse. The hook doesn't know the output size until the command runs.
<details>
<summary>Example PostToolUse compression hook</summary>
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
[ "$TOOL" != "Bash" ] && exit 0
RESPONSE=$(echo "$INPUT" | jq -r '.tool_response // empty')
TOKEN_EST=$(echo -n "$RESPONSE" | wc -c | awk '{print int($1/4)}')
[ "$TOKEN_EST" -lt 1000 ] && exit 0
TOTAL_LINES=$(echo "$RESPONSE" | wc -l)
HEAD=$(echo "$RESPONSE" | head -30)
TAIL=$(echo "$RESPONSE" | tail -10)
COMPRESSED=$(printf "%s\n\n... [%d lines, ~%d tokens — truncated] ...\n\n%s" \
"$HEAD" "$TOTAL_LINES" "$TOKEN_EST" "$TAIL")
jq -n --arg content "$COMPRESSED" \
'{hookSpecificOutput: {hookEventName: "PostToolUse", updatedToolOutput: $content}}'
</details>
Additional Context
Prior Art: updatedMCPToolOutput already exists and works for MCP tools (confirmed in docs and via #24788). The replacement mechanism is implemented - this request extends it to built-in tools.
Prior requests (all closed without resolution):
- #4635 - received one engineering question, then auto-closed for inactivity
- #4544 - closed as duplicate of #4635
- #18594 - closed as stale
Audit data: 8 sessions, 603 tool calls, 626K total tokens. Tool results averaged ~60% of context. Worst offenders: Bash outliers (5-6K tokens per call), Read full-file dumps (2K avg), Task subagent result blobs (5.9K avg). Estimated 82% of tool result tokens are compactable.
Prompt caching benefit: Deterministic compression hooks produce stable, shorter results - improving prompt cache hit rates across turns.
8 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
I have directly addressed issues mentioned here as a duplicates and explained why this issue is different - I have provided more reasoning and preliminary research details.
This is well-researched and the data is compelling — 60%+ of context tokens going to tool results is a real problem, and the input/output asymmetry for Bash makes PreToolUse insufficient.
From a security perspective, PostToolUse output modification would also enable a powerful pattern we can't do today: redacting sensitive data from tool results before they enter the context window.
Right now, if a Bash command outputs environment variables containing API keys, or
git logshows a commit that accidentally included credentials, that content enters the context window and can leak through subsequent tool calls. A PostToolUse hook withupdatedToolOutputcould scan the result, redact secrets/PII, and return a cleaned version — the model never sees the raw credentials.This pattern composes well with the author's compression use case:
The scan step adds <1ms of latency (Sentinel AI's scanners run at ~0.05ms average) and prevents the compressed output from carrying sensitive content forward into the context.
+1 for extending
updatedToolOutputto built-in tools.Strong +1 on this. I've been building PostToolUse hooks for security scanning (PII/secrets detection in tool outputs) and hit the same limitation — we can detect sensitive data in tool results but can't redact it from the context window.
Our current PostToolUse hook (source) scans Bash/Read/WebFetch outputs for PII and secrets, but can only warn about leaks — it can't redact SSNs, API keys, or emails from what's actually stored in context.
The
updatedToolOutputfield would unlock two critical safety use cases:The SSN is 123-45-6789→The SSN is [REDACTED]before it enters contextThis would make PostToolUse hooks a complete safety layer rather than just an alerting mechanism.
Post-hoc trimming is a great escape hatch, but reducing output at the source is even better — no risk of accidentally stripping something the agent needs. We've been running MCP servers that return structured JSON (only fields an agent would act on) instead of raw CLI text, and it cuts tool output 80-90% before it ever hits the context window. Pare takes this approach for git, test runners, npm, build tools, etc.
Strong +1 on this. We hit the same wall and arrived at a similar design independently (#34872, now closed as duplicate).
Two additions from our experience:
1. Deferred eviction (
retain_turns)Not all tool outputs should be compressed immediately. A file read might still be useful for 2-3 turns (while the agent is editing it). We proposed a
retain_turnsfield that keeps full output for N turns, then auto-applies the summary:This avoids the problem of over-eager eviction while still reclaiming context when the output has served its purpose.
2. Between-session proof of concept
We've already built and open-sourced the between-session version of this pattern: Digital-Process-Tools/claude-remember
The architecture works. The gap is applying it within a session — which is exactly what
updatedToolOutputwould enable.Happy to share more implementation details if useful.
Additional evidence:
updatedInputfield whitelist is enforced at runtime (v2.1.88)We ran PoC experiments to test whether PreToolUse
updatedInputcould serve as a workaround by injecting output-limiting parameters into built-in tools. Results confirm it cannot:| Test | Hook Output | Actual Behavior | Result |
|------|------------|-----------------|--------|
| Read
limitinjection |{"updatedInput":{"file_path":"...","limit":5}}| Full file returned (113 lines) |limitignored || Grep
head_limitinjection |{"updatedInput":{...,"head_limit":3}}| All 7 matches returned |head_limitignored |Only whitelisted fields are applied:
file_pathfor Read/Edit/Write,commandfor Bash. All other fields inupdatedInputare silently dropped.This confirms the author's conclusion — PreToolUse cannot solve the output reduction problem for Read (beyond file_path redirect) or Grep/Glob at all.
updatedToolOutputin PostToolUse remains the only viable path for built-in tool output compression.Our use case: We maintain context-os, a token budget management hook suite. Current workarounds:
.tldrcache redirect viafile_path— works but requires pre-generated summary files+1 for extending
updatedToolOutputto built-in tools.This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.