A small, auditable loop that runs Claude Code unattended against a queue of well-specified tasks.
The premise is that the bottleneck in AI-assisted development is not the model, it is the human approving each step. Remove the human from the inner loop and the work is bounded by how well the tasks were specified rather than by how many hours you sit in front of the terminal. That only holds if two things are true: the work is defined precisely up front, and something other than your judgement at 3 a.m. decides whether the result is acceptable. Here that something is a verification command that either passes or does not.
This repository is deliberately small. It is a bash loop, two prompts, and a set of guardrails. The guardrails are the point.
queue/todo/*.md --> for each task:
git worktree from BASE_BRANCH (your checkout is untouched)
claude -p <- prompts/build.md + the task
VERIFY_CMD
|- pass -> commit on branch auto/<task> [push] [PR]
|- fail -> claude -p --resume <- prompts/fix.md
(bounded by MAX_FIXES)
--> queue/done/ or queue/failed/
--> logs/report-<timestamp>.md
One task at a time, one worktree per task, one report per run. In the morning you read a single file.
auto-claude.sh the loop
add-task.sh task scaffolding helper
config.example.sh configuration template (copy to config.sh)
prompts/build.md system rules for the build session
prompts/fix.md system rules for a fix round
queue/{todo,doing,done,failed}/
logs/ reports, traces, prompts, verification output
test/run-tests.sh suite, runs against a fake claude CLI
claude(Claude Code CLI), authenticatedgit,jq,bashghonly if you enableOPEN_PR
chmod +x auto-claude.sh add-task.sh test/run-tests.sh
cp config.example.sh config.sh
$EDITOR config.sh # REPO, BASE_BRANCH, VERIFY_CMD, SETUP_CMD
./add-task.sh "Add a health check endpoint"
./auto-claude.shconfig.sh holds machine-local paths and is gitignored; config.example.sh is
the versioned template.
To run overnight without the machine sleeping and without holding the terminal:
caffeinate -is nohup ./auto-claude.sh > logs/nohup.log 2>&1 &To run it on a schedule, 0 22 * * * cd ~/auto-claude && ./auto-claude.sh is
enough. Note that cron runs with a minimal environment; make sure claude and
jq are on the PATH it sees.
This is where most of the outcome is decided, and it is the part that stays
human. A task file is plain markdown in queue/todo/. Files are picked up in
alphabetical order, so a numeric prefix gives you ordering.
A task that survives an unattended run has:
- A closed objective. "Add
GET /healthreturning{"status":"ok"}", not "improve observability". - Verifiable acceptance criteria. This is what the model uses to decide it is finished.
- Context. Which files to look at, which pattern to follow, what not to touch.
- The scope of an afternoon, not a sprint. Split larger work across several
numbered files. Each task branches from
BASE_BRANCHindependently, so if task 02 depends on task 01 you must either say so in the text or merge them into one file.
queue/todo/00-example.md.sample is a worked example.
| Risk | Guardrail |
|---|---|
| Damage to your working tree | Every task runs in its own git worktree on branch auto/<task> |
| Unreviewed code reaching the mainline | The loop never commits or pushes to BASE_BRANCH; PUSH_BRANCH and OPEN_PR default to false |
| The run dying on a usage limit | Limits are detected, the loop sleeps LIMIT_SLEEP and resumes the same task; nothing is lost |
| Two runs at once (a cron overrun plus a manual start) | A lock under .lock/ refuses concurrent runs and recovers locks left by a dead process |
| A hung verification or setup command freezing the night | VERIFY_TIMEOUT and SETUP_TIMEOUT kill the command after the configured seconds |
| A transient API outage (HTTP 500/502/503/504) | Bounded retries (TRANSIENT_RETRIES every TRANSIENT_SLEEP seconds) before the session is given up |
| An unbounded loop | MAX_HOURS stops new tasks outside the window; MAX_FIXES bounds fix rounds per task |
| Unbounded spend | MAX_BUDGET_USD per invocation; the report totals actual cost |
| False completion | Nothing is archived as done without VERIFY_CMD passing, and the prompts forbid deleting or skipping tests to get there |
| Work lost to an interrupted run | Tasks left in queue/doing/ are requeued at the start of the next run |
| Wasted hours on broken credentials | An HTTP 401 or 403 aborts the run immediately and requeues the task instead of retrying |
Each run writes to logs/:
report-<timestamp>.md- per task: PASS or FAIL, branch, diffstat, duration, fix rounds, and Claude's own handover summary, plus totals and cost.run-<timestamp>.log- the loop's own trace.verify-<task>.log- the last verification output for a failing task.prompt-<task>-*.txt- exactly what was sent, for when a result surprises you.
Failed tasks keep their branch and worktree so you can inspect the work.
./test/run-tests.shThe suite runs the loop end to end against a fake claude CLI on PATH, so it
makes no API calls and costs nothing. It covers the happy path, base-branch
isolation, fix rounds, the MAX_FIXES ceiling, usage-limit backoff, the
authentication abort, requeueing of interrupted work, report accuracy, the
run lock (concurrent and stale), transient 5xx retries, verification timeouts,
permission-flag quoting, config validation, and task numbering. If shellcheck
is installed, the suite also lints every script.
AUTO_CLAUDE_HOME relocates queue/ and logs/, and AUTO_CLAUDE_CONFIG
points at an alternative config file. The tests rely on both.
Read this section before pointing the loop at anything you care about.
PERMISSION_FLAGS defaults to --dangerously-skip-permissions. That flag is
what makes unattended operation possible, and it means Claude executes shell
commands, edits files, and reaches the network without asking. The blast radius
is bounded by the worktree and by the fact that nothing is pushed by default,
not by the flag and not by the wording of the prompts. Prompt instructions are
guidance, not a sandbox.
Practical consequences:
- The process inherits your credentials. SSH keys, cloud CLI sessions, npm and Docker tokens are all reachable from inside the worktree. A worktree is isolation for your source tree, not for your machine or your accounts.
SETUP_CMDandVERIFY_CMDrun arbitrary code, including whatever lifecycle scripts your package manager triggers. Untrusted dependencies get executed unattended.- Task files are prompts. Treat anything the loop reads, including files pulled in from the repository, as untrusted input capable of steering the model. Do not queue tasks you did not write.
- Enabling
PUSH_BRANCHorOPEN_PRmakes the output visible to others. Keep them off until you have read a few mornings of reports and trust what you see.
Reasonable hardening, in rough order of value: start on a throwaway repository;
run inside a container or VM with only the credentials the task needs; replace
PERMISSION_FLAGS with an explicit allowlist and accept that the loop will
occasionally stall waiting for a permission nobody will grant, for example:
PERMISSION_FLAGS=(--allowedTools "Read Edit Write Bash(npm *) Bash(git diff)")set MAX_BUDGET_USD; and review every branch before merging.
Two mechanical protections back this up: the loop refuses a config.sh that is
world-writable (it is sourced as code), and PERMISSION_FLAGS is passed to
claude as an argument array, so a quoted allowlist reaches the CLI intact
instead of being re-split by the shell. The legacy string form still works and
is split honouring its quotes, with glob expansion disabled.
- Parallelism. Worktrees already permit it. Run several instances against separate queues, mindful of your usage limits.
- Notifications. Post the report summary to a webhook at the end of the run.
- A review pass. Add a second
claude -pcall over the diff with a code review prompt, and let its verdict count toward verification.
- "another run is active (pid N)". Two instances share the same
AUTO_CLAUDE_HOME; let the first finish, or stop it. A lock whose owner died is detected and removed automatically on the next start. - Every call fails with HTTP 401 and the run aborts. An
ANTHROPIC_API_KEYexported in your shell profile takes precedence over your claude.ai login. Unset it, or start the loop withenv -u ANTHROPIC_API_KEY ./auto-claude.sh. - A task ends in
queue/failed/. Its branch and worktree are intact. Start withlogs/verify-<task>.log, then the prompt files. - Stale worktrees.
git -C <repo> worktree prune, then delete theauto/*branches you no longer need.
This software is provided as an example of an unattended workflow. It executes AI-generated code on your machine with your credentials and without human review at the moment of execution. You are responsible for everything it does: for the code it writes, for the commands it runs, for the cost it incurs, and for anything that reaches a shared branch. Review the output before it goes anywhere that matters. There is no warranty of any kind; see LICENSE.
MIT. See LICENSE.