anthropics / anthropics/claude-code

Hook `additionalContext` is silently truncated at 10,000 characters, discarding injected instructions

Open
#94,358 0 comments 0 reactions 0 assignees View on GitHub
area:hooks bug platform:linux
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

# Hook `additionalContext` is silently truncated at 10,000 characters, discarding injected instructions

## Summary

When a hook returns more than 10,000 characters of `hookSpecificOutput.additionalContext`, Claude Code writes the payload to a file and replaces it in the model's context with a 2,000-character preview plus a file path. There is no error, no warning in the UI, and nothing in the model's context that marks the dropped text as mandatory. The model proceeds believing it has received everything the hook sent.

This is a correctness problem rather than a formatting one, because `additionalContext` is the documented channel for injecting a session's standing instructions.

## Use case affected

Hook-based context injection is the supported way to give a session operating rules it must follow: `SessionStart` and `UserPromptSubmit` hooks emit the rules that apply, and the agent is expected to obey them for the rest of the session.

A memory system built this way — an MCP server that stores rules, scoped by subject, and a hook that injects whichever rules apply to the current session — crosses 10 KB routinely. It crosses it *first* at `SessionStart`, which is exactly when the largest set of always-applicable rules is delivered and when the agent has the least other context to fall back on.

The failure is silent in both directions. The producer gets no signal that its payload was not delivered. The model sees a truncated preview that reads as complete.

### Concrete incident

A session's environment-safety rules were delivered through a `SessionStart` hook: a 9.9 KB payload containing seven rules, among them "every machine other than this one is production; never initiate a state-changing command there without per-command human review."

The payload exceeded the threshold by roughly a hundred characters. The model received the first two rules and a file path.

Later in that session the agent was asked to change a setting that the MCP tools did not expose. Rather than reporting the limitation, it searched the filesystem, read an infrastructure repository, extracted the backing service's address, and queried that service's HTTP API directly. Its next call would have been an unreviewed write to a production host. The rule forbidding precisely this had been delivered by the hook and discarded before reaching the model.

The agent then told the user it had "never received" that rule — itself false, and only discoverable by opening the persisted file after the fact.

## Details

Observed in 2.1.270; present at least since 2.1.257 (minified identifiers differ between builds, the behaviour does not).

The hook output path:

```js
async function Qhe(e, n, r, { threshold: s = HEr, storageV5: d } = {}) {
if (e.length <= s) return e; // under the limit: passed through
let m = await dj(e, `hook-${n}-${r}`, DS(), d); // over: persisted to disk
...
return cee(m); // context receives the preview
}
```

- Threshold: `HEr = 1e4`, i.e. **10,000 characters**.
- Preview: `S2e = 2000` characters, trimmed back to the last newline.
- Call sites using it: `additionalContext`, `systemMessage`, `initialUserMessage`, and raw hook `stdout`. **None of the four passes a `threshold` argument**, so the default is always in force.
- No environment variable or settings key reaches this constant. `CLAUDE_CODE_MAX_CONTEXT_TOKENS`, `CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS`, `CLAUDE_CODE_STOP_HOOK_BLOCK_CAP` and the other hook-related variables all affect different code paths.

For comparison, the tool-result persistence path in the same build uses a default of **400,000** characters and routes through a resolver that consults a per-tool remote-config override. A single tool result may therefore occupy forty times more context than the channel that carries the session's operating instructions, and only the latter is uncapped-by-nobody and unconfigurable.

## Reproduction

1. Create a hook script that emits more than 10,000 characters of context:

```bash
cat > /tmp/big-context-hook.sh <<'SH'
#!/bin/bash
python3 -c '
import json
rules = "RULE: this line must reach the model.\n" * 300
print(json.dumps({"hookSpecificOutput":
{"hookEventName": "SessionStart", "additionalContext": rules}}))
'
SH
chmod +x /tmp/big-context-hook.sh
```

2. Register it as a `SessionStart` hook in `settings.json`.
3. Start a session and ask the model to repeat the last rule it was given.

Observed: the model's context holds a `` block with `Output too large (…). Full output saved to: …` and the first 2,000 characters. The remaining rules are absent, and nothing states that they are mandatory or that the file must be read.

Expected: all 10,000+ characters present in context.

## Suggested fixes, in order of preference

1. **Do not truncate `additionalContext`.** It is instruction input chosen by the user's own configuration, not unbounded tool output. Its size is already under the operator's control.
2. **If a cap must exist, make it configurable** through a settings key or environment variable, and default it to something comparable to the 400,000 used for tool results.
3. **Make the failure loud.** Surface it to the user in the UI, not only in telemetry. In the model's context, replace the neutral preview wrapper with an explicit statement that mandatory instructions were withheld and must be read from the named file before any other action.
4. **Document the per-invocation budget.** If the cap is applied per hook output, say so, so that producers can split deliberately across hook entries instead of discovering the limit through a silent loss.

Fixes 1 and 3 are independent: even with a generous cap, an agent that is told "your instructions were cut" behaves very differently from one that is not told.

## Environment

- Claude Code 2.1.270 (also reproduced in 2.1.257)
- Linux x86_64

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at the hook output path shown in the report, including Qhe, HEr, S2e, and the call sites for additionalContext, systemMessage, initialUserMessage, and raw hook stdout. Reproduce the behavior with the provided /tmp/big-context-hook.sh script and verify that oversized instruction output is handled according to the selected fix, with an explicit signal if content remains withheld.

Written by the indexing model from the issue text.

Assessment

Tech stack
bash, javascript, python
Domain
ai, cli, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.