Yurii SerhovskyiWriting

A harness grown from losses

I almost never see the auto-compact warning in Claude Code. That is not a setting. I dislike what compaction does — the conversation is replaced by a lossy summary, and the nested instruction files do not survive it — so the workflow was built so that almost no session reaches half of its context window. The “half” is an observation, not a measurement: nothing I keep records how large a session’s window got. What is recorded is everything around the sessions.

A harness is everything around the model that is not the model. The model generates text and calls tools. The harness decides what the model sees before it has done anything, what it is allowed to do, how it learns that it was wrong, and when it stops. The claim behind “harness engineering” is that the environment, more than the prompt, decides what the agent produces.

Mine has five layers, each with a file in the repository of Pantensa, the household pantry and nutrition tracker I build alone, with an agent doing most of the typing: context, permissions, feedback, orchestration, and tests of the harness itself. The first three get a paragraph each. The last two are the post.

The dates matter, because the harness grew during use. The first five hooks landed on 14 August 2026, in a commit that says they “refuse five losses that already happened here”. The wave of work measured below started two days later, just after midnight. That afternoon, mid-wave, a second commit turned five more lessons “that had the shape of a command” into rules, and four minutes after it came the test that checks the rules are wired. The wave closed on 17 August. Every number here was measured while the harness was still growing out of what the wave was losing.

Context: measure the position, never inherit it

A session starts by stating the tree’s actual position — branch, HEAD, tracked and untracked files, divergence from main — and it measures all of that itself, because a handoff note written by the previous session is already stale by the time it is read. A SessionStart hook, session_state.py, does that. The same file holds a budget for the memory index: 6144 bytes, because that file is loaded into every session and “a line kept there is rent paid per turn”. Context is debt with interest. Every turn re-reads the whole history — cached, at roughly a tenth of the base input price by the API docs as of August 2026, but re-read until the session ends. A token that settles into a long-lived window is paid for dozens of times. A token burnt in a window that is thrown away is paid for once.

Permissions: two verdicts, and the line between them is mine

guard_bash.py runs before every shell command. Its docstring: “Each rule encodes a loss that has already happened in this repo, not a style preference.” It knows two verdicts. A refusal is only for irreversible loss — fourteen of them, and commit, merge, rebase and branch switching are not on the list, because prompting on routine work was ruled out as friction. One of the fourteen reads: git checkout -- already erased an uncommitted fix in this repo once. A warning is for a command that “will answer truthfully and mean something else”: a count blind to untracked files, a viewport that is not 390 px, a gate that runs nowhere. “Blindness is not loss, so it never blocks”; the warning arrives beside the command that caused it, which is the only moment it is useful.

Feedback: the finding arrives at the command

Piping a test run into tail -5 is how the controller keeps thousands of lines of runner output out of its window — and the pipe reports the pager’s exit code, so pytest | tail -5 exits 0 on a red suite. That has already been read as a pass here, by an agent in a session that was mine to check, and I did not. warn_pipefail.py now fires on the command that caused it, “not in review”. A trick that saved context had a known way to lie, and it was closed with a hook rather than a reminder in the skill.

Orchestration: where each token burns

Long work runs in waves. A wave is a plan split into links, one fresh session per link, and the registry of the August wave opens with the rule: “Memory between links is the ledger and git log, never the dialogue.” Three skills carry it — one audits the plans and writes the link prompts, one is the controller of a single link, one closes a link from the ledger alone — and eight reviewer roles sit under .claude/agents/.

The controller’s skill states its principle in one line: “your context is the scarce resource, and the ledger is your memory.” The rules follow from it. A task brief is passed as a path, never pasted. The history of previous tasks is never pasted — a real session reached a 42k-character prompt that was 99% pasted history. The diff reaches the reviewer as a file. Reports are files, and the controller receives four lines. Suite runs go to a log in the background, and only the summary lines and the exit code come back. A subagent’s transcript is never read. After a compaction, trust the ledger and git log, not recollection.

The controller never opens the plan. The six plans of the August wave are 143 KB. The controller read them zero times; the scripts take the path. The skill’s reason: a controller that opens the plan “has started paying, in the one budget the link cannot refill, for information two subagents already have.”

A file goes in, four lines come out. Each implementer writes its report to disk and returns status, commits, test numbers, caveats. The reports for the wave’s 23 tasks measured 353 KB on 17 August 2026 — the artefacts have since been deleted, so that number stands on my notes. What reached the controller was 23 times four lines.

