CCLearn

Design your tooling for an agent: crash loudly, never hang

The highest-leverage change is often the project's own tooling rather than the prompt: make commands fail fast and loudly instead of hanging, guard dev-server targets with a pidfile, and tee output to log files the agent can read.

docs corroborate

The official documentation describes the mechanism this relies on. The technique itself is community practice.

What this costs you if it is wrong

Retrofitting a Makefile, pidfile guards and file logging into an existing project is real work with no guaranteed payoff — this is one developer's practice, not a measured result. Logging everything to files inside the repo is a secrets-in-logs footgun: a debug mode that prints outbound emails, tokens or auth links to stdout and into a tracked log directory will leak them the moment that mode runs somewhere shared or the log gets committed. Gitignore the log directory and keep the debug switch off by default.

You asked for a new route and told Claude to check that it works. It ran npm run dev, the server came up, and the command never returned. Two minutes later the turn resumes, Claude has no idea whether the server is actually up, and it runs npm run dev again — now two processes are fighting over port 3000 and the session has learned nothing about your route.

The instinct is to fix the prompt: "don't start the dev server", "always background long commands". Armin Ronacher's argument is that the prompt is the wrong layer. Treat the agent as a chaos monkey that will invoke your tooling completely wrongly, then make wrong invocation cheap. The rule that falls out of that: a crash is acceptable, a hang is not.

What Claude Code already does about hangs

Know the floor before you build on it, because it is higher than the advice assumes.

  • The Bash tool has a timeout — 120 seconds by default, set by BASH_DEFAULT_TIMEOUT_MS, with a ceiling from BASH_MAX_TIMEOUT_MS (10 minutes by default).
  • When a command hits that timeout, Claude Code moves it to the background instead of stopping it, and the tool result says so explicitly, naming the task ID and the path of the file the output is being written to.
  • Background output goes to a file Claude can pull back with the Read tool.

So a hang does not burn the whole session the way it would in a naive harness. It burns two minutes and leaves an orphan process. And half of "log everything to a file the agent can read" is already true for anything that got backgrounded.

What the timeout does not fix

FailureWith Claude Code aloneWith the tooling change
make dev never returns120s burned, then backgrounded with a log pathreturns immediately, or errors in under a second
A second make dev while one is upnothing stops it — two servers, confusing port-in-use errors laterpidfile guard exits non-zero with services already running
Command fails but says littleClaude sees the exit code and whatever reached stdouttee to logs/dev.log, and Claude greps the file for the real stack trace
Agent needs an auth link from an emailit stops and asks you, ending the unattended rundebug mode prints the email to stdout and it pulls the link out itself
Agent does not know the command's nameguesses between npm run dev, pnpm dev, docker compose upone make dev, one make test, named in CLAUDE.md

The pattern: the timeout protects the session, not the machine state and not the legibility of the failure. Those are yours to fix.

The concrete changes

One name to learn. Put the critical commands in a Makefile so there is a single vocabulary, and list it in CLAUDE.md — the docs already tell you CLAUDE.md is for Bash commands Claude cannot guess.

text
.PHONY: dev test logs
dev:
	@if [ -f .dev.pid ] && kill -0 "$$(cat .dev.pid)" 2>/dev/null; then \
		echo "services already running (pid $$(cat .dev.pid)); run 'make stop' first" >&2; \
		exit 1; \
	fi
	@mkdir -p logs
	@npm run dev > logs/dev.log 2>&1 & echo $$! > .dev.pid
	@echo "dev started, pid $$(cat .dev.pid), logs in logs/dev.log"

That target returns in milliseconds, is safe to run twice, and hands the agent a path instead of a wall of streamed output. The error on the second invocation is the whole point: it is a loud, immediate, self-describing crash, and the next thing Claude does is read logs/dev.log rather than ask you what happened.

The rest of the set:

  • Fail fast on missing preconditions. A target that waits on a database that will never come up is worse than one that exits 1 with postgres not reachable on 5432 — run make db.
  • Log to files, and make verbosity a knob the agent can turn. LOG_LEVEL=debug make test beats a permanently noisy build that floods the context window.
  • Print outbound email to stdout in debug mode. This is the single biggest unattended-run unblocker in the list: an agent that can read the signup link out of a log finishes the auth flow instead of stopping to ask you for it.
  • Never require interactive input. Anything that reads from stdin is a guaranteed timeout, and if it is a compound command or touches git, it is a hard stop with no log to recover from.

What is unverified here

This tip comes from a June 2025 post by one experienced developer describing what worked in his own projects. Nobody has measured it. There is no A/B showing that a repo with a Makefile and pidfile guards produces better agent runs than the same repo without one, and the strongest form of the original claim — that a hang "burns the whole session" — is weakened by Claude Code's own timeout and auto-backgrounding, which the docs describe plainly and which the post predates.

What would settle it: instrument your own sessions. Count, over a month, how many turns end with a command that timed out, and how many times Claude double-started a service. Then make the tooling change and count again. Short of that, you can at least confirm what the harness does on its own: hand Claude a command that will outlast its Bash timeout and read the tool result rather than the terminal. The docs describe the shape to look for — a moved command reports Command did not complete within its 120s timeout and was moved to the background, "with the seconds matching the timeout that applied, followed by the task ID and the path of the file the output is being written to." If you get no task ID and no path, the command was stopped instead of moved, and you are in one of the three cases in the note above.

Checkpoint

  • Where does the output of a command that hits its Bash timeout go, and how does Claude get at it?

    Show answer

    Into a file, not into the void. The Bash tool times out at 120 seconds by default — BASH_DEFAULT_TIMEOUT_MS, with a ceiling from BASH_MAX_TIMEOUT_MS, 10 minutes by default — and on hitting it Claude Code moves the command to the background instead of stopping it. The tool result says so explicitly, naming the task ID and the path of the file the output is being written to, and Claude pulls that file back with the Read tool.

    Know that floor before retrofitting anything: half of "log everything to a file the agent can read" is already true for whatever got backgrounded, which is why the strongest form of the original claim — that a hang burns the whole session — is weaker than it sounds.

  • Which three kinds of command are stopped at the timeout rather than moved to the background?

    Show answer

    A command starting with sleep; a command with git anywhere in it; and a compound command Claude Code cannot fully parse into simple commands, where a parameter expansion such as a braced variable is enough to make it unparseable. Those get a hard stop with no background task and no log path.

    That is also how you tell which case you are in: no task ID and no file path in the tool result means the command was stopped, not moved. A make dev that shells out through a wrapper Claude Code cannot parse lands here.

  • Your make dev is run twice in one turn. What should the second run do, and why is exiting non-zero better than starting a second server?

    Show answer

    The pidfile guard should catch it and exit non-zero in milliseconds with services already running, telling you to run make stop first. Nothing in the harness stops a second start on its own — the timeout protects the session, not your machine state and not the legibility of the failure.

    Non-zero beats a second server because two processes fighting over the port surface much later as a confusing port-in-use error, at a point where nothing connects it back to the double start. A loud, immediate, self-describing crash tells Claude what happened on the spot and points it at logs/dev.log instead of leaving it to guess or to stop and ask you. The timeout and backgrounding behaviour is documented; the Makefile-and-pidfile practice around it is one developer's, and nobody has measured it.

Where this came from

Official pages that back this