Your repo
Pass a gate and its module goes in, with real files that do a real job. At the end you take the harness you earned to your own project.
Nothing earned yet. Everything is still yours to take: the gates are there to teach, and nothing is locked.
0 of 18 gates passed, 0 of 44 files lit.Press a wire to see what its gate earns.
- .claude/0 of 4
- agents/0 of 1
- hooks/0 of 2
- .github/0 of 3
- workflows/0 of 1
- docs/1 of 12
- decisions/0 of 1
- scripts/0 of 9
- src/0 of 2
AGENTS.md
The instructions any agent reads: the commands, where things are, what never to do. Your kept lines are merged in, each with room for the mistake it came from. It mentions CI only once your download has the kit's CI file, which runs every gate.
What it stops: The agent working out how to run your tests by trial and error, every session.
Earned at gate 6, The agent. You can read it and take it now anyway. Press Keep in my rules on any term, and it lands here.
Really ran: Kept lines merge into AGENTS.md, and CLAUDE.md imports it; What AGENTS.md says CI runs is what the download's CI runs.
# AGENTS.md(One sentence: what this project is and who it's for.)## Commands- `bun install`: install from the lockfile.- `bash scripts/check.sh --fast`: run this before you say you're done. Quiet when green, and it names the gate when not.- `bash scripts/check.sh`: every gate, including the dependency audit. Run it before you merge.- Mutation testing is one of those gates, and the slowest.- CI, in `.github/workflows/ci.yml`, runs `bash scripts/check.sh`: every gate, not only the fast ones. If you change what CI runs, change this line.- `bun test`: the tests alone.- `bun scripts/crap.ts --report`: the most complicated functions.## Where things are- `src/`: the code, with tests beside it as `*.test.ts`.- `docs/decisions/`: why things are the way they are. Read this before changing an area it covers.- `docs/ratchet-log.md`: past failures and the check each one caused.## Never- Never make a check pass by weakening it: no skips, no `@ts-ignore`, no `--no-verify`, no lower limits. If a check is wrong, say so and ask.- Never read `.env` or any credential file. `.env.example` lists the names, and a person fills in the values.- Never add a dependency without saying why, and what the lockfile diff brought with it.- TypeScript stays on version 6 while Stryker is here. Stryker core 10.1.1 crashes on TypeScript 7.0.2.## Rules from real mistakesEach rule here came from something that went wrong. If a rule could be a type, a lint rule or a test, make it that and delete the line. If nobody can say what the mistake was, delete the rule.(None yet. When you correct an agent on the same thing twice, write the rule here, with what went wrong under it.)Having these files proves nothing
Anyone can download them, with no gates passed. An interviewer knows that, and will look past the config to the history.
What proves something is the commit trail of using them: a check that refused real work, the fix, the rule it caused, and a row in the ratchet log that names the mistake. Nobody can download that.
The first hour after unzipping
For TypeScript on Bun. These steps are in the download too, as docs/first-hour.md.
1Put it on its own branch
unzip -n never overwrites a file you already have. Anything it skips, such as your own package file or .gitignore, you merge by hand. That includes .claude/settings.json: once one is in place, an agent can't change it, or a hook already there, which is the point. If an agent brings the kit in for you, have it do .claude/ last. Starting a new project? Run git init -b main first.
git switch -c harnessunzip -n ~/Downloads/harness-typescript.zip2Install it
gitleaks and jq are separate programs, and until they are installed the check says no. That is on purpose: a check that can't run has not passed.
bun installbunx lefthook installbrew install gitleaks jq3Commit it unchanged
This is the first entry in your trail. Everything after it shows you adapting the harness to a real project, which is the part nobody can download.
git add -Agit commit -m "chore: add the harness, unchanged"4Find out where you stand
On a real project it will be red. Read the first failure, and don't lower a limit to make it go away. If the mutation floor or the complexity limit is out of reach today, set it to your real number, write that in docs/ratchet-log.md, and only ever raise it.
bash scripts/check.sh5Watch each gate say no
A check you have never seen fail is one you only hope works. Break a test and try to commit it. Then rename a tool the check needs and run it again. Save both logs: they are your first evidence.
bash scripts/test-hooks.sh6Try the hooks on a live agent
We ran these hooks inside the real Claude Code program, but with a script standing in for the model, so nobody has seen how a real model reacts. In a scratch repo, ask your agent to delete a folder with find -delete. Then break a test, ask it to finish, and check that it gets sent back.
7Turn CI on, and make it count
We linted the workflow, but it can only run on GitHub, so read it first and watch its first run. Then add a ruleset on main that requires the check job and a pull request. Until that exists, nothing stops a merge.
git push -u origin harness8Write the first real rule
Next time you correct your agent on the same thing twice, add the strongest check that would have caught it, watch it fail on the old mistake, add a row to docs/ratchet-log.md, and commit with a message saying what went wrong. One of those is worth more than this whole download.
What really ran, and what didn’t
scripts/check/kit.ts builds the zip with this page’s own code, unzips it into a scratch folder, installs it on a tiny app, then seeds real failures and confirms each gate says no. Last run: 5 November 2026 at 22:13 UTC, on darwin arm64.
Not run
- A real model reacting to being blockedThe Claude Code program here is real, but the model is a script on localhost. Nobody has seen how a real model behaves when it is sent back.
- The workflow running on GitHubIt can only run on GitHub, and no repo was used for it. It may still fail there: read its first run.
- dependabot.ymlGitHub reads this file, and it was not tried there.
- gitleaks' own rules on a real-looking keyOn purpose. Only the made-up canary was seeded, so nothing here looks like a real key. gitleaks' own rules were loaded, and not exercised.
- WindowsOnly macOS on Apple silicon was tried. The scripts are bash, so Windows would need WSL or Git Bash.
Ran, and nothing stops it
Each of these was really tried. Better you know now than find out in front of someone.
- Turning strict types off in the configNothing in the kit stops it. This is why a person reads every change to a config file.
- If the Stop script itself is missing, the agent gets away with stoppingAs documented, onFailure has no effect on Stop hooks, so a missing script fails open. A file the agent can't edit, and CI, are what cover this.
Ran and passed
Download9 passed
- The page's zip code, opened by a real unzip45 files passed unzip's CRC test and match byte for byte.
- Execute bits survive the zip9 scripts unzip executable, and nothing else does.
- Every chapter's earned download renders cleanlyFiles per chapter, the same on both stacks: 1:4 2:1 3:1 4:1 5:1 6:5 7:2 8:1 9:7 10:5 11:3 12:1 13:2 14:1 15:2 16:4 17:2 18:1.
- Deny rules already in place never stop the agent adding a later chapter's files31 files arrive after the first settings.json, and no deny rule already in place covers any of them. Changing settings.json itself, or a hook already there, stays denied.
- What AGENTS.md says CI runs is what the download's CI runsAGENTS.md mentions CI only in downloads that have it (chapters 16 to 18), and there it says CI runs every gate, which the workflow does: bash scripts/check.sh, without --fast.
- AGENTS.md, the ratchet log and package.json name only what the download hasIn every chapter's download, each path these three files name is in it, and they mention mutation, complexity and CRAP only once those gates are.
- Kept lines merge into AGENTS.md, and CLAUDE.md imports itAGENTS.md is 38 lines with two kept rules. CLAUDE.md is exactly @AGENTS.md.
- Every shell script parses9 scripts checked with bash -n.
- .claude/settings.json against the published schemaValid against json.schemastore.org/claude-code-settings.json, apart from onFailure, which that schema does not list yet. The Claude Code docs do, from 2.1.295.
Install1 passed
- bun install, from package.jsonInstalled, and wrote bun.lock.
Good code passes1 passed
- scripts/check.sh passes on good codeformat and lint (2 s), types (0 s), duplication (1 s), secrets (1 s), tests (0 s), complexity and CRAP (0 s), budgets (0 s), the hook still blocks (8 s), dependency audit (2 s), mutation (1 s)
Each gate says no11 passed
- A lint errorExit 1 at the "format and lint" gate.
- A type errorExit 1 at the "types" gate.
- Copied codeExit 1 at the "duplication" gate.
- A committed fake secretExit 1 at the "secrets" gate. It matched only the kit's made-up canary rule.
- A failing testExit 1 at the "tests" gate.
- A too-complex functionExit 1 at the "complexity and CRAP" gate.
- A file over its budgetExit 1 at the "budgets" gate.
- Tests that run the code and check almost nothingExit 1 at the "mutation" gate.
- A skipped testExit 1 at the "format and lint" gate.
- @ts-ignore over a type errorExit 1 at the "format and lint" gate.
- A missing tool is a no, not a passRefused with each one gone: bun, gitleaks, jq, biome.
Hooks, fed the documented JSON9 passed
- The PreToolUse hook blocks rm -rf and allows lsrm -rf src: exit 2 with a reason on stderr. ls -la: exit 0.
- The hook holds across its whole list of spellings68 spellings blocked, 27 ordinary commands allowed.
- With jq missing, the hook says noEven ls -la gets exit 2 when the command can't be read.
- The Stop hook lets the agent stop on greenExit 0.
- The Stop hook blocks on a red suiteExit 2, with the failing output on stderr for the agent to read.
- It still blocks with stop_hook_active trueExit 2 on the second attempt too. Claude Code's own limit is what ends a loop.
- Near Claude Code's limit it still blocks, and asks for a final message a person can useExit 2 on all eight attempts in a row. From the 7th it tells the agent to stop fixing and write a final message: not finished, which gate, what it tried. The first block doesn't.
- With a tool missing, it blocksWith no bun: exit 2. With scripts/check.sh gone: exit 2.
- If the checks hang, it blocks before its own timeoutGiven a check that hangs and ignores TERM, with the limit set to 1 s: exit 2 after 4 s, saying the checks took too long.
Inside real Claude Code10 passed
- CLAUDE.md really pulls in AGENTS.mdA rule kept in AGENTS.md was in what Claude Code sent to the model, through the one-line CLAUDE.md.
- The hook stops a delete the deny rules don't namefind src -delete: the files are still there, and the agent was told why.
- The hook lets an ordinary command runls ran, and its output came back to the agent.
- A deny rule keeps .env away from the Read toolThe Read was refused, and the value never reached the model.
- The agent's file tools can't change its own hooks or settingsEven with permission prompts bypassed, a Write to each was refused by a deny rule: .claude/hooks/block-destructive.sh, .claude/hooks/stop-gate.sh, .claude/settings.json.
- With chapter 6's settings in place, the agent can add chapter 9's stop hookThe Write tool added .claude/hooks/stop-gate.sh past chapter 6's deny rules, which name only the hook already there. Wiring it into settings.json stays a person's job.
- With the hook script missing, onFailure blocks the commandEven ls was blocked, with Claude Code naming onFailure as the reason.
- The Stop hook sends a finished agent back, and keeps doing soWith a stand-in that keeps saying it's done, the hook sent it back 8 times before Claude Code ended the turn: the limit in the docs, not a retry guard. The last turn it got asked for a final message saying what is still red.
- On green, the agent stops first timeOne turn, and no block.
- A pass of ralph.sh can run the check and commit, as the loop prompt asksThe real Claude Code program, driven by ralph.sh, ran the fast check, ticked the item and committed it, and ralph.sh then stopped with exit 0: plan done, checks green.
Git hooks, on real commits5 passed
- lefthook.yml is valid, and installslefthook validate passed, and pre-commit and pre-push hooks are installed.
- git commit refuses a lint errorCommit refused, exit 1.
- git commit refuses a staged fake secretCommit refused, exit 1. It matched only the kit's made-up canary rule.
- With lefthook missing, git commit says noCommit refused, exit 1, because assert_lefthook_installed is on.
- git push refuses a red suiteRed push refused, exit 1. Green push accepted.
Drill scripts8 passed
- tamper-check passes an honest branchExit 0 with nothing changed.
- tamper-check refuses an added skipExit 1.
- tamper-check refuses a deleted testExit 1.
- tamper-check refuses a changed expected valueExit 1: main's tests fail against the changed code.
- quiet.sh prints one line on a pass, everything on a failOne line on exit 0. Full output and the same exit code (3) on a fail.
- curl-timing.sh on a real local requestReported a 200 with timings from a real local server.
- package-exists.sh against the real registryleft-pad exists, a made-up name is missing, exit 1.
- ralph.sh calls it done only with the plan ticked and the checks green, and stops on its limitsEach stop fired, against a stand-in for claude that spends nothing: done and green (exit 0), an empty plan, the pass limit, an agent error. Green checks with an item still open is not done, and neither is a ticked plan with red checks. Not run against a real model.
Earned downloads2 passed
- Only gate 6: the hook works, with no Stop wiringPreToolUse wired, Stop left out, and the spellings list passes.
- Only gate 9: the check runs on the gates it has, and one of them runs the tests11 files. The check passes with: format and lint, types, tests. With a failing test added, it says no at the tests gate.
CI3 passed
- The CI workflow, linted with actionlint1.7.12
- Each pinned SHA really is that tag on GitHubactions/checkout v7.0.1, oven-sh/setup-bun v2.2.0, resolved with git ls-remote.
- The gitleaks checksum in the workflow is the published oneCompared with gitleaks_8.30.1_checksums.txt from the release.
Tools
- gitleaks
- 8.30.1 (not installed here, so fetched into the scratch folder and checked against its published checksum)
- bun
- 1.4.2
- uv
- uv 0.10.10 (8c730aaad 2026-03-13)
- git
- git version 2.54.0 (Apple Git-157)
- jq
- jq-1.7.1-apple
- actionlint
- 1.7.12
- unzip
- UnZip 6.00