Building a Harness From Zero A developer built a repository harness from zero that gives AI coding agents the files, conventions, and rules needed to operate without chat-fed context. The harness centers on a CLAUDE.md entry point under 100 lines, an ARCHITECTURE.md file, a startup script, a feature_list.json, and a claude-progress.md session log, demonstrated on a minimal ledger-cli project. The developer argues that moving the definition of "done" out of chat and into the repository makes agent work reproducible across sessions. We're going to build a harness from zero, explaining step by step how to do it and why. A harness is the set of files, conventions, and rules that give an AI agent everything it needs to operate in a repository without depending on someone feeding it context in a chat. It defines how the agent starts, what it can and can't do, how it knows a task is done, and how it leaves a record of what it did so the next session another agent, or you can continue without rebuilding everything from scratch. The underlying idea is simple: if the criterion for "done" only lives in your head, or in a chat message that's going to disappear, any agent or any person who continues the work is going to make different decisions than you would. A harness takes those decisions out of the chat and puts them in the repository, as the single source of truth. We're going to build a minimal sandbox project, since it's not the important part here — what matters is what we build around it. https://github.com/crisbonomodev/ledger-cli https://github.com/crisbonomodev/ledger-cli The project is a simple ledger that lets you record credit and debit operations, search by date, and get an account's balance, built on top of a LinkedList and a Map. We'll also need a verification mechanism: check and test scripts the harness can run to validate its own changes. Once we have the base of our ledger, let's set up the initial structure of our harness. We need an entry point for our harness — an AGENTS.md file or CLAUDE.md, in Claude Code's case . We'll use /init in Claude to generate an initial CLAUDE.md, and then modify it. Reference commit with the generated base file: https://github.com/crisbonomodev/ledger-cli/commit/e0cb08c81b1d92d3c7fc831bdc05f931b683908f diff-6ebdb617a8104a7756d0cf36578ab01103dc9f07e4dc6feb751296b9c402faf7 https://github.com/crisbonomodev/ledger-cli/commit/e0cb08c81b1d92d3c7fc831bdc05f931b683908f diff-6ebdb617a8104a7756d0cf36578ab01103dc9f07e4dc6feb751296b9c402faf7 CLAUDE.md should contain only the instructions the harness needs to operate, plus references to other files the agent can reach if it needs them. It shouldn't go over 100 lines. Our CLAUDE.md had architecture details in it — let's move those to a new ARCHITECTURE.md file. You can see the state after that refactor in this commit: https://github.com/crisbonomodev/ledger-cli/commit/7ef78c14c334748a29a7b22e1aaf67d77a993e54 https://github.com/crisbonomodev/ledger-cli/commit/7ef78c14c334748a29a7b22e1aaf67d77a993e54 Now we need to add our startup script, which takes care of initializing everything related to the project: installing dependencies, running verification scripts, and the start command, all in a single step — so the Operating Loop has something real to run. You can see it in this commit: https://github.com/crisbonomodev/ledger-cli/commit/bed716002edb4678412f85a4544e17fbc912018e https://github.com/crisbonomodev/ledger-cli/commit/bed716002edb4678412f85a4544e17fbc912018e Once this script is validated, we can move on to structuring feature list.json. But first, we need to define what our ledger-cli's features will be. For this example, they'll be: history account Now let's define the structure of our feature list.json. Each feature needs: You can see the feature list.json in this commit: https://github.com/crisbonomodev/ledger-cli/commit/50fdc573689144e858cba83226cdeeca059d2f3b https://github.com/crisbonomodev/ledger-cli/commit/50fdc573689144e858cba83226cdeeca059d2f3b Now we need a file where we can log every session and task we work through. For this, we use claude-progress.md or just PROGRESS.md . You can see it here: https://github.com/crisbonomodev/ledger-cli/commit/19cf16927c6fcfa83511010a61fc37945182021e https://github.com/crisbonomodev/ledger-cli/commit/19cf16927c6fcfa83511010a61fc37945182021e With this, we now have the base of our harness. Let's run our first feature. The history feature needs to: For now, we'll just leave it the information it has in feature list.json and see what it does. We open Claude in a terminal and give it the following prompt: Follow the operating loop from CLAUDE.md and work on the top priority feature pending on @harness/feature list.json Commit with the result of that run: https://github.com/crisbonomodev/ledger-cli/commit/9e7fbe53907d0a0227de1ec54084b01b2cbb6632 https://github.com/crisbonomodev/ledger-cli/commit/9e7fbe53907d0a0227de1ec54084b01b2cbb6632 Looking at the result, we can see the harness completed the task, but took quite a few liberties with how the code was written. For example: history account: string : { transaction: Transaction, balance: number } { const transactions = this.load .toArray .filter tx = tx.account === account .sort a, b = a.date.localeCompare b.date let balance = 0 return transactions.map transaction = { balance += transaction.type === TransactionType.CREDIT ? transaction.amount : -transaction.amount return { transaction, balance } } } Notice it used an anonymous type, when by convention it would've been better to declare a new AccountHistoryEntry { transaction: Transaction; runningBalance: number } interface in src/types.ts, following the convention we'd been building toward. But we never wrote that convention down anywhere, and since the repository is the single source of truth, it implemented it however it saw fit. To improve this, we have a few options: You can see the files in this commit: https://github.com/crisbonomodev/ledger-cli/commit/261dd0fb7a3c2dc596fcd06693770ef525d9173d https://github.com/crisbonomodev/ledger-cli/commit/261dd0fb7a3c2dc596fcd06693770ef525d9173d Now let's implement the next task and see the result. Before dispatching the implementer, we wrote this task's sprint contract void . What's interesting isn't just the Scope section, but the Exclusions one: Exclusions explicitly out of scope for this sprint - Behavior when id does not exist — undefined, not required, not tested.- Preventing or detecting double-voiding the same transaction — undefined, not required, not tested. Explicitly saying what not to solve is just as important as saying what to solve — without this, an agent might "improve" the task by adding error handling nobody asked for, or worse, leave it half-done. Using our Claude terminal in the project, we run /clear and then: use the implementer subagent for ledger-002 Commit with the implementation: https://github.com/crisbonomodev/ledger-cli/commit/d8d3d187a0f93a8944886ead3ae2802b0e37f04c https://github.com/crisbonomodev/ledger-cli/commit/d8d3d187a0f93a8944886ead3ae2802b0e37f04c And then, to have it evaluated: use the evaluator subagent for ledger-002 The evaluator doesn't give a free-form verdict — it fills out evaluator-rubric.md, which scores 6 dimensions from 0 to 2: correctness does it do what feature list.json asks for? , contract fidelity did it respect the names and locations from the sprint contract, not just "does it work"? , verification did it re-run the checks itself, or trust the implementer's notes? , scope discipline did it stay within what the contract allowed? , reliability does it survive a rm -rf dist && ./init.sh from clean? , and handoff readiness was progress properly recorded? . Only if everything scores 2 or there's a documented exception is the verdict Accept. Now we can see that different agents were used, conventions were respected, the sprint contract was implemented, and the rubric was completed. But the progress file didn't record the commits, and we still aren't really validating that the app stays in a consistent state and runs correctly, beyond the tests. To fix that, we need to add a file listing the checks the evaluator agent has to run as part of the passing requirement. The underlying problem is that "tests pass" isn't the same as "the repository is left in a consistent state." An agent can leave the build broken, progress badly recorded, or stray work files behind, and the tests for that specific feature will still pass just fine. The clean state checklist is a short list of non-negotiable conditions build compiles, tests pass, recorded progress matches what actually happened, no leftover temporary artifacts that the evaluator has to confirm one by one before considering the session closed — it's not enough for just the feature at hand to work. One detail worth calling out: the check that recorded progress is accurate has to happen after committing, not before — the commit hash depends on the entire tree, including the progress file itself, so you can't verify a reference that doesn't exist yet. We implemented the clean-state-checklist.md file in the harness. And while we're at it, remember we'd blocked the evaluator agent from committing — that has to change, because if the evaluator is the one running the final checklist, it also has to be the one closing the session with the commit. https://github.com/crisbonomodev/ledger-cli/commit/bdf3b8a1122dd843109dbd1db78b9e8db55348bf https://github.com/crisbonomodev/ledger-cli/commit/bdf3b8a1122dd843109dbd1db78b9e8db55348bf With these changes in place, we can now implement the third task ledger-003, transfers . Since we already wrote the sprint contract before dispatching it, the prompt is straightforward after a /clear : implement the next task by priority https://github.com/crisbonomodev/ledger-cli/commit/51fa2cbadc51773ec72d167d83fb58347b6a6d7b https://github.com/crisbonomodev/ledger-cli/commit/51fa2cbadc51773ec72d167d83fb58347b6a6d7b This time the whole cycle contract → implementer → evaluator → checklist held up without any manual adjustment on my part. The difference from the first task isn't that the agent got better — it's that this time the design decisions were already in the repository before it started writing code, instead of living in my head. We now have our three features implemented, but our agents left loose fixes and validations scattered along the way. We're going to tackle error handling and validation across the flows. But to do that, we need to split the implementation into subtasks — and that's where we need a safe mechanism for agents to record what they did, so each agent can pick up exactly where the last one left off. We add a new session-handoff file to handle this, update our CLAUDE.md and agent definitions, add the task to the feature list, and write our sprint contract. https://github.com/crisbonomodev/ledger-cli/commit/555b93016eec16974985ee0d1b16cfdd6e9a8e0e https://github.com/crisbonomodev/ledger-cli/commit/555b93016eec16974985ee0d1b16cfdd6e9a8e0e Now we can start implementing our task. Session A builds the shared mechanism the error hierarchy and proves it out on a single flow: You are the implementer. Work on ledger-004 following harness/sprint-contracts/ledger-004-validation-errors.md — but ONLY the "Session A" part of the Delivery Plan: create a complete src/errors.ts the 4-exception hierarchy and implement/test only record 's guard. Don't touch transfer or void . When you're done, write harness/session-handoff.md instead of harness/claude-progress.md, and leave the feature in progress. https://github.com/crisbonomodev/ledger-cli/commit/e854260551a37c401e9121154bc89550c3211b40 https://github.com/crisbonomodev/ledger-cli/commit/e854260551a37c401e9121154bc89550c3211b40 Now we move on to Session B. This is the key point of the whole experiment: it starts with a /clear , with nothing from Session A's chat, and the only context it receives is session-handoff.md: You are the implementer. Start by reading harness/session-handoff.md — don't assume anything else about what the previous session did beyond what that file says. Follow the rest of CLAUDE.md's Operating Loop and finish ledger-004 according to the "Session B" part of harness/sprint-contracts/ledger-004-validation-errors.md: transfer , void , and the CLI's try/catch. Write harness/claude-progress.md as usual. Note: between Session A, Session B, and the evaluator, the intermediate commits were made by me, by hand, in the terminal, orchestrating each dispatch. The implementer is explicitly forbidden from committing — so if you spot a commit "mid-task" anywhere, that's the human operating the harness, not the agent breaking its own rule. Finally, we run the evaluator, which is the one that actually closes out the task with a single commit: You are the evaluator. Evaluate ledger-004 against harness/sprint-contracts/ledger-004-validation-errors.md and harness/evaluator-rubric.md, reviewing both sessions' work together A and B — including whether Session B's code is genuinely consistent with the classes Session A defined, without redefining them or drifting from them. https://github.com/crisbonomodev/ledger-cli/commit/35ffd50101e43dc736be0bd7869e1436c0f9a6a3 https://github.com/crisbonomodev/ledger-cli/commit/35ffd50101e43dc736be0bd7869e1436c0f9a6a3 What matters here isn't that Session B finished the work — it's that it did so without having seen a single line of the chat where Session A was designed. Everything it needed — which error classes already existed, where they lived, what they were called — was in session-handoff.md. And I'm not taking the evaluator's word for it: I checked the code myself afterward — zero redefinitions, zero naming drift. Session B used exactly the classes Session A left behind. That's the point of the whole exercise: a harness isn't a tidy set of files, it's the answer to a concrete question — what does the next session need to know so it doesn't repeat or contradict what's already been decided? When that answer lives in the repo instead of in your memory or in a chat that's about to close, the work becomes pickable-up by anyone: another agent, another session of yours, or someone else on the team. And that habit of not trusting an agent's declared state without checking the evidence yourself is, to me, the one that holds up all the rest. All this talk of contracts, roles, and checklists can sound abstract if you never see the result actually working. After building npm run build , here's a real terminal session using the four features we built: bash $ node dist/index.js record checking 1 500 "Paycheck" $ node dist/index.js record checking debit 120 "Groceries" $ node dist/index.js balance checking balance of checking: 380 $ node dist/index.js history checking 2026-09-13 1 1 500 - Paycheck | balance: 500 2026-09-13 2 0 120 - Groceries | balance: 380 $ node dist/index.js void 2 3 voids 2 - checking 1 120 $ node dist/index.js transfer checking savings 100 4 checking - 5 savings - 100 transferId 4 $ node dist/index.js balance checking balance of checking: 400 $ node dist/index.js balance savings balance of savings: 100 Side note: 1 as the second argument to record means CREDIT — a known, never-fixed bug means comparing it against the string "credit" doesn't actually work; anything else falls back to DEBIT by default. It's exactly the kind of minor technical debt a sprint contract deliberately leaves out of scope, and that stays documented instead of quietly fixed by nobody's request. The numbers check out: 500 − 120 + 120 reversed − 100 transferred = 400 in checking, 100 in savings. That's the result of all four features, built across separate agent sessions, working together without friction. ledger-cli is a toy project on purpose — the idea was to see the problem and the solution in a full cycle in a couple of hours, not to build something production-grade. Depending on the complexity of the project it's applied to, these files will need to be more or less detailed — and in fact, building and maintaining a harness turns out to be an iterative, ongoing process. The full code — CLAUDE.md, sprint contracts, the implementer/evaluator split, all of it — is on GitHub: ledger-cli. Curious how others handle this: when two agent sessions can't share chat context, what do you pin outside the conversation so the second one doesn't quietly redefine what the first one built?