Terminal Tool Output Compression: Filtering Predictable Noise at the Harness¶
Strip predictable-shape noise from terminal output at the harness boundary so context holds signal, not lockfile churn.
The Pattern¶
Long shell output decomposes into two populations. Predictable noise — npm install progress, lockfile diffs, ls -l columns, unchanged hunks inside git diff — has near-zero value per token but consumes context budget at the same rate as signal, competing with the failing test or changed function.
Terminal output compression is a post-processing filter at the harness boundary: it drops noise, leaves signal intact, and prepends a banner so the model can opt out per call.
Reference Implementation: VS Code 1.120¶
VS Code 1.120 ships this as chat.tools.compressOutput.enabled for the agent chat tool (Preview), per the release notes. The filter set:
| Tool output | Compression |
|---|---|
git diff and similar |
Large unchanged hunks collapsed |
| Lockfile and snapshot diffs | Dropped entirely |
ls -l |
Reduced to entry names |
npm install |
Progress bars, deprecation warnings, audit summaries stripped |
The release notes specify the audit contract directly: "A short banner is prepended to any compressed output, so the model can see which filters fired and how to disable compression if it needs the raw text." The banner is non-optional — without it, the pattern degrades into silent error masking.
Where the Lever Lives¶
Compression is a harness-layer concern. The same primitive recurs across assistants under different names:
| Harness | Mechanism |
|---|---|
| VS Code agent chat | chat.tools.compressOutput.enabled setting |
| Claude Code | PostToolUse hook returning hookSpecificOutput.updatedToolOutput |
| MCP server-side | _meta["anthropic/maxResultSizeChars"] (Claude Code only) |
The shape is identical: the harness reads raw tool_output, applies filters, and returns the compressed string; the original stays in the transcript for audit (PostToolUse Output Replacement).
graph LR
A[Tool runs] --> B[Raw output]
B --> C{Harness filter}
C -->|Noise population| D[Drop / collapse]
C -->|Signal population| E[Keep verbatim]
D --> F[Banner + compressed output]
E --> F
F --> G[Model context]
B -.->|Always| H[Transcript / audit log]
Noise-Dominated vs Signal-Dominated Output¶
The contract only works when the filter set is calibrated: false positives drop the byte that mattered, false negatives leave the noise in.
Noise-dominated (safe to compress by default):
- Lockfiles (
package-lock.json,yarn.lock,Cargo.lock), snapshots, bundles - Package manager progress: download bars, percent indicators, "added N packages"
- Directory metadata:
ls -lcolumns,find -ls - Repeated structure: identical error lines across files in a multi-file lint
Signal-dominated (never compress without explicit reason):
- Test output (the failing assertion is often one line in a block)
- Compiler diagnostics with column numbers and fixes
- Error traces with file paths and line numbers
git diffof source files — collapse unchanged hunks only, never changed ones- Anything with a URL, token, ID, or path the model may cite
The Banner Contract¶
The banner is the auditability hinge. Without it, the model cannot know compression happened, and the developer reading the transcript sees output the model never saw. A minimum banner records which filters fired (lockfile-dropped, unchanged-hunks-collapsed), how to disable compression next call, and original vs compressed size.
Relationship to Adjacent Patterns¶
Six levers solve adjacent problems and compose; none substitutes for another:
| Page | Layer | Trigger |
|---|---|---|
| Terminal output compression (this page) | Harness, per-call | Tool returns predictable noise |
| PostToolUse Output Replacement | Harness, post-hoc | Rewrite result before model |
| Graceful Tool Output Truncation | Tool author, runtime | PARTIAL with continuation handle |
| Semantic Tool Output | Tool author, design | Less noise emitted at source |
| Observation Masking | History, post-hoc | Tool result used — drop it |
| Context Compression Strategies | History, threshold | Budget threshold — offload, summarise |
When This Backfires¶
- Over-compression hides the failing byte. Stripping
npm installaudit summaries also strips the advisory ID the model needs. The banner names the filter but not the dropped content, so the model must rerun with compression disabled. - False-positive pattern matches. A test fixture starting with
+++markers gets treated as a diff; a rarely-changingREADME.mdmay match the "lockfile-shape" heuristic and be dropped. - Interactive debugging. Compressing "predictable progress" while narrowing a flaky test can mask the timing variance that explains it.
- Compression masks agent learning. An agent that only sees compressed
npm installoutput never learns its full shape — the same tax observation masking levies.
The fix in every case is the banner plus the opt-out; without those, compression degrades into silent error masking.
Example¶
A Claude Code PostToolUse hook compresses git diff output: it collapses unchanged hunks, leaves changed hunks verbatim, and prepends a banner.
.claude/hooks/compress-git-diff.sh:
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Only act on git diff invocations
if [ "$TOOL" != "Bash" ] || ! echo "$CMD" | grep -qE '^git diff'; then
exit 0
fi
OUTPUT=$(echo "$INPUT" | jq -r '.tool_output // empty')
ORIG_SIZE=${#OUTPUT}
# Collapse unchanged hunks (lines starting with space) into a count summary;
# leave +/- lines and file headers intact.
COMPRESSED=$(echo "$OUTPUT" | awk '
/^[+-]/ || /^@@/ || /^diff / || /^index / || /^--- / || /^\+\+\+ / { print; unchanged=0; next }
/^ / { unchanged++; if (unchanged == 1) print " [... unchanged context follows ...]"; next }
{ print }
')
NEW_SIZE=${#COMPRESSED}
if [ "$NEW_SIZE" -ge "$ORIG_SIZE" ]; then
exit 0 # No win — pass through.
fi
BANNER=$(printf "[compress-git-diff: collapsed unchanged hunks, %d -> %d bytes. To disable: rerun with --no-compress in the command.]\n\n" "$ORIG_SIZE" "$NEW_SIZE")
jq -n --arg out "${BANNER}${COMPRESSED}" \
'{hookSpecificOutput: {hookEventName: "PostToolUse", updatedToolOutput: $out}}'
The model sees the banner first, knows compression fired, and can request raw output by adding --no-compress to a follow-up git diff call (which the hook detects and skips). The full diff remains in the transcript regardless.
Key Takeaways¶
- Terminal output compression strips predictable noise (lockfile diffs, package-manager progress,
ls -lmetadata, unchanged diff hunks) at the harness boundary before the model sees it. - The lever lives at the harness, not the tool — VS Code ships it as
chat.tools.compressOutput.enabledin 1.120 (Preview); Claude Code implements the same shape viaPostToolUseupdatedToolOutput. - The banner is non-optional. Without a record of which filters fired and how to disable them, compression becomes silent error masking.
- Compress the noise-dominated set (lockfiles, progress bars,
ls -ldirectory metadata, unchanged hunks). Never compress the signal-dominated set (test failures, error traces, changed code, anything carrying an ID the model may need to cite). - Compression composes with — does not replace — semantic tool output, observation masking, and threshold-triggered context compression.