How Claude Code injects context: hooks, native memory, and the prompt cache

How Claude Code injects context: hooks, native memory, and the prompt cache

My agents have long-term memory, and at some point that memory has to actually reach the model. The lazy answer is to shove everything into every prompt, every turn, and let the LLM sort it out. That doesn't scale, and most of the time it isn't even necessary.

Most of what an agent needs isn't needed on every single turn. Short and medium-term memory, what's currently in flight, the last few decisions, sits fine as a compact summary already in context, cheap and always there. The deep stuff, the specific decision from three weeks ago, the exact reasoning behind something, lives in the Brain's vector database instead, and only gets pulled out when the conversation genuinely needs it. That's a tool call, and a tool call is a round trip, slower than something already sitting in context. Worth paying for when you actually need the detail. Not worth paying on every single turn just in case.

That tradeoff, cheap-and-always-present versus slower-and-fetched-on-demand, is the one that matters. And once you're thinking in those terms about your own memory system, you start asking it about everything else feeding the model too. Token optimisation is a constant, ongoing thing on my team, not a one-off pass, and a good chunk of it is just watching how Claude Code itself gets context into the model behind the scenes, which channel it uses, what lands in the cache and what doesn't, what a subagent inherits versus what it has to fetch fresh.

That's what this post is actually about. Not memory in the abstract, the concrete mechanics of how context actually gets injected, what each channel costs, and specifically what's cached and what isn't, because caching is where most of the real money gets saved or quietly wasted.

The one idea to hold onto

Every token an agent "knows" arrives through one of a handful of INJECTION CHANNELS, and each channel has three properties that decide its cost: its size cap, whether it lands in the cached prefix, and whether spawned subagents inherit it. Optimising an agent is mostly a matter of putting each piece of context in the channel whose three properties fit it. Get the channel wrong and you either truncate silently, pay for the same bytes every turn, or multiply a cost across every subagent you spawn.

The channels, and their three properties

Sizes below are given in tokens, the actual currency you're billed in, with the raw character/line limits noted alongside since that's how the platform defines them. Rough conversion used throughout: about 4 characters per token.

1. CLAUDE.md (and its @-imports)

  • Cap: none. Loaded in FULL, however big.
  • Cached: yes, part of the stable prefix.
  • Subagent inheritance: YES. This is the trap. A subagent is forced to load the project's CLAUDE.md and everything it @-imports. Put a persona worth roughly 5,000 tokens (about 19KB of markdown) in CLAUDE.md and every subagent you spawn pays that same ~5,000 uncached tokens on top of its actual task.

2. SessionStart hook additionalContext

  • Cap: HARD 10,000 characters, roughly 2,500 tokens. Over that, Claude Code persists it to a file and injects only a short preview + pointer, i.e. it silently becomes a stub.
  • Cached: yes, it becomes part of the session's cached prefix (static between session-start events, so it is cache-READ each turn, not re-sent).
  • Subagent inheritance: NO. SessionStart does not fire for spawned subagents. This is what makes it the right channel for main-session-only context.

3. Native auto-memory (MEMORY.md + per-fact files)

  • Cap: first 200 lines OR roughly 6,000 tokens (25KB) of MEMORY.md, whichever comes first. Beyond that is simply not loaded at session start (the topic files load only when the agent Reads them on demand).
  • Cached: yes (native, in the prefix).
  • Subagent inheritance: YES (confirmed empirically, a spawned subagent carried the full injected memory).
  • Keying gotcha: the memory directory is derived from the GIT REPO ROOT, so every subdirectory of one repo SHARES one memory dir unless you override it with autoMemoryDirectory.

4. UserPromptSubmit hook additionalContext

  • Cap: same 10,000-character (~2,500-token) hook limit applies.
  • Cached: NO in the useful sense. It is injected fresh every turn, so it is NOT amortised by the cache. Keep it small and genuinely dynamic (a live clock, unread inbox, fresh observations), never large static content.
  • Subagent inheritance: fires per prompt, so scope carefully.

