[FEATURE] PostToolUse hooks: allow `updatedToolOutput` for built-in tools (context budget recovery)

Status Fixed / completed
Maintainer reply None cached
Activity 8 comments · opened Mar 8, 2026 · closed Apr 24, 2026

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 statusgit 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.

  1. Agent runs git status on a repo with 200+ untracked files → 5,491 tokens dumped into context
  2. Agent only needed to know "branch is clean, 203 untracked files" → ~50 tokens of actual signal
  3. A PostToolUse hook would see the full result, compress it to a summary, and return ~200 tokens
  4. Savings: 94% per call. Across a session with 257 Bash calls, this recovers ~35% of total context budget

Another scenario:

  1. Agent runs tail -100 app.log during debugging → 6,215 tokens of log output
  2. A PostToolUse hook keeps error lines + last 10 lines → ~500 tokens
  3. 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.

View original on GitHub ↗

8 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/4635
  2. https://github.com/anthropics/claude-code/issues/4544
  3. https://github.com/anthropics/claude-code/issues/18594

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

buildoak · 5 months ago

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.

MaxwellCalkin · 5 months ago

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 log shows a commit that accidentally included credentials, that content enters the context window and can leak through subsequent tool calls. A PostToolUse hook with updatedToolOutput could 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:

Raw output (5000 tokens) 
  → security scan (redact secrets/PII) 
  → semantic compression (500 tokens)
  → updatedToolOutput

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 updatedToolOutput to built-in tools.

MaxwellCalkin · 5 months ago

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 updatedToolOutput field would unlock two critical safety use cases:

  1. PII redaction: Replace The SSN is 123-45-6789The SSN is [REDACTED] before it enters context
  2. Secrets scrubbing: Strip leaked API keys, database URLs, JWT tokens from bash output

This would make PostToolUse hooks a complete safety layer rather than just an alerting mechanism.

Dave-London · 5 months ago

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.

fdaviddpt · 5 months ago

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_turns field that keeps full output for N turns, then auto-applies the summary:

{
  "context_directive": "summarize",
  "summary": "Read config.php (200 lines): DB config, cache settings, module options.",
  "retain_turns": 3
}

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

  • Haiku summarizes verbose session data into one-line entries
  • Near-Duplicate Compression merges repeated entries about the same work
  • Layered retention: recent data stays detailed, older data gets compressed
  • Production result: 81% token reduction in memory loaded at session start

The architecture works. The gap is applying it within a session — which is exactly what updatedToolOutput would enable.

Happy to share more implementation details if useful.

nullnull-kim · 4 months ago

Additional evidence: updatedInput field whitelist is enforced at runtime (v2.1.88)

We ran PoC experiments to test whether PreToolUse updatedInput could serve as a workaround by injecting output-limiting parameters into built-in tools. Results confirm it cannot:

| Test | Hook Output | Actual Behavior | Result |
|------|------------|-----------------|--------|
| Read limit injection | {"updatedInput":{"file_path":"...","limit":5}} | Full file returned (113 lines) | limit ignored |
| Grep head_limit injection | {"updatedInput":{...,"head_limit":3}} | All 7 matches returned | head_limit ignored |

Only whitelisted fields are applied: file_path for Read/Edit/Write, command for Bash. All other fields in updatedInput are 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. updatedToolOutput in 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:

  • Bash: PreToolUse command wrapping (test/build/install output filtering) — works but whack-a-mole as the author noted
  • Read: .tldr cache redirect via file_path — works but requires pre-generated summary files
  • Grep/Glob: No workaround available

+1 for extending updatedToolOutput to built-in tools.

github-actions[bot] · 3 months ago

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.