learnaiwithrafa
ClaudeWorkflows

To Stop Babysitting Claude, Give It a Failing Test

Autonomy is not a permissions problem, it is a verification problem. Claude can already run unattended — what is missing is a check it cannot talk its way past. Here are the three you can wire up today.

6 min read3 sources
  • #claude-code
  • #testing
  • #hooks

Everyone asking "how do I let Claude run without watching it" is asking the wrong question. You already can — auto mode approves the tool calls, and /loop and cron will re-run it all night. The reason you still hover over the terminal is not permissions. It is that you have no way to know whether the work is actually done, so you read every diff yourself. That is not supervision, that is being the test suite.

The fix is to stop being the check. Every hour of unattended agent work you want has to be bought with a check that runs without you, and there are exactly three places to put one.

The insight that reframes everything: /goal

/goal sets a completion condition and Claude keeps taking turns until it is met. After each turn, a small fast model — Haiku by default — reads the conversation and decides whether the condition holds. A "no" comes back with a reason, which becomes guidance for the next turn.

/goal all tests in test/auth pass and the lint step is clean

Now the constraint that tells you how to write every condition you will ever write: the evaluator does not run commands and does not read files. It can only judge what Claude has already surfaced in the conversation.

Sit with that. It means a condition like "the code is well structured" is worthless — nothing in the transcript proves it. But "pnpm test auth exits 0" works perfectly, because Claude has to run the command, and the output lands in the transcript where the evaluator can read it. The verifiable condition is not just better practice; it is the only kind that functions.

So a condition worth writing has three parts: one measurable end state, the command that proves it, and what must not change on the way there.

/goal every call site of the old session API is migrated.
Done means: `pnpm typecheck` exits 0, `pnpm test auth` exits 0,
no file outside src/auth/ is modified, or stop after 20 turns.

That last clause is not decoration. Conditions can run up to 4,000 characters, and a goal keeps going until it is met or you clear it — a turn limit is your circuit breaker.

The layer that does not negotiate: hooks

/goal is a model judging a transcript. Useful, but a model. When you need a check that holds regardless of what Claude decides, you need a hook — and the docs are explicit that hooks execute as shell commands at fixed lifecycle events, no matter what the model concluded.

A PostToolUse hook that runs after every edit and pushes failures back:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check.sh"
          }
        ]
      }
    ]
  }
}
#!/bin/bash
input=$(cat)
file_path=$(jq -r '.tool_input.file_path' <<<"$input")

if ! npx eslint "$file_path"; then
  jq -n '{decision: "block", reason: "Lint failed. Fix before continuing."}'
  exit 0
fi
exit 0

The {"decision": "block", "reason": "..."} shape is the thing to learn. Your reason goes to Claude as feedback, so it self-corrects instead of stopping. You have turned your linter into part of the agent's loop rather than something you run afterwards and get annoyed about.

Two more worth knowing:

  • PreToolUse blocks before execution — exit 2 and your stderr becomes the blocking reason, or return permissionDecision: "deny" in JSON. This is where a rule you cannot express as a glob belongs.
  • Stop fires when Claude thinks it is finished. Exit 2 and it is not finished. This is your "did you actually run the tests" gate, and it is the highest-leverage hook in the system.

/goal, in fact, is a session-scoped prompt-based Stop hook under the hood. Which tells you where the real power sits: a Stop hook in your settings file applies to every session in its scope, and can run a script for a deterministic check instead of asking a model.

The layer for work that outlives the session

/loop re-runs a prompt on an interval — /loop 5m check if the deployment finished. Omit the interval and Claude picks its own delay each iteration based on what it saw, between one minute and one hour.

Know the boundaries before you rely on it:

  • Tasks only fire while Claude Code is running and idle. Close the terminal and they stop.
  • Recurring tasks expire 7 days after creation — a deliberate bound on how long a forgotten loop can run.
  • There is no catch-up. A missed fire fires once, not once per interval missed.
  • A fresh conversation clears everything; --resume restores what has not expired.

For anything that must survive your laptop closing, that is the wrong tool — use Routines on Anthropic's infrastructure, a Desktop scheduled task, or a schedule trigger in GitHub Actions.

Pick by what should start the next turn

The clean mental model, which took me too long to arrive at:

  • /goal — next turn starts when the previous one finishes; stops when a model confirms your condition.
  • /loop — next turn starts when a time interval elapses; stops when you stop it or seven days pass.
  • Stop hook — next turn starts when the previous one finishes; stops when your script says so.

Polling a build? /loop. Grinding a migration to green? /goal. Enforcing "tests pass before you tell me you are done" across every session forever? Stop hook.

The uncomfortable conclusion

If your repo has no tests, you cannot have autonomous agents. Not "it works less well" — the mechanism is missing. /goal has nothing to evaluate, your hook has nothing to run, and you are back to reading diffs by hand.

Which reframes test coverage entirely. It stopped being about catching regressions for me and became the interface I use to delegate. The team I run writes tests faster now than before Claude Code, and not for a virtuous reason: an untested module is one nobody can hand to an agent, so it stays slow, and everyone notices.

Do this today

Take one task you would normally supervise. Write the /goal condition first, before any code — with a real command and an exit code in it. If you cannot write that line, you have just found the missing test, and writing it is the actual work. Then run the goal with auto mode on and go do something else.

Sources