Why the cache is the whole game

The Anthropic prompt cache rewards a STABLE PREFIX. Content that sits unchanged at the front of the context (system prompt, CLAUDE.md, SessionStart injections) is written to cache once and cache-READ cheaply on every subsequent turn. Content injected per-turn (UserPromptSubmit) sits after the stable prefix and is paid fresh each time.

Two consequences that drive real decisions:

  • Large + static, put it in the cached prefix (SessionStart hook or CLAUDE.md), not a per-turn hook. You pay once, then read cheap.
  • Subagents do not share the main session's cache. Everything a subagent is forced to load (inherited CLAUDE.md, inherited memory) is uncached, paid in full, EVERY spawn. So the highest-leverage optimisation is usually not the main loop, it is making subagents lean.

For scale: Haiku's own context window tops out at 200,000 tokens; Sonnet, Opus and Fable all run up to a million. Worth being precise about which channel actually multiplies here, though. The SessionStart hook's 2,500-token cap is capped, but it's also never passed to subagents at all, so on its own it's moot for this story. CLAUDE.md is the one that matters: no cap, and always inherited by every subagent. A five-thousand-token persona sat in CLAUDE.md gets paid, uncached, by every single subagent you spawn, that's exactly what bit us before the persona cutover in worked example one, below. Spawn a hundred subagents in a day and that's the tax that actually compounds, not the hook cap.

Why the actual work happens in subagents, not the main thread

My long-lived agents, the ones with a persona, a memory, a standing role on the team, always have their memory loaded and injected. That's deliberate, they need to remember who they are and what they've learned across a session that can run for days. But that memory isn't free. It's real tokens sitting in context on every single turn, cache-read cheap, but not zero, and it still counts against the context window.

If that same long-lived agent then did the actual work itself, a long multi-step search, a big refactor, a research sweep, every turn of that work would carry the full weight of its own memory alongside it. The context grows fast, compaction kicks in sooner, and you're paying the memory tax on every step of a job that has nothing to do with remembering who you are.

The fix is to keep the main thread light and hand the actual work to a subagent instead. A subagent starts clean. It doesn't carry the main agent's full persona and memory stack, that's the SessionStart mechanism below, subagents never fire it, so they never inherit that injected content. It gets a scoped task, does the work, and hands back a compact result. The main thread's own history barely grows, one instruction out, one result back, while the heavy lifting happened somewhere its context cost doesn't compound against yours.

image.png

Worked example 1: the persona cutover

Personas used to live as @-imports in each agent's CLAUDE.md (a big block of full persona files). Two problems: (a) it was inherited by every subagent, pure uncached waste on task-scoped workers, and (b) an earlier attempt to move it into a SessionStart hook blew the 10,000-character (~2,500-token) cap and truncated to a useless stub.

The fix matched content to channel:

  • A distilled CORE (cardinals + character + routing) injected via the SessionStart hook. Under the 2,500-token cap, cached, main-only (subagents do not fire SessionStart), so subagents stop inheriting it.
  • A forceful "first action: read the full files" instruction for depth on demand.

Result: subagent persona load dropped by more than an order of magnitude. Main session keeps its cardinals inline (cached) and pulls full depth by reading files when needed.

The honest caveat (a real lesson): the "read the files" step is a SKIPPABLE instruction, and under a live task agents skip it. Cardinals survived because they are in the CORE; what was lost was register and depth. So a hook can reliably deliver a bounded floor, but it cannot force an unbounded read. If depth must be guaranteed, it has to be in the cached prefix, not behind an optional action.

Worked example 2: the shared-memory accident

