Build your own Claude Code
The Bash tool & your first real fix·35 min

Fix a real bug

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage fix-a-bug --watch

Everything you've built converges here. The loop turns tool_use into tool_result and stops on end_turn. The tools — view, str_replace, bash — run for real against a sandboxed repo. The dispatcher routes the model's choices to them. Wire all of it together and your agent does the one thing that made Claude Code worth building: it fixes a real bug, end to end, on its own.

The planted repo has an off-by-one: sum.js subtracts instead of adds, and test.js fails because of it. Your agent reads the failing test, reads the source, makes the one-line edit, re-runs the test, sees it pass, and stops. You won't write the steps — the model does. You just make sure the loop runs them.

The trajectory the loop must produce
Read  test.js          → see the failing assertion: sum(2,3) !== 5
Read  sum.js           → see the bug: (a, b) => a - b
Edit  sum.js  "a - b" → "a + b"
Bash  node test.js     → "PASS", exit 0
(text) end_turn        → "Fixed the operator in sum.js."

What to build

Your task

Wire the tools into runLoop

Implement the run-task path in program.ts: seed a sandbox from the case files, build Tools and makeExecute(tools), and drive runLoop with the prompt and the case's cassette as the model. After the loop, run the planted oracle command (tools.bash(C.oracle).exit_code) and snapshot the reported files. Print { trajectory, stopReason, files, oracleExit }. You're not writing the fix — the cassette supplies the model's moves; your job is to make the loop execute them for real so the bytes on disk and the trajectory both come out right.

src/program.ts
case "run-task": case "fix-a-bug": {
  const dir = mkSandbox(C.files); const tools = new Tools(dir);
  // runLoop(prompt, makeCassetteModel(C.cassette), makeExecute(tools), C.cap ?? 25)
  // then: oracleExit = C.oracle ? tools.bash(C.oracle).exit_code : undefined
  // print { trajectory, stopReason, files: snapshot(dir, C.report), oracleExit }
}
The cassette only works if your tools really run

The cassette is a fixed list of moves, but each move only succeeds if the previous tool ran for real. The Edit of sum.js needs the read-before-edit anchor set by the preceding view (Chapter 04); the final end_turn text is only honest because the Bash actually returned exit 0. If a tool no-ops or mis-routes, the trajectory diverges from expected.json and the step fails — that's the cassette doing its job.

How it's graded

program.ts run-task is checked four ways at once. The trajectory must equal the recorded tool calls (Read test.js → Read sum.js → Edit → Bash). The final files must match byte-for-byte: sum.js is module.exports = (a, b) => a + b;\n. The stopReason must be end_turn (bounded — no runaway). And oracleExit — the planted node test.js re-run after the loop — must be 0. The agent didn't just produce a passing diff; it produced the right bytes and the test agrees.

You just built claude-code-lite

That's the whole core: a loop, the read/search/write/edit/bash tool surface, a dispatcher, and an agent that fixes a real repository — done by the third hour of the course. Everything from here makes that agent safe, extensible, and durable: permissions so it can't rm -rf your home dir, a sandbox so a confused step can't escape the tree, subagents, MCP, hooks, skills, and compaction for long sessions.