[FEATURE] A hook that can transform the message array before each model request

Status Closed — not planned
Maintainer reply None cached
Activity 1 comment · opened Aug 7, 2026 · closed Aug 7, 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

There is no way for an extension to remove or rewrite conversation content that is already in context.

The decisive constraint is timing. In a long agentic session the expensive content is tool results — a file read, a large grep, a test log. At the moment that content is produced you cannot know whether it will still matter in ten turns. You only learn that it is finished with later. By then, no hook can reach it.

I enumerated the full hook surface against Claude Code v2.1.224 and the official docs. Of 30 hook events, exactly four can alter content, and every one of them acts at creation time or on display only:

| Hook | What it can change | Why it doesn't solve this |
|---|---|---|
| PreToolUse.updatedInput | tool input | before the result exists |
| PostToolUse.updatedToolOutput | tool result | fires when the result is created — the wrong moment |
| PermissionRequest | tool input | same |
| MessageDisplay.displayContent | on-screen text | display only; the model still sees the original |

Everything else can only append (additionalContext), block, or produce side effects. The docs state it plainly: hooks "cannot directly modify, delete, or replace messages in the ongoing conversation."

How much context this concerns. Across 371 real transcripts (29,175 messages, 51.4 MB of content blocks): tool results are 31% of stored content, and since thinking blocks travel with empty text (signature only), roughly 53% of what is actually transmitted each request. In long sessions this is the dominant cost, and it is precisely the content whose usefulness expires.

Concretely, from the same corpus, a Read tool result measures 1,013 tokens at the median and 3,496 at the mean (p90 3,287 — the mean sits above p90 because a few very large reads skew it). So the 40-file refactoring session in the use case below carries roughly 40K tokens of file contents at the median, and ~140K at the mean — 20–70% of a 200K window, all of it dead weight once each subsystem is done, and none of it removable.

What I tried, and why each falls short:

  1. Rewriting the session transcript (~/.claude/projects/<slug>/<sid>.jsonl). The file is a parentUuid linked list, so replacing a span with a summary entry is well-defined, and Claude Code honours the edit on --resume (verified). But it holds the conversation in memory during a session — I pruned a live transcript and watched the request message count keep climbing (1 → 3 → 5 → 7 → 9 → 11, never dropping). So edits only land at session boundaries, and compression that requires a restart isn't usable.
  1. A local gateway that rewrites request bodies. Technically this works, but the gateway protocol reference is explicit that a gateway should "inspect without modifying", and the system attribution block carries "a fingerprint derived from the conversation". I am not asking for a workaround here — I am asking for the supported path, because the supported path does not exist.
  1. Subagent delegation, which the cost docs recommend, keeps bulk out of the main context — but only for work you knew to delegate in advance.

Proposed Solution

A hook that receives the message array about to be sent to the model and may return a modified array.

// settings.json
{
  "hooks": {
    "PreModelRequest": [
      { "hooks": [ { "type": "command", "command": "~/.claude/hooks/context-manager.mjs" } ] }
    ]
  }
}

The hook receives the outbound messages plus token accounting, and returns either nothing (unchanged) or a replacement array:

{
  "hookSpecificOutput": {
    "hookEventName": "PreModelRequest",
    "updatedMessages": [ /* ... */ ]
  }
}

Claude Code would validate the result before sending — rejecting anything that breaks API invariants (unpaired tool_use/tool_result, modified signed thinking blocks) and falling back to the original array, the same way updatedToolOutput is already validated against the tool's output schema today.

This has direct precedent in comparable agent harnesses: OpenCode exposes experimental.chat.messages.transform, and the Pi coding agent exposes a context event with the same shape. A whole class of context-management extensions is built on those hooks and cannot be ported to Claude Code for want of an equivalent.

This improves prompt caching rather than hurting it

The obvious objection is cache invalidation. The measured result is the opposite, for two reasons.

The system and tools prefix is never touched. Message-array edits sit entirely after it, so the largest cached block survives every rewrite. I measured this directly: across 19 consecutive requests in which the message array was rewritten from 41 messages down to 4 every single time, cache_read_input_tokens held at 40,969 on all 19 — matching what /context attributes to system prompt + tools + skills. Even under that pathologically aggressive rewriting, 93.8% of input tokens still came from cache.

The edit geometry is prefix-stable between compressions. Incremental compression rewrites the oldest segment wholesale — old messages become a summary block, and later those blocks are distilled into denser ones — leaving the shape [summaries][recent raw messages]. It never holds head and tail fixed while altering the middle. Between compressions the prefix is byte-identical and the growing tail caches normally; a compression invalidates the message segment once, and that segment is small precisely because compression keeps it small.

Production numbers from the OpenCode ACP plugin, over 6 engineering sessions and 11,000+ API calls: aggregate prompt-cache hit ratio 91% (87–95% per session), with context held at p50 ~100K and p90 150–190K of a 1M window.

Compare that to threshold-based compaction, which fires only at 80–90% of the window and then invalidates 100% of a context that had grown to 50–80% of the window. Frequent small invalidations of a small context cost far less than rare total invalidations of a large one — which is why incremental compression ends up ahead on both cache hit rate and total tokens.

Alternative Solutions

A much smaller version would solve most of it. Claude Code already sends context_management — but only {"edits":[{"type":"clear_thinking_20251015","keep":"all"}]}. The API also supports clear_tool_uses_20250919, which retroactively clears old tool results server-side. Exposing that as a setting requires no new API surface and would address the ~53% case on its own.

That variant has been raised before: #26215 (Feb) and #44521 (Apr), both closed as not planned, with #44521 re-filed as #68074, which is currently open. #44521 carries the fullest write-up, including production impact data from long agent sessions. I am not re-filing that request here — this issue is for the general hook — but I would be glad to see either land, and the smaller one is the higher value per unit of effort.

The general hook is worth more because it allows summarisation — replacing a range with model-written text — rather than only dropping content. clear_tool_uses discards stale tool results; a transform hook can replace them with a summary that keeps the conclusions. But if only one is possible, the setting is the cheaper win.

Priority

High - Significant impact on productivity

Feature Category

API and model interactions

Use Case Example

A long refactoring session reads 40 files. After each subsystem is finished, those file contents are dead weight, but they stay in every subsequent request until auto-compact eventually summarises the entire conversation — losing recent detail along with the stale content.

With a message-transform hook, an extension could replace just the finished ranges with short summaries, on its own schedule, keeping recent work at full fidelity. This is what /compact does, except selective and incremental instead of all-or-nothing.

Additional Context

All findings above are from Claude Code v2.1.224 on Linux: binary string forensics for the hook and feature surface, and a read-only logging proxy (request bodies unmodified) for observing live traffic. Happy to share the measurement scripts if useful.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