Then the number I keep coming back to. In one link, a single test file dumped 74 MB of stderr in one run. Into the context went tail -5: five lines. The reviewer that audits gates carries the counter-rule to that trick — “three numbers or none: Tests, Test Files, failed”, because a failing file does not change the total, so “N passed” reads green over a red gate.

The heaviest artefacts of the wave are the ones I can no longer cite — the review diffs, the task briefs, the per-task reports. They lived in an ignored directory, each was read by one window, and they were gone once the verdict was in, because after the verdict nobody needs them. That is the design. The expensive reading happens in windows that are discarded; the window that lives gets decisions and measurements.

The same idea as a table:

the fact is needed by it lives in what it costs
every session CLAUDE.md re-injected every request, survives compaction
some sessions, rarely memory index + files, 6 KB budget one line in context; the body on demand
one link ledger + registry a file, read at specific points
one task brief + report files of a throwaway window
nobody, once decided the subagent’s window discarded with the window

What the same artefacts say about quality. The wave forecast roughly eight or nine new test cases over 23 tasks. It landed 22: 1910 to 1932. The registry names the divergence. One link forecast as a pure refactor closed at plus two, because two independent reviews found that the one region actually rewritten had nothing proving it, and I authorised a witness; another closed at plus three against plus one for the same reason. The overrun was review catching empty proofs, not scope creep.

Gross tokens across all windows are larger than a single long session would burn; the review fan-out alone guarantees that, and on a subscription “tokens” means how fast the usage limit drains. What falls is the compounded cost in the window that lives. What rises is the chance the work survives a dead session: the ledger is written after every task, so any window can die at the cost of one repeated task. Anthropic’s own docs recommend most of this — clear between tasks, compact at pauses, subagents for exploration, files as external memory — as session discipline. The harness makes it something a session cannot skip: the controller has no plan to read because the skill lists the files it opens, and nothing else.

The rule without a witness

Everything above is enforcement, and enforcement is code. The first post on this site mentions two i18n lint rules that had died silently: a misconfiguration disabled them and lint stayed green. The fifth layer exists because the hooks can die the same way.

test_claude_hooks_are_armed.py opens with the reason: “A rule nobody registered never fires. A rule nobody witnesses stops firing without anyone noticing — the shape the i18n lint died in.” It asserts that every hook is registered on the event and matcher it is useless without; that every rule id carries exactly one verdict; that the warning channel cannot answer a permission prompt; and that every rule has a behaviour witness in the neighbouring test file, one that goes red if the rule is removed. The witnesses have names like “the fourteen denials still bite” and “the hook that cannot measure says nothing at all”. The test parses the hooks with ast rather than grepping them, because “the reasons these rules carry quote the very commands they refuse, so a grep for decide reads prose as code.”

The other half of the layer is a reviewer that reads no code. The gate-evidence auditor gets the ledger and the reports and answers one question: “Did it go red — and did it go red because of what WE did?” Three verdicts per gate, OURS, NOT OURS, UNPROVEN, and “probably fine” is UNPROVEN. If proving a gate means moving the tree, it does not; it names the command and hands it back, because an unproven verdict with a recipe beats a fabricated one. It carries a catalogue of twelve ways a gate stays silent, and “every one of these has shipped in this repo”: a pinned count that had to be lowered in the same commit; a not.toContain(file) that becomes true the moment the file is deleted, and asserts nothing; a gate that passed because of how it was run — and nine more.

Instructions are context too

The skills themselves were tested the same way, the day before the first hooks. One fix for a controller that kept rationalising its way into the plan was a table of the rationalisations and the counter to each. The scenario went green. Removing the table and re-running also went green: the bare rule held on its own. The table cost about 180 words loaded into every link session and bought nothing, so it now lives in the evidence file instead of being re-read on every dispatch. The second finding from the same round: “do not open the plan” was argued with; “these are the files you open” was not. An enumeration has nothing to negotiate against.

So the harness is not a prompt. A hook refuses before the tool runs, a test goes red when a rule loses its witness, and neither is text the model can weigh against a reason. A gate that cannot go red is worse than no gate, because it is believed. And a harness nobody tests dies the way the lint did — green, and quiet.

These numbers are from one wave in August 2026, taken while the rules were still being written, and the window sizes they imply were never recorded. What I would keep is the order of events: a loss, then a rule with the loss in its reason, then a test that fails if the rule stops firing. I cannot point to a rule in it that arrived any other way.