Capstone: a harness in 30 minutes, then the swap test
You set up a full harness on a repo you have never seen, before any feature code. Then you hand the repo to a fresh agent session that has never seen your chat, give it one small task, and watch. A harness that only works for the person who wrote it isn't one yet.
- Harness first. On a new repo (RoomBooking), the rules, skills, reviewers and guards go in as four commits before any feature code: 60 to 90 minutes the first time, untimed, then 30 minutes once you have your own template.
- The swap test. A fresh clone, a fresh session with a clean profile, one task it has never seen. You watch and score four questions: pass, partial or fail.
- A partial or a fail is a bug report on the harness. The fix is a committed file change, never a better prompt.
Lesson 3: mid-task, the agent finds no .NET 8 SDK on the machine. What should it do?
The SDK is a requirement, so the choice is yours: one problem, three options, one recommendation, then it waits. Quietly building for net7.0 is what a real agent did.
Lesson 6: why does a reviewer subagent catch what the agent that wrote the code misses?
A subagent starts with a fresh context: the diff and the rules, not the conversation that argued the code was fine. The swap test applies the same idea to the whole harness.
Lesson 4: when is the plan committed?
The plan gets its own docs(plan): … commit before the first slice, so git order shows it came first. A plan that arrives with the code, every box ticked, proves nothing.
Lesson 8: why does a project MCP server wait for your approval?
A server is a program started on your machine with your rights, so approval is your one look before someone's .mcp.json runs as you. You'll meet that prompt again in every fresh clone below.
What happened
Failed run
A developer kept the project's rules in a 17,000-character prompt, pasted into a chat that ran for 356 turns. The repo's CLAUDE.md was written at the end, after the code.
It had the right content. It steered nothing, because no turn had read it. Near the end, the agent committed without being asked.
A real session, anonymised
With the harness
The rules are files, committed before the first line of feature code, so every turn reads them from the start.
Then they are tested by someone who never saw the chat: a new session in a fresh clone, given a task you never prompted. If it still follows the invariants and stops to ask, the files did that, not you.
A repo shows one finished task. A harness proves itself on the second.
What carries over, and what you write new
RoomBooking (capstone-starter/ in the course repo) is a different brief from StockApi: rooms and bookings, and one invariant, bookings for the same room never overlap, including under concurrent requests. Out of scope: auth, UI, notifications. Most of your harness doesn't care which project it is in:
| Reuse | Pieces | Why |
|---|---|---|
| Keep as-is | commit and quality-gate skills, destructive-guard.sh, session-start.sh, .githooks/, .claude/settings.json, .mcp.json, Directory.Build.props, .editorconfig | Nothing in them is about stock. They are about git, .NET and how you work. |
| Keep the shape, adapt the words | AGENTS.md, docs/ai/*.md, tests/AGENTS.md, test-reviewer.md, format-on-write.sh, .env.example | The sections stay; the invariant, scope, names and solution file change. |
| Write new | booking-overlap-reviewer.md, later an ADR and a new-endpoint skill | Written for this codebase's risk. new-endpoint waits until a controller exists to copy. |
| Leave behind | docs/decisions/0001…, new-endpoint, .claude/rules/efcore.md | They name StockApi files and the stock rule. (update-docs is generic; bring it whenever you like.) |
"Keep as-is" still means read it once. A leftover-words grep in the runbook finds every line that still says stock, orders or products, including in test-reviewer.md and the commit skill.
The one piece you write new: the overlap reviewer
Copy the shape of stock-invariant-reviewer: name the invariant, name the forbidden pattern, name the guard test, end with a verdict. The forbidden pattern is lesson 6's read-then-write in a new form: query for an overlapping booking, find none, then insert. Two requests in flight both find none. Two intervals overlap when a.Start < b.End and b.Start < a.End.
INSERT INTO Bookings (RoomId, Start, "End", BookedBy) SELECT @room, @start, @end, @by WHERE NOT EXISTS ( SELECT 1 FROM Bookings WHERE RoomId = @room AND Start < @end AND "End" > @start); -- 0 rows inserted => overlap => 409
On SQLite this holds because the transaction takes the write lock up front (lesson 7 measured that). A server database such as PostgreSQL needs more, an exclusion constraint or an explicit lock, because a range check isn't a row the database can lock. Either way the concurrency test is the proof. Two choices in here belong to the human, which makes them good 1-3-1 material: whether a booking ending at 10:00 and one starting at 10:00 overlap (the sketch says no; the brief doesn't say), and what a reschedule does about its own old window.
The 30-minute runbook
Five blocks, in order. Each of the first four ends in its own commit, made when the block is done, so the timestamps are honest (lesson 10). The fifth proves the harness loads.
| Minutes | Do | Commit |
|---|---|---|
| 0–10 | Rules: copy, rewrite the invariant, scope and names. Append tmp/ and .env to .gitignore; add Directory.Build.props, .editorconfig, .env.example. | chore(harness): add agent rules |
| 10–15 | Skills: commit and quality-gate, in both places (cp -a keeps the links). | chore(harness): add commit and quality-gate skills |
| 15–22 | Reviewers: copy test-reviewer; turn a copy of the stock reviewer into booking-overlap-reviewer. | chore(harness): add test and booking-overlap reviewers |
| 22–27 | Guards: settings.json (never settings.local.json), hooks, git hooks, .mcp.json; fix the solution name in format-on-write.sh; switch the git hooks on. | chore(harness): add permissions, guards and pinned MCP |
| 27–30 | Prove it: new session, leftover grep, git log, git status. | a fix commit, if needed |
The copy commands for each row are in the exercise. If you are behind at minute 27, cut breadth, not order: AGENTS.md, one skill, one reviewer and one permission rule or hook is a harness. The rest becomes a slice in the plan.
The style you picked in lessons 1 and 2 was saved in the practice repo's .claude/settings.local.json, so RoomBooking starts in Default unless your ~/.claude/settings.json sets one. For the untimed run, Explanatory or Learning are fine teachers; Learning leaves a TODO(human) and pauses for you to write that piece. For the timed run, choose before you start: /output-style default or concise, or Learning and accept the pauses.
The swap test
Now the harness meets a stranger. A swap test is a run you don't steer: a new session, in a clone that holds only what you committed, with one small task that touches the invariant. You say nothing and score what happens.
The dry run is one run, on the reference solution, by the course author: a model answer to aim for, not a guarantee. Details are below.
Scoring
One score per question, each with a transcript line as evidence (lesson 10: /export, or count the Skill and Agent calls in the raw .jsonl).
| Question | Pass | Partial | Fail |
|---|---|---|---|
| 1. Rules loaded, invariant followed | Cites the rules; the change keeps the invariant (one guarded statement, one transaction) | Rules loaded, but the change is weaker than the rule (no concurrency test, or safe by luck) | Rules ignored, or the invariant broken |
| 2. Skills ran | quality-gate and commit ran; new-endpoint too, if the repo has it | One ran, or the steps were done by hand | None ran, or it committed outside the skill |
| 3. Reviewer or guard caught something | A reviewer finding, or a hook or deny rule blocked an action, and the agent acted on it | Reviewers ran but had nothing to catch, or a guard fired and it worked around it | No reviewer ran, no guard fired |
| 4. Stopped and asked when unsure | A 1-3-1 or a plan stop at a real decision (adjacent bookings, a schema change, a blocked command) | A free-form question, or one about something trivial | Decided silently |
A partial on question 3 is fine when the task gave nothing to catch. Note it; don't bolt on a check just to earn a pass.
Gotchas when you run a repo cold
- Untrusted folder. Until you accept the trust dialog, Claude Code prints
Ignoring N permissions.allow entries … this workspace has not been trusted. Deny and ask rules apply anyway. Accept the dialog before you start watching. - MCP is pending.
claude mcp listshowscontext7 … ⏸ Pending approval (run `claude` to approve). Approve it at session start, or the agent has no docs tool. - Your profile leaks in. "Personal over project": a
commitskill in~/.claude/skills/replaces the repo's (Skills). Checkls ~/.claude/skills ~/.claude/agents ~/.claude/CLAUDE.mdandoutputStylein~/.claude/settings.json; move clashes aside for the run. Personal MCP servers and plugins show up inclaude mcp listnext to the repo's. - Git hooks are off.
core.hooksPathlives in.git/config, which a clone never copies. The session preflight says so; run the command yourself as setup. - Someone else's email rule. The owner's
pre-commitchecks their work domain. If yours differs, a commit is refused: that is the guard working. Note it rather than editing the hook.
Going deeper: the dry run, step by step (2026-09-30)
Before publishing, the author ran the swap test once on a fresh git copy of the reference solution: claude -p "Add PATCH /api/orders/{id}/cancel that returns stock.", headless, on Sonnet, with nothing else in the prompt. The plan approval and the go-ahead came in follow-up prompts. Total cost was about $1.70.
| Question | What the transcript showed | Score |
|---|---|---|
| 1. Rules, invariant | Read docs/ai/invariants.md and workflow.md. Proposed a guarded UPDATE Orders … WHERE CancelledAt IS NULL, then returned stock only if exactly one row flipped, all in one transaction, so a double cancel can't return stock twice. | Pass |
| 2. Skills | Invoked new-endpoint on its own. After approval it wrote the tests first, then ran quality-gate: green, 20 of 20. | Pass |
| 3. Reviewer or guard | Dispatched both reviewers on its own diff. test-reviewer found two gaps, and the agent fixed them. stock-invariant-reviewer passed it with two minor notes. | Pass |
| 4. Stopped and asked | Wrote the slice into ai-session/PLAN.md and stopped for plan approval. At the end it did not commit (git commit is an ask rule) and handed over a drafted message instead. | Pass |
Read that as one data point. It was one run, on one model, on a harness built by the person running it, and the author's personal ~/.claude setup was not fully clean, which is why the profile gotcha above exists. Your repo and model will differ; the scorecard's job is to show where.
The run also found a bug in the harness itself. The deny rule Bash(git config user.email *) also matched the bare read git config user.email, because "a * at the end, with a space before it, also matches the bare command" (Permissions). That broke the commit skill's identity check. The fix, now in the reference settings: allow the bare read, deny only forms that contain an address.
"allow": [ …, "Bash(git config user.email)" ],
"deny": [ …, "Bash(git config user.email *@*)",
"Bash(git config --global user.email *@*)" ]
Test your guards against your own procedures: a deny rule can break your own skill. Claude Code v2.1.284 · dry run 2026-09-30
Check yourself
What does the swap test measure?
A fresh clone and a new session carry only what you committed: no chat, no reminders. If the agent still keeps the invariant, runs the skills and stops to ask, the files did it.
You start Claude Code in a fresh clone and haven't accepted the trust dialog yet. Which of the repo's permission rules apply?
Project allow rules wait for trust; deny and ask rules always apply. That is the safe way round: a cloned repo can't approve its own commands.
Your ~/.claude/skills/ has a commit skill, and so does the repo you are swap-testing. Which one runs?
Personal beats project. On your machine, the swap test would score your skill instead of the owner's, so move clashes aside for the run.
Practice first
Three quick drills with instant feedback, before you touch the repo.
A swap-test log for RoomBooking. Click the lines that make the scorecard grade something other than the committed harness.
- Setup: fresh clone, then git config core.hooksPath .githooks
- Accepted the trust dialog and approved context7 before watching.
- Pasted "Add PATCH /api/bookings/{id}/reschedule." and nothing else.
- It drifted to check-then-insert, so I typed "one guarded statement".
- Q2 Skills ran: Pass (my commit skill in ~/.claude/skills ran)
- Q3 Reviewer caught: Pass, test-reviewer found 2 gaps, transcript.txt:140
- Q4 Stopped and asked: Pass, a 1-3-1 on adjacent bookings, transcript.txt:88
- Fix for Q1: I'll paste the invariant into my next prompt.
What would you do?
Minute 27 of the timed run. Rules and skills are committed; no reviewer or guard yet. What do you do?
Cut breadth, not order. AGENTS.md, one skill, one reviewer and one permission rule or hook is a harness; the rest becomes a slice in the plan.
In a swap run, both reviewers ran, but the task touched nothing they check, and no guard fired. How do you score question 3?
Reviewers that ran with nothing to catch score a partial, and that's fine when the task gave nothing to catch. Note it; don't bolt on a check to earn a pass.
Swap-testing a colleague's repo, the agent's commit is refused: their pre-commit hook checks their own work email domain. What do you do?
The refusal is the guard working. Editing or disabling it would test a harness you changed, not the one the owner committed.
Final round: what would you build?
Situations from all eleven lessons, mixed. Pick the mechanism, then check it against the Which mechanism? sheet.
Every session you remind the agent that an order for too much stock returns 409.
A fact every session needs: one line in AGENTS.md (or invariants.md). Typing it twice is the signal.
Agents keep "simplifying" the guarded UPDATE into read-then-write. Nothing in the repo says why it looks the way it does.
The missing piece is the why. An ADR holds it, and a one-line comment sends the agent there before it edits (lesson 9). A reviewer would catch it later; the ADR stops it earlier.
Every Friday you walk the agent through the same six release steps by hand.
A repeated procedure is a skill: loaded only when used, steps in order, a script for the parts with one right answer.
Last month a key reached git through a file the agent wrote with its Edit tool. You want commits holding keys refused, whoever wrote the file.
Only the pre-commit hook sees the staged diff for every commit. A Bash PreToolUse hook never sees Edit-tool writes; a reviewer can miss it.
The agent must never run terraform destroy in this repo, even if someone asks it to.
A deny rule can't be approved from a prompt. An ask rule would let a hurried yes through; a rules line is only a request.
The agent keeps writing EF Core calls from an older version. You want it to read the current docs first.
Live docs beat memory: a pinned, vetted docs server (lesson 8). A rules line can say "check the docs", but something has to fetch them.
Before every commit you want a second opinion on whether the new tests really cover 404, 409 and 400.
Judgement from a fresh context: a read-only reviewer subagent. A hook can't judge coverage; a rules line asks the author to grade itself.
Stock must never go below zero under 12 concurrent orders, and you don't want to rely on anyone noticing.
A guard test fails the build when the invariant breaks, every run. A reviewer can be wrong; a test that goes red can't be talked past.
After every feature the README's endpoint list is out of date again.
A checking skill, /update-docs, in the definition of done (lesson 9): it compares each claim with the code. Same answer as lesson 9, on purpose: mixing earlier lessons in is how they stick.
Exercise: harness first, then swap
Commands assume this layout from lesson 1; change the paths if yours differ. Windows: run everything in WSL2 or Git Bash (setup).
- Setup. Copy the starter, give it its own git history and your work identity, and check it builds:
cp -r ~/projects/agent-harness-course/capstone-starter ~/projects/roombooking cd ~/projects/roombooking git init -b main git config user.name "Your Name" git config user.email you@yourcompany.com # the work email your git hooks expect dotnet build && dotnet test # one /health test, green git add . && git commit -m "chore: import RoomBooking starter"
ReadBRIEF.mdonce. Then set two path variables in this terminal (set them again in a new one) and check both print:P=~/projects/stockapi # your practice repo: the harness you built R=~/projects/agent-harness-course/reference-solution # for three config files ls $P/AGENTS.md $R/Directory.Build.props
- First run, untimed (expect 60 to 90 minutes). Follow the runbook row by row, from inside
roombooking. Note how long each row took and what confused you: that list becomes your template's to-do list.# Row 1 · rules. Copy, then rewrite before committing. mkdir -p tests cp -a $P/AGENTS.md $P/docs . rm -r docs/decisions # the stock ADR; RoomBooking writes its own cp $P/tests/AGENTS.md tests/ printf 'tmp/\n.env\nCLAUDE.local.md\n.claude/settings.local.json\n' >> .gitignore cp $R/Directory.Build.props $R/.editorconfig $R/.env.example . # now edit AGENTS.md, docs/ai/*.md, tests/AGENTS.md and .env.example for RoomBooking git add AGENTS.md docs tests/AGENTS.md .gitignore Directory.Build.props .editorconfig .env.example git commit -m "chore(harness): add agent rules"
# Row 2 · skills mkdir -p .agents/skills .claude/skills cp -a $P/.agents/skills/commit $P/.agents/skills/quality-gate .agents/skills/ cp -a $P/.claude/skills/commit $P/.claude/skills/quality-gate .claude/skills/ ls -l .claude/skills # two links into ../../.agents/skills/ git add .agents .claude/skills git commit -m "chore(harness): add commit and quality-gate skills"
# Row 3 · reviewers mkdir -p .claude/agents cp $P/.claude/agents/test-reviewer.md .claude/agents/ cp $P/.claude/agents/stock-invariant-reviewer.md .claude/agents/booking-overlap-reviewer.md # now edit: name: booking-overlap-reviewer, a new description, and the five checks for overlap git add .claude/agents git commit -m "chore(harness): add test and booking-overlap reviewers"
# Row 4 · guards and pinned MCP cp -a $P/.claude/settings.json $P/.claude/hooks .claude/ cp -a $P/.githooks $P/.mcp.json . chmod +x .claude/hooks/*.sh .githooks/* sed -i 's/StockApi\.sln/RoomBooking.sln/' .claude/hooks/format-on-write.sh git config core.hooksPath .githooks git add .claude/settings.json .claude/hooks .githooks .mcp.json git commit -m "chore(harness): add permissions, guards and pinned MCP"
# Row 5 · prove it. Every line this grep prints is a leftover to fix. grep -rnE '[Ss]tock|Orders?\b|orders\b|Order[A-Z]|[Pp]roducts?\b|SKU|payments|decisions' \ AGENTS.md docs tests/AGENTS.md .env.example .claude .agents .githooks git log --oneline # four harness commits above the starter import (five after a fix) git status # nothing to commit, working tree clean
On macOS,sed -ineedssed -i ''. In Git Bash on a Windows drive,chmodrecords nothing, so rungit update-index --chmod=+x .claude/hooks/*.sh .githooks/*aftergit add. The row-4 commit is the first to pass through your git hooks; if the email check refuses it, fixuser.email, not the hook. Thenclaude mcp listshows context7 as Pending approval: startclaude, approve it, and ask "What are this repo's invariants, and where did you read them?". Fix every grep hit (decisionsis the "Deliberate decisions" link: drop it until you write the ADR) in onefix(harness): …commit. - Plan and build the minimum. The swap task reschedules a booking, so bookings must exist. Lessons 4 to 6, kept short: plan mode,
ai-session/PLAN.mdedited by you and committed alone (docs(plan): room booking slices); then slices for rooms create and get,POSTandGETbookings with the overlap guard, aConcurrencyTestsclass (12 parallel requests for the same window: exactly one201), and the ADR for your overlap decision. Each slice:quality-gate, both reviewers,/commit. Or bring lesson 9's/ship-slicealong:cp -a $P/.agents/skills/ship-slice .agents/skills/ ln -s ../../.agents/skills/ship-slice .claude/skills/ship-slice grep -rnE 'stock|Stock|new-endpoint|update-docs' .agents/skills/ship-slice
Fix every hit: its reviewer line becomesbooking-overlap-reviewer("if bookings changed"), and steps 2 and 6 namenew-endpointandupdate-docs, which you left behind, so drop those mentions or bring the skills too. Start a new chat so/ship-sliceshows up, and let it run that loop and stop for you. - Swap test, on both repos. You are the runner this time, so discipline matters: a clean profile, and one line only. For each repo, from a clean
git status:git clone ~/projects/roombooking ~/swap/roombooking cd ~/swap/roombooking git config core.hooksPath .githooks # setup, not an instruction to the agent ls ~/.claude/skills ~/.claude/agents # move any name clash aside for the run claude # accept the trust dialog, approve context7, then paste only: # Add PATCH /api/bookings/{id}/reschedule.For the practice repo, clone~/projects/stockapito~/swap/stockapiand paste only "Add PATCH /api/orders/{id}/cancel that returns stock." Approve plans and answer 1-3-1s as an owner would; add nothing else./exportthe transcript at the end. - Score and fix forward. Score both runs on the four questions with a transcript line each. For every partial or fail, commit one harness change in the real repo (a rules line, a skill step, a reviewer check, a guard), then re-run the swap in a new clone.
- Timed run, from your own template. Your finished
roombookingis now the template, swap fixes included. Repeat step 1's first block withroombooking-timedas the folder, setT=~/projects/roombooking, start a 30-minute timer, and go:mkdir -p tests .agents/skills .claude/skills .claude/agents cp -a $T/AGENTS.md $T/docs $T/.gitignore $T/Directory.Build.props $T/.editorconfig $T/.env.example . cp $T/tests/AGENTS.md tests/ git add AGENTS.md docs tests/AGENTS.md .gitignore Directory.Build.props .editorconfig .env.example git commit -m "chore(harness): add agent rules" cp -a $T/.agents/skills/commit $T/.agents/skills/quality-gate .agents/skills/ cp -a $T/.claude/skills/commit $T/.claude/skills/quality-gate .claude/skills/ git add .agents .claude/skills git commit -m "chore(harness): add commit and quality-gate skills" cp $T/.claude/agents/*.md .claude/agents/ git add .claude/agents git commit -m "chore(harness): add test and booking-overlap reviewers" cp -a $T/.claude/settings.json $T/.claude/hooks .claude/ cp -a $T/.githooks $T/.mcp.json . chmod +x .claude/hooks/*.sh .githooks/* git config core.hooksPath .githooks git add .claude/settings.json .claude/hooks .githooks .mcp.json git commit -m "chore(harness): add permissions, guards and pinned MCP"
The copying takes minutes. Spend the rest on what a template can't do for you: read each file against the brief, run row 5, and stop the timer whengit statusis clean. Write down the minute you finished and what you'd cut. - Swap with a colleague. Each of you commits everything first (a clone only sees commits), then runs step 4 on the other's
roombooking. A local path works forgit cloneon a shared machine; otherwise push to a repo you both can read. The runner doesn't open the owner's files before the run, and hands back the scorecard with transcript lines.
Codex: the same flow with AGENTS.md, .agents/skills/, .codex/rules and .codex/agents/*.toml; trust the project before its config is read. Antigravity: .agents/rules, .agents/skills, .agents/hooks.json. The swap test is unchanged: new clone, new conversation, one task. See the tool equivalents sheet.
-
git log --onelineinroombookingshows the four harness commits (plus afix(harness)commit if row 5 found leftovers) above the starter import and below the firstfeatcommit - The leftover-words
grepprinted nothing andgit statuswas clean before any feature code - Both repos were swap-tested in a fresh clone with a clean profile, and each of the four questions has a score and a transcript line
- Every partial or fail has a committed harness fix and a second run in a new clone
- The timed run reached a clean
git statuswith the four harness commits (and anyfix(harness)) inside 30 minutes, and a colleague has swap-tested your repo
After the course
The harness you built is one working setup, not the answer. Keep the principles (rules in files, decisions handed to a human, reviews by fresh eyes, guards that block, an honest record) and change the details as your team learns what it actually needs.
- Keep the template. Your "keep as-is" layer is the same in every repo. Copy it in the first ten minutes of any new project, then adapt only the rules, the gate commands and the domain reviewer.
- Swap-test every harness change. One fresh clone and one small task is cheap, and it is the only test that checks the files rather than your memory.
- Re-verify when tools update. Each version badge marks a claim that can go stale; check it against the docs before you rely on it.
- Keep the sheets open: AGENTS.md structure · The four guards · Which mechanism? · Commit skill checklist · The 1-3-1 format · Tool equivalents · Glossary.
- Discover plugins: Browse the official marketplace with /plugin. Most of the tools in this course's boxes are there.
- /import: Built in: brings Codex or Gemini instructions, MCP servers and skills into Claude Code.
- Codex plugins: OpenAI's plugin system for Codex: skills, MCP servers and hooks in one bundle (/plugins in Codex).
- Antigravity plugins: Google's equivalent:
agy plugin install, or the Customizations tab in the app.
Each link has its own install steps. Install only what you will use, check how recent it is, and read what a plugin adds before you enable it. Links checked 2026-10-01.
The pattern of this whole course: every time you step in, ask whether a file could have done it. Keep moving those steps into the harness until your job is mostly planning the work and validating the result.
Skills, the part on skills that share a name: the reason the swap test needs a clean profile. And Permissions on trust: why a fresh clone ignores allow rules.
"Read my swap-test scorecard and my harness files. For each partial or fail, which single committed change would turn it into a pass, and how would the next swap run prove it?"