Repository navigation
Conversation
Claude Code caps a hook's stdout at 10,000 characters. Longer output is saved to a file and replaced with the file path and a preview of up to 2,000 characters, and Claude is not asked to read the file. The inject-claude-md output has been over the cap since 0.52.0 in empty directories and since 0.53.0 in Vercel projects (10,830 and 10,425 characters on main), so sessions see only the start of it and lose the rest of the knowledge update, including the Integrations on Vercel section, the Next.js bundled docs pointer and, in empty directories, the greenfield guidance. hooks.json now runs inject-claude-md twice: once for the session context and greenfield guidance (570 or 976 characters) and once with --knowledge-update for the knowledge update (9,852 characters). The greenfield guidance moves ahead of the knowledge update, so the OpenCode context, which joins all parts into one string, still equals the two Claude Code outputs joined in hooks.json order. A new test runs every registered invocation and fails if any output is longer than 10,000 characters.
shamsham2026
approved these changes
Oct 15, 2026
After merging main, the --knowledge-update output alone is 10,191 characters, over Claude Code's 10,000-character cap (it compares the JS string length of each hook output), so the knowledge update was again replaced with a file path and a 2,000-character preview. inject-claude-md now builds the session context, greenfield guidance and knowledge update as one ordered list of blocks, splits the knowledge update at `##` sections (and a section that is too long on its own at its top-level list items and paragraphs, never inside a code fence), and packs the blocks greedily into parts of at most 9,000 characters. A part that continues the knowledge update repeats its title with "(continued)". hooks.json runs the hook once per part with `--part <i>/<n>` (two parts). If the content ever needs more parts than are registered, the last part prints all of the rest and the hook warns on stderr, so nothing is dropped, and the tests fail. OpenCode builds its context from the same parts, so it still equals the Claude Code outputs joined in hooks.json order. Tests run the built hook the way hooks.json does, in a Vercel project and an empty directory, and assert every output is at most 9,000 characters, every `##` heading of the knowledge update appears exactly once, and every line of the session context and knowledge update appears in order. Fixture tests cover a knowledge update that grows past one part, content that outgrows the registered parts, and a single block longer than the limit.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Claude Code caps a hook's plain stdout at 10,000 characters. Over the cap, it saves the output to a file in the session directory and replaces it with the file path and a preview of up to the first 2,000 characters. From the hooks reference, "Output size limits": "Claude Code doesn't ask Claude to read the file, so keep anything Claude must always see within the cap."
inject-claude-md.mjsprintsvercel-session.mdand the knowledge-update skill body as one SessionStart output. On main that output is 10,425 characters in a Vercel project and 10,830 in an empty directory. The preview ends at the last line break before 2,000 characters, so in every activated session Claude sees the session context and the first 1,249 of the knowledge update's 9,852 characters. Everything after the "Vercel is a full compute platform" bullet is missing from context, including:tests/inject-claude-md.test.tsdescribes as the load-bearing contract every activated session getsnode_modules/next/dist/docs/The hook runs on
startup,resume,clearandcompact, so the same happens after every compaction.Reproduction
Output size, with the hook run the way hooks.json runs it (
$PLUGINis a checkout of main or an installed 0.53.0 or later):In an empty directory (no
vercel.json) the same command prints10830.What Claude receives: I ran Claude Code 2.1.300 in print mode with this repo loaded through
--plugin-dir(with.mcp.jsonremoved so no MCP connection is attempted), an isolatedCLAUDE_CONFIG_DIR, andANTHROPIC_BASE_URLpointed at a local server that records the Messages API request. The first user message of the recorded request contains:"Integrations on Vercel", "hardcode a provider SDK" and
node_modules/next/dist/docs/do not appear anywhere in the request. In the empty-directory run, "Greenfield execution mode" does not appear either. With the same setup and the 0.50.0 hook (8,009 characters), the whole output is in the request.Root cause
The output grew past the cap as content was added; no single change is at fault. Hook output size per release, measured with the command above:
#282 added the Next.js section and took the empty-directory case over the cap, #290 took the Vercel-project case over, and #307 added another 417 characters. The existing tests check that the content is in the hook's stdout, which it is, so the size never failed a check.
Fix
hooks.jsonrunsinject-claude-md.mjsa second time with--knowledge-update. That invocation prints only the knowledge update (9,852 characters). The existing invocation prints the session context (570 characters, or 976 with the greenfield guidance). Claude Code measures each hook's stdout on its own, "even when several hooks run for the same event" (same docs section), so both arrive in full. A flag on the same script keeps the activation rules in one place; both invocations use them unchanged.buildInjectClaudeMdPartsnow puts the greenfield guidance before the knowledge update. OpenCode joins all parts into one context string, so with this order its context still equals the two Claude Code outputs joined in hooks.json order, which is whattests/opencode-plugin.test.tsasserts.hooks/inject-claude-md.mjsis rebuilt withbun run build:hooks, and CLAUDE.md lists the new registration.Trade-offs
nodeprocess at session start. It runs in parallel with the other SessionStart hooks and took 45 to 120 ms per run on this machine.additional_contextvalues from this hook instead of one. The session-start profiler already returnsadditional_contextto Cursor whenever it has a message (for example when the Vercel CLI is missing), so more than one SessionStart hook supplying it is not new. I did not test in Cursor.Not taken: trimming
skills/knowledge-update/SKILL.mdto fit one hook is a content decision for its owners, and the docs sync updates that file. Replacing the injection with a pointer to theknowledge-updateskill would change the always-on behavior the tests describe.Testing
each session-start output fits within Claude Code's hook output capruns every inject-claude-md invocation registered in hooks.json, for a Vercel project and an empty directory. It checks that each output is at most 10,000 characters and that together they contain the session context,## Integrations on Verceland, for the empty directory, the greenfield guidance. On main it fails withExpected: <= 10000/Received: 10425. On this branch it passes.tests/inject-claude-md.test.tsand the OpenCode session-context tests now run every registered invocation, since the knowledge update moved to the second one. The Cursor test also checks the second invocation's JSON.bun test: 1079 pass, 0 fail on main (d874dac); 1080 pass, 0 fail on this branch, across 60 files.bun run typecheckandbun run build:from-skills:checkpass on both, andbun run validatepasses on both (on the branch with--coverage skip, which skips the vercel.com/llms.txt fetch).bun run build:allon the branch leaves the tree unchanged.<persisted-output>block.Environment
Linux Mint 22.3 (kernel 7.0.0-38-generic), Node 20.20.2, Claude Code 2.1.300, Bun 1.4.3 for the test suite. Plugin at main d874dac (0.54.1).
Update (@molebox)
Merged
main. After the merge, the knowledge-update output was 10,191 characters, back over the cap (main added more content), and this PR's test failed. I replaced the fixed two-way split with a structural one so it keeps working as content grows:hooks.jsonruns it as--part 1/2and--part 2/2, still two processes.##headings. A section too long on its own splits between top-level bullets or paragraphs, never inside a code fence. Later parts repeat the title with "(continued)", because Claude Code runs matching hooks in parallel and may deliver outputs in any order.--partentry.hooks.jsonorder.e.length <= 1e4in the 2.1.220 binary).Sizes on the merged tree:
Tests run the real built hook as
hooks.jsoninvokes it. They check the size limit, that every##section appears exactly once, ordering, and greenfield placement, plus growth, overflow and oversized-block scenarios.CLAUDE.mdandvercel.mdare updated. Checks:typecheck,validate, andbun run test(1124 pass).Separate pre-existing bug, not fixed here: the hook exits silently when started through a symlinked path (e.g.
/var→/private/var).