Task-induced context normalization for coding agents. A native Claude Code plugin (with a Codex fallback) built on one idea: the agent should not receive the repository — it should receive a representative of the repository's equivalence class with respect to the task. The task induces a projection; everything else is built around preserving the answer, not around shrinking text. Deterministic, stdlib-only, zero API keys — and every claim below is backed by a measurement you can re-run.
- 117 tests, pure stdlib, ~7s (
python3 -m unittest discover -s tests) - Zero dependencies, zero API calls — verification runs in-session
- Measured live: −79% tokens on a real session, −96% below the file-level floor on pandas, 46× faster repeated slicing, 100% sufficiency on a 60-case fault benchmark
This is not compression. gzip makes text smaller and unreadable; normalization maps a context to a canonical, smaller member of the same answer-equivalence class. The output is still code, still readable, still sufficient — by construction where possible, by measurement everywhere else.
(The framing owes a debt to the operator-theoretic language of spectral theory — projections, kernels, decompositions. It is an analogy used with care, not a claim of isomorphism.)
Let
This is the load-bearing observation: there is no such thing as "the"
context. There are infinitely many representations equivalent under the task.
The agent does not need the document — it needs any representative of the
document's equivalence class
A task-induced projector is a map
-
idempotent:
$\pi_Q(\pi_Q(x)) = \pi_Q(x)$ — normalizing twice changes nothing; -
answer-preserving:
$A_Q(\pi_Q(x)) = A_Q(x)$ , i.e.$\pi_Q(x) \in [x]_Q$ .
The subscript is not decoration. Change the question and the kernel changes: what is invisible to "fix this KeyError" may be load-bearing for "audit the license headers". A projector without a task index is either trivial or wrong.
Everything
— the part of the context that cannot move the answer.
The kernel splits into two parts of very different nature:
-
Syntactic kernel — invisible to every task: ANSI escapes, progress-bar
spam, consecutive duplicate lines, a file re-read that is byte-identical to
the copy already in context. Because
$\ker_{\mathrm{syn}}$ does not depend on$Q$ , it can be projected away ambiently, by a hook that never needs to know what you are working on. That is exactly what$T_1$ is. -
Semantic kernel — invisible to this task: the 1,404 pandas files that a
mergeKeyError cannot see. This is the hard part; it requires knowing$Q$ . That is what$T_2$ –$T_4$ are for.
This dichotomy is why the architecture has the shape it has: task-independent normalization runs everywhere and always; task-induced normalization runs when there is a symptom to induce it.
Define the task state:
the canonical small representative of
In the continuous (embedding-based) picture,
The discrete, exactly-computable picture replaces embeddings with reachability on the dependency graph — which is what the shipped operators use, because for code, structure beats similarity (we measured it; see §6).
When is
$\pi_2 \circ \pi_1$ is answer-preserving iff the premises of$\pi_2$ hold on$\mathrm{Im}(\pi_1)$ .
Order matters through the premises, not through the algebra. Concretely: run
and projectors on different tensor factors commute. Run a
Two structural facts worth stating plainly:
- Answer-preserving projectors (with compatible premises) form a monoid under composition — closed, associative, with identity.
- They do not form a group. There is no
$\pi_Q^{-1}$ : the inverse of a projection is not an operator — it is an access path. That is the page fault (§1.6).
Perfect answer-preservation cannot be guaranteed by static analysis alone (dynamic imports, dependency injection, config indirection). So the guarantee this plugin actually makes is deliberately weaker and checkable:
Every exclusion is a prior, not a prohibition. The manifest declares what was projected away; if the agent needs an excluded piece, recovering it costs one read — a page fault, in the OS sense. The interesting quantity is then not "was the projection perfect?" but "what did the faults cost?" — and that is logged, per run.
Every normalization trades rate (fraction of tokens removed) against distortion (probability the answer moved out of its class). Both are measured:
- rate — from the manifests and the savings log;
- distortion — via an objective oracle: take real fault sites in a repo
(actual
raisestatements), synthesize the partial symptom a user would report (the caller's frame plus the error message, not the raise site itself), and check whether the projection keeps the raise site in the task state. If the file that throws is projected away, the answer changes with certainty.
The curve lives in ~/.context-kernel-pipeline.jsonl, one JSON row per run.
| Operator | Kernel it targets | What it does | Guarantee | |
|---|---|---|---|---|
normalize (impl. compress.py) |
syntactic | Signal-preserving normalization of tool outputs (dedup, ANSI/progress strip, head+signal+tail elision). Plus re-read deltas: an unchanged re-read collapses to a 3-line marker; a changed file arrives as a unified diff against the copy already in context. | Signal lines (errors/warnings) always survive; every elision leaves a visible marker; a canary verifies each replacement was actually applied (§4) | |
| repo slice | semantic | Projects the repository onto the working set induced by the symptom: seeds from stack frames / quoted literals, dependency closure, bounded importers, related tests. Token budget (auto-derived from the live context window) selects the richest closure that fits; on monolithic repos it descends to symbol level ( |
Sound on the static import graph; blind spots are declared exclusions + page faults; results cached by repo fingerprint and operator hash | |
| task charter | semantic | Extracts the constraints the fix must respect — contracts, invariants, behaviors pinned by tests — each with a mandatory file:line citation, ≤ ~10 items. |
Every claim is citable; a stale citation is detectable | |
| verifier | — (checks, does not project) | Adversarial check of the fix against the charter, constraint by constraint; or answer-invariance judgment |
Reads ground truth via sed/awk, never through its own (normalized) Read tool |
flowchart LR
C[Repository + tool outputs] --> T1["T1 · syntactic normalize\n(hooks, ambient, task-free)"]
T1 --> T2["T2 · repo slice\n(task-induced, graph + budget)"]
T2 --> T2b["T2b · symbol slice\n(only if budget\nunsatisfiable)"]
T2 --> T3["T3 · task charter\n(constraints, file:line)"]
T2b --> T3
T3 --> FIX["fix / answer"]
FIX --> T4["T4 · adversarial verify"]
T4 -->|"page fault: one read"| C
Determinism is not uniform across the pipeline, and it should not be hidden. Guarantee classes: formal (holds by construction), supervised heuristic (heuristic, but every application is checked by an instrument), probabilistic, auditable (LLM-produced, but every claim carries a citation that can be verified deterministically), empirical (LLM judgment).
| Operator | Time | Space | Token effect (measured) | Guarantee class |
|---|---|---|---|---|
|
|
|
−45…−93% per output | supervised heuristic (canary) | |
|
|
|
state ≤ ~2 MB | −98% on unchanged re-reads | formal (hash equality) |
|
|
|
−75…−97% of repo | formal on the static graph; premises declared | |
|
|
|
20 entries | — | formal (fingerprint + operator hash) |
|
|
−96% below file-level floor | formal w.r.t. "behavior of symbol S" | ||
| budget resolution |
|
picks the point on the curve | formal (arithmetic on measured state) | |
|
|
1 LLM pass | — | ~10 constraints replace the diff context | probabilistic, auditable (file:line) |
|
|
1–2 LLM passes | — | — | empirical, adversarial |
All numbers below are from real runs (July 2026, Claude Code 2.1.x), reproducible with the commands shown.
| Case | Before | After | Rate |
|---|---|---|---|
pip3 list, 1053 lines |
14,900 tok | 995 tok | −93% |
| One live session (Bash+Read+WebFetch) | 66,832 tok | 14,212 tok | −79% |
| Unchanged file re-read (delta) | ~2,000 tok | ~40 tok | −98% |
bench/sufficiency_bench.py on pandas (1,415 source files, 60 real raise
sites, partial symptoms — caller frame + message only):
xychart-beta
title "pandas, 60 fault sites: token rate removed (bars) vs sufficiency (line)"
x-axis ["full closure", "deps<=2", "deps<=1 + importers<=1"]
y-axis "percent" 0 --> 100
bar [75, 86, 91]
line [100, 100, 100]
Sufficiency stays at 100% at every depth. The measured lesson: distortion is dominated by seed quality, not closure depth — cutting depth is nearly free. (The benchmark also earned its keep: its first run scored 85% and the misses exposed a real seeding bug — ambiguous in-root absolute paths — which is now fixed and regression-tested.)
On lodash (JavaScript, 1,048 files, a real node stack trace): task state
30/1048 files (−97%) with all four trace frames retained at every depth.
Measuring the budget in tokens (not files) exposed a structural wall:
| Level | Task state for a real pandas KeyError
|
Cost |
|---|---|---|
| file-level minimum (11 files) |
frame.py, generic.py, … |
~372k tok — unsatisfiable |
|
|
DataFrame.merge (32 lines), NDFrame._get_label_or_level_values (59 lines), _MergeOperation.__init__, _get_merge_keys, def-use slice of merge
|
~15.4k tok (−96%) |
Class-enclosed frames become exact method line-ranges (sed -n 'a,bp');
top-level functions become backward def-use slices. The manifest ships the
extraction commands ready to run.
The budget needs no human input. The PreToolUse rule
injects --budget auto into any slicer invocation that lacks one:
budget: auto: session 6e2e49dc, model claude-fable-5,
window ~467k, in use ~362k, headroom ~104k -> budget 41k
| pandas (1,415 files) | |
|---|---|
| cold run | 12.0 s |
| identical re-run | 0.26 s (46×) |
Every manifest is stamped operatore: T2@<hash> — change the script and the
cache invalidates itself, and telemetry knows which operator version produced
each data point.
A savings log proves the hook computed a replacement — not that the harness applied it. The canary closes that gap: each replacement records its exact footer (with the numbers); on the next invocation the hook checks the session transcript for that footer. Present → verified. Absent → alarm, with the model told in-band that savings are being overstated.
This is not theoretical. The canary's first real alarm led to three real
bugs in one night: the Read tool's nested response shape had silently never
been normalized; canary verification could false-positive on content that merely
quoted a footer; stderr-only output was destroyed by the replacement path.
All three are fixed and regression-tested — and the fix was itself produced by
running this plugin's own pipeline (
-
Deterministic wherever possible.
$T_1$ /$T_2$ are pure functions of their inputs. No learned behavior in the projection path — learning would trade auditability for adaptivity, and the certificate would die. -
Structure beats similarity for code. Graph reachability + real seeds gave
100% sufficiency; embedding scores are kept only as a research projector for
prose (
span_rd.py, repo root). - Honest failure. An unsatisfiable budget says so. Unknown tool-response shapes are logged (keys only) by a shape sentinel. Unknown context windows are estimated conservatively and printed.
-
Mechanisms, not conventions. Judge agents' Reads are exempted from
normalization via the hook payload's
agent_type(discovered empirically with the built-in payload tap), not via a polite instruction. - Everything is measured, and the instruments get tested too. The benchmark found bugs in the slicer; the canary found bugs in the harness contract. Instruments that never fire are decoration.
From GitHub (once published):
/plugin marketplace add pinperepette/context-kernel
/plugin install context-kernel
From a local checkout (the repo root is a marketplace):
/plugin marketplace add /path/to/repo
/plugin install context-kernel@context-kernel-marketplace
The plugin registers by itself: the context-kernel:kernel-slice, …:kernel-repo-slice, …:kernel-invariants,
…:kernel-verify, …:kernel-pipeline), 3 agents (kernel-scout,
kernel-extractor, kernel-verifier) and the MCP server (kernel_slice,
kernel_repo_slice). Manage with /plugin; verify with /hooks and
python3 hooks/savings.py.
bash install.sh # idempotent; writes ~/.claude/settings.json (with backup)Do not use both routes at once — hooks would stack (a guard prevents double
normalization, but it is waste). Codex glue lives in codex/config.toml.
cd claude-context-kernel
python3 -m unittest discover -s tests # 117 tests, ~7s, stdlib onlyTests exercise the real contract (JSON on stdin → JSON on stdout, via subprocess), because that is where the bugs lived:
| File | Covers |
|---|---|
test_compress.py |
dict/nested/string replacement shapes, stderr-only, signal preservation, no-op safety, shape sentinel, judge-agent exemption, re-read deltas, double-run guard, session attribution |
test_canary.py |
exact-footer verification, quoted-footer false positives, elision-marker false positives, legacy fallback, subagent pendings, TTL, --reset-canary
|
test_repo_slice.py |
seeds from traceback/literals/suffix/relativization, ambiguity refusal, package-root imports, test↔source heuristic edges, budget ladder, |
test_bench.py |
the sufficiency oracle itself (a fixture repo where the answer is known) |
test_pretool_rewrite.py |
quiet-flag rules, --budget auto injection, segment-aware insertion (pipes, fd redirects) |
test_savings.py, test_slice.py, test_mcp_server.py
|
report parsing (5/6-field CSV), AST slicer semantics (executed, not eyeballed), MCP JSON-RPC contract |
| Variable | Default | Meaning |
|---|---|---|
CK_MIN_TOKENS |
800 |
below this, |
CK_TOOLS |
Bash,Grep,Read,Glob,WebFetch |
tools |
CK_AGENT_SKIP |
kernel-verifier,kernel-extractor,kernel-scout |
agent types whose Reads are never altered |
CK_DELTA / CK_DELTA_MIN
|
1 / 200
|
re-read deltas on/off, minimum size |
CK_CANARY |
1 |
end-to-end application check |
CK_SLICE_CACHE |
1 |
|
CK_CONTEXT_WINDOW / CK_BUDGET_MAX
|
auto / 80000
|
window override, budget cap |
CK_PRETOOL |
1 |
command rewriting (quiet flags, budget injection) |
CK_TAP_FLAG |
~/.context-kernel-tap |
touch it to log hook payload keys (never content) |
CK_LOG_OFF |
– | set 1 to disable all logging |
Reports: python3 hooks/savings.py (per-tool and per-session savings, canary
status), python3 hooks/savings.py --reset-canary (acknowledge investigated
failures). Curve data: ~/.context-kernel-pipeline.jsonl.
- The formal slice (
$T_{2b}$ def-use,kernel_slice) is answer-preserving by construction with respect to "what does symbol S do" — unreachable top-level units cannot change S's behavior. - The repo slice is sound on the static import graph. Dynamic imports, DI containers, config indirection are invisible to it: that is exactly what the page-fault model and the declared exclusions are for.
-
$T_1$ normalization is heuristic but supervised: signal lines always survive, markers make every elision visible, and the canary checks that what you think entered the context actually did. - The harness contract (
updatedToolOutputand friends) is undocumented and can change under you. This plugin cannot prevent that — it can only notice immediately (canary) and tell you. That is a defense, not a guarantee. - Subscription quota (5-hour windows) is not exposed by Claude Code in any readable form. The resource that is readable — and the one that actually constrains a task state — is the context window, and that is what the budget uses.
MIT