Because native auto-memory keys to the git repo root, and a whole agent fleet can live under one repo, agents can end up unknowingly SHARING a single memory file, commingled across many agents. Two failures at once: cross-agent leak (one agent's memory injected into others) and truncation (only the first ~6,000 tokens of a larger index ever loads, so the rest never reaches the model).

The fix: set autoMemoryDirectory per agent so each reads/writes its own dir, and split the commingled file back to its true owners. Controls worth knowing: autoMemoryEnabled: false and CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 turn memory off entirely; there is no native "exclude subagents only" switch, so fully removing memory from subagents needs disable-plus-a-main-only-reinject-hook, the same pattern as the persona cutover.

Worked example 3: the Claude Code port

This one is my own migration story, a founder porting a working system between platforms, not an internal audit.

I had built an observational memory system for my agentic setup in OpenClaw: every turn, the full memory dump got passed straight into context. It worked well. When I lost OpenClaw access, I set out to port the same pattern into Claude Code, expecting a straightforward like-for-like move. It wasn't. Claude Code has no per-turn "just pass it all in" channel, the nearest equivalent, CLAUDE.md, is uncapped but INHERITED, so a full memory dump living there gets forced onto every subagent spawned off the main thread, whether or not that subagent's task has anything to do with it. I didn't know why token spend was climbing until I traced it back to exactly this: subagents doing small, scoped jobs were each paying for the whole memory file.

My first fix attempt was a per-turn tool call injecting the memory fresh each time, the UserPromptSubmit-style pattern. That solved the subagent-inheritance problem but created a new one: injecting fresh content every turn busts the prompt cache the moment anything in it changes even slightly, so I was paying full uncached price on every single turn. The fix was to move the injection to the SessionStart hook instead, main-session only (subagents don't trigger it), and static between session-start events, so it sits in the cached prefix rather than being re-paid every turn.

That surfaced the next wall: the SessionStart hook has a hard 10,000-character, roughly 2,500-token cap (not the 20,000 I said on camera, a slip I corrected in the video itself, so this write-up states the real number). Anything over the cap gets truncated and replaced with a pointer to a file, which defeats the point, the agent no longer has memory present in context each turn, it has to go and look a file up. The actual fix: stop putting full content in CLAUDE.md and switch to pointers, CLAUDE.md references the files where the real information lives, and the SessionStart hook injects a cut-down summary of them (comfortably under the cap), enough gist to be useful, plus an instruction to read the full files from the CLAUDE.md links if more depth is needed.

That fix surfaced one more mechanism worth stating plainly, because it is the root cause behind the original inheritance problem: @-linking a file in CLAUDE.md pulls its full content in; a bare pointer or mention does not. My agents' identity, soul and memory files were @-linked, which is exactly what dragged them into every subagent in the first place. Swapping to plain pointers (not @-links) plus the SessionStart summary is what actually cut subagent token spend, because SessionStart hooks don't fire for subagents at all, so a subagent now inherits the pointer-only CLAUDE.md (small, static, cached) instead of the fully-expanded memory dump.

image.png

The transferable principles

  1. Classify every context block by the three properties (cap, cached?, subagent-inherited?) before deciding where it lives.
  2. Static and large belongs in the cached prefix. Dynamic and small belongs in the per-turn hook. Never the reverse.
  3. Subagent-inherited channels are cost multipliers. Audit what your subagents are forced to carry; that is usually where the waste is, because subagents run uncached.
  4. Respect the caps or you truncate silently. ~2,500 tokens for hook output, ~6,000 tokens (200 lines) for memory, uncapped-but-inherited for CLAUDE.md. Over-cap failures are quiet (a stub, a dropped tail), not loud.
  5. A hook can guarantee a bounded floor, not an unbounded read. If something must always be present, inline it under the cap; do not rely on an instruction to fetch it.
  6. Verify the mechanism empirically. The docs are silent or ambiguous on several of these (subagent memory inheritance, the exact key derivation). Confirm behaviour by probing a live subagent's context and reading ground-truth catalogs, not by trusting assumptions.