●RELEASE — Claude Code v2.1.246 shipped on August 26, adding a startup warning for Bash allow rules that put a wildcard ahead of the subcommand●PERMISSIONS — /permissions now has an Auto mode tab, so you can see in one place what runs automatically and where Claude still stops to ask●MEMORY — Unbounded memory growth in long interactive sessions is fixed: subagent tool results are released once they scroll out of the recent display window●MCP — In headless and remote sessions, a tool call interrupted by an incoming message is now reported as an explicit interrupted error instead of completing with no output●RUNNER — claude self-hosted-runner gains --proxy-authorization-command and --proxy-authorization-file for egress proxies that issue a fresh auth header on every connection●LIMITS — The 50% weekly limit increase runs through August 31 for Pro, Max, Team, and seat-billed Enterprise accounts, which leaves four days●RELEASE — Claude Code v2.1.246 shipped on August 26, adding a startup warning for Bash allow rules that put a wildcard ahead of the subcommand●PERMISSIONS — /permissions now has an Auto mode tab, so you can see in one place what runs automatically and where Claude still stops to ask●MEMORY — Unbounded memory growth in long interactive sessions is fixed: subagent tool results are released once they scroll out of the recent display window●MCP — In headless and remote sessions, a tool call interrupted by an incoming message is now reported as an explicit interrupted error instead of completing with no output●RUNNER — claude self-hosted-runner gains --proxy-authorization-command and --proxy-authorization-file for egress proxies that issue a fresh auth header on every connection●LIMITS — The 50% weekly limit increase runs through August 31 for Pro, Max, Team, and seat-billed Enterprise accounts, which leaves four days
Don't let your verification script's full output flow back through a hook
The check was working the whole time. The conversation was what ran out of room. Measured side by side: one scan of 833 files returns 58 bytes, another returns 42KB. Here is how to put an output budget on your hooks.
The check ran correctly to the very end. The conversation was the thing that broke.
I had added a hook that ran a static check after every edit. Working alone as an indie developer across several repositories, I have found that checks tied to the edit itself hold up far better than checks I have to remember to run. The check worked exactly as intended and caught the violations it was supposed to catch. But somewhere after a few dozen edits, responses got noticeably sluggish, and eventually long prompts stopped going through at all.
I spent a while staring at the check script looking for the problem. Wasteful loops. Slow regular expressions. But it was never about processing time. The strings the check was returning had been piling up inside the conversation, one edit at a time.
What lands in context is what you returned, not what you scanned
Here are numbers I measured on my own machine. Against the same repository (833 MDX files), I ran several checks with different personalities and recorded the bytes and lines each one wrote to standard output.
Process
Scope scanned
Output bytes
Lines
Approx. tokens
Frontmatter integrity check (pass)
833 files
58 B
1
~26
Verbatim-duplication scan (pass)
833 files
109 B
1
~50
Redirect integrity check (pass)
whole repo
126 B
1
~57
Single-file check (violations found)
2 files
893 B
18
~406
Enumerating grep
833 files
42,280 B
398
~19,218
Dumping the files themselves
1 directory
5,748,215 B
—
~2.61M
Token figures are rough, based on roughly 2.2 bytes per token for mixed Japanese and English text. Your tokenizer will give different numbers.
The gap between the top three rows and the bottom two is the part worth sitting with. The same 833 files, scanned by both — one returns 58 bytes, the other 42,280. Roughly 728 times more. The size of what you scan does not determine the size of what you return. The only thing that determines it is what the author decided to print.
Until I built that table, I had been quietly assuming that heavier checks produce heavier output. The opposite tends to be true. A well-designed full-repository check returns one line when everything passes. A hastily written grep -rn prints every matching line it finds. The second one is far cheaper to run and three orders of magnitude more expensive to keep.
That last row, 5.7MB, is there as a ceiling. Return that and it will not fit in a million-token window, let alone a working conversation. "A hook whose output stalls the session" is not an abstract hazard — it is one misplaced cat away.
The best thing a passing check can say is almost nothing
Depending on the hook type, whatever your script returns can be carried into the next turn's context. That is the part that makes hooks different from ordinary shell work. In a terminal, a long dump scrolls past and is gone. Hook output does not scroll away. It stays, accumulates, and becomes the running cost of everything that comes after.
Which makes the design rule fairly blunt:
When the check passes, return the fact that it passed and nothing else
When it fails, return the smallest thing that lets someone fix it
Full logs, complete listings, and raw traces go to a file, not to the conversation
The top three rows of that table are exactly this shape. Inspect 833 files, print "clean," stop. Nothing about listing which files passed would change anyone's next move.
Failure is a different negotiation. That 893-byte, 18-line output contains the violated rule names and where they occurred. For something you are about to fix on the spot, that trade is fair.
Why "show everything, just in case" backfires
More information feels safer. For logs a human reads, that instinct is usually right.
But when the reader has a finite context window, the total volume of available evidence has a ceiling. Fill the ceiling and there is nowhere left for the information that actually matters later. Worse, most of what filled it was the names of files that turned out to be fine.
Information that does not change a decision becomes pure debt the moment you include it. That holds well beyond checks — it is a reasonable default for anything a hook hands back.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to estimate how many bytes a verification script sends back into the conversation before you wire it into a hook
✦You will know how to split success output from failure output, so a passing check stops quietly eating your context window
✦You will understand why a scan across 833 files can still return a single line, and be able to set an output budget for your own hooks
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Decide the output budget before you write the body
In practice, the reliable move was to fix a byte budget per hook up front. I first tried to manage this by feel and it did not hold, so it became a number.
Rough values I use now:
Hook character
Budget on success
Budget on failure
Runs on every edit (high frequency)
0–120 B
1,500 B
Runs at checkpoints, e.g. pre-commit
0–200 B
4,000 B
Runs once at session start
up to 2,000 B
4,000 B
The one rule that matters is that higher-frequency hooks get tighter success budgets. Adjust the specific numbers to your workflow. If something that runs on every edit returns 100 bytes, a hundred edits cost you ten thousand bytes — small enough that you will never be surprised by it.
Put a wrapper in front that enforces the budget
Deciding on a policy is not enough, because check scripts drift toward being chatty. Swap a tool, or watch --verbose quietly become the default, and your assumption breaks without a word. So the budget is enforced by a wrapper, not by the good intentions of the script.
This is the shape I use, rewritten to be generic. Call any check through it and the returned volume stays inside the budget.
#!/usr/bin/env bash# hook-guard.sh — keep a check command's output inside a byte budget# usage: hook-guard.sh <budget_bytes> <evidence_dir> -- <check command...>set -uo pipefailBUDGET="$1"; shiftEVIDENCE_DIR="$1"; shift[ "${1:-}" = "--" ] && shiftmkdir -p "$EVIDENCE_DIR"STAMP="$(date +%Y%m%d-%H%M%S)-$$"EVIDENCE="${EVIDENCE_DIR}/${STAMP}.log"# Send stdout and stderr to the evidence file; nothing reaches the conversation yet"$@" > "$EVIDENCE" 2>&1STATUS=$?TOTAL_BYTES=$(wc -c < "$EVIDENCE" | tr -d ' ')TOTAL_LINES=$(wc -l < "$EVIDENCE" | tr -d ' ')if [ "$STATUS" -eq 0 ]; then # On success, state the fact and nothing more — not even the evidence path echo "check ok (${TOTAL_LINES} lines suppressed)" exit 0fi# Only on failure do we spend the budget on actual contenthead -c "$BUDGET" "$EVIDENCE"if [ "$TOTAL_BYTES" -gt "$BUDGET" ]; then printf '\n--- truncated: %s of %s bytes shown, %s lines total ---\n' \ "$BUDGET" "$TOTAL_BYTES" "$TOTAL_LINES" printf 'full log: %s\n' "$EVIDENCE"fiexit "$STATUS"
The call site looks like this:
# command invoked from a hook entry in settings.json~/bin/hook-guard.sh 1500 ~/.cache/hook-evidence -- \ python3 tools/frontmatter_check.py "$CLAUDE_FILE_PATH"
Why it is written this way
Three details, each one learned the hard way.
Output goes to a file before any decision is made. Truncating through a pipe kills the writing process with SIGPIPE, and the exit status stops matching reality — a passing check reported as failed, or the reverse. That confusion cost me real time, so the wrapper takes the plain route and writes to disk once.
On success, the evidence path is not printed. It used to be. It felt helpful. But on a high-frequency hook, that one line of path accumulates on every single run, and since the check passed, nobody ever opens the file. Returning something you know will never be read is just a subscription to debt.
When output is truncated, the totals always come along. A bare head -c leaves the reader unable to tell whether they are looking at everything or at a fragment — and a fragment mistaken for the whole leads somewhere wrong. "1,500 of 42,280 bytes shown, 398 lines total" removes the ambiguity entirely. Truncating is fine. Truncating silently is not.
What to keep and what to drop, in order
When the budget is exceeded, what goes first? Deciding this in advance keeps the implementation from wandering. My order:
Total violation count — one versus two hundred changes the entire response
The first three to five representative violations — enough to see the shape of the fix
Breakdown by violation type — distinguishes "one cause, 200 times" from "200 different problems"
Path to the evidence file — the door back to the full listing
Everything else — not returned
Items 1 and 2 usually settle the next move on their own. Item 3 earns its place when the count is high and you want a single sweeping fix.
Put the other way around: a hook that returns item 5 is almost certainly wasting its budget. If 200 violations share one cause, 199 of them say nothing the first one did not already say.
Exit codes and output volume are separate design axes
One more distinction worth making explicit. Exit status and output size are independent, and all four combinations have legitimate uses.
Short output
Long output
Success (exit 0)
A passing check. Make this the default
Suspicious. It passed and still spent your context
Failure (non-zero)
A failure you can state in one line. Ideal
Fine within budget. Over budget, summarize and offload
The quadrant people miss is the top right: passing, and long. Nothing failed, so nobody investigates. No error appears anywhere. And it removes a slice of the context window on every run, reliably. When things get slow, the hook to suspect first is not the one that failed — it is the one that has been quietly succeeding.
I lost real time reading failure logs for exactly this reason. The culprit had never failed once.
Measure your own hooks for one week
Before changing any design, it helps enormously to turn the present state into numbers. Here is the smallest instrument that does it.
Read the total column, not the average. Two hundred bytes per run becomes sixty thousand if it fires three hundred times a day. Two thousand bytes once per session is not worth a thought. Frequency multiplied by volume is what your conversation is actually paying.
In my case the top of that list was the check I had been calling "the light one." I had been looking only at the per-run number and feeling reassured by it.
One next step
Just one thing is enough. Take the hook that fires most often, and cut its success output down to a single line.
The question that decides what can go: does this line change the next action anyone takes? If it does not, it does not belong in the conversation.
I have spent a lot of time making the net finer. But a fine net attached to a verbose report ends up slowing the very judgment it was meant to sharpen. The checks I trust most now are the ones that pass in silence.
Thank you for reading. If you measure your hooks and the ranking surprises you, that surprise is where to start.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.