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

The Bash tool

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

This is the tool that turns your agent from a file editor into something that can act: run the test suite, install a dependency, git diff, list a directory the way a human would. bash hands the model a shell — and a shell is the most dangerous tool in the box, because it can do anything the process can.

Three non-negotiables follow from that. The command runs with its cwd pinned to the sandbox (never the host's working directory). Its stdout, stderr, and exit code are captured and fed back as one observation. And it is killed at a hard wall-clock timeout — because at grade time a hanging command would hang the entire stage, and a graded stage can never hang.

One bash call → one observation
bash("node test.js", 30000)
  └─ spawn  bash -c "node test.js"   cwd = $TASK_DIR   timeout = 30000ms
       ├─ exits in time  → { content: stdout+stderr, is_error: status!==0, exit_code: status }
       └─ still running  → SIGKILL → { content: "timed out…", is_error: true, exit_code: 124 }

What to build

Your task

Implement Tools.bash

Implement bash(command, timeoutMs) on the Tools class. Run the command through a shell with cwd pinned to the sandbox root (this.taskDir), capture stdout and stderr concatenated into content, and return the process exit code. Set is_error when the exit code is non-zero. If the command doesn't finish within timeoutMs, kill it and return a timeout result (exit_code: 124) instead of waiting forever. Default timeoutMs to 30000.

src/tools.ts
  /** `bash` — run with cwd pinned to the sandbox, captured output, hard timeout. */
  bash(command: string, timeoutMs = 30000): ToolOutput & { exit_code: number } {
    // spawn a shell with cwd = this.taskDir, capture stdout+stderr,
    // kill at timeoutMs, and return { content, is_error, exit_code }
  }
A hang must die at the timeout — a graded stage can never wait forever

If bash can block, your whole stage can block, and a grader that hangs is a grader that fails everyone. Enforce the deadline yourself: with Node's spawnSync pass { timeout: timeoutMs } and check r.error.code === 'ETIMEDOUT'; if you drive it asynchronously instead, Promise.race the exec against a timer and SIGKILL the child on expiry. Either way, a runaway command resolves to a timeout result — it never lingers.

Pin cwd to the sandbox, not the host

The command must run with its working directory set to the sandbox (this.taskDir) — relative paths like node test.js resolve there, not in whatever directory the agent process happens to live in. The full path-canonicalization guard arrives in Chapter 07; here the cwd pin is the floor under it.

How it's graded

program.ts bash seeds a sandbox, runs your bash(command) against the case fixture, and prints { content, exit_code }. The grader compares that JSON exactly — so cat hi.txt against a seeded hi.txt: "hello" must yield {"content":"hello","exit_code":0}. Capture and exit-code reporting are checked here; a companion process test confirms a hang is actually killed.