loop-hello-world · cloud run job

Example run log: the job clones the library, starts the credential-injecting proxy, the agent appends its greeting, verify.sh exits 0, the stop hook commits and pushes, and the transcript is archived to Cloud Storage.

Loop engineering · headless

Run any agentic loop headless in a Cloud Run Job — the harness guarantees commit → push → verify → record before it exits, so the agent can't forget its own work.

npx skills add SaschaHeyer/loop-runner

then ask your agent: "help me set up the loop runner"

Apache-2.0 · Claude Code on Vertex AI · Google Cloud Run Jobs

TRIGGER EXECUTE VERIFY RECORD STOP the agent forgets — the repo doesn't ✦

skill-first

Install it like a skill. Because it is one.

Loop Runner ships as a Claude Code skill. One command teaches your agent the whole runbook — setup, canary, authoring, deploy, debug.

1

Add the skill

Installs /loop-runner into Claude Code, following the Agent Skills standard. Add -g to keep it across every project.

npx skills add SaschaHeyer/loop-runner
2

Say the words

Claude follows the runbook: fork the library, fill .env, wire secrets into Secret Manager, deploy the hello-world canary.

"set up loop runner and deploy the canary"

3

Watch a run land

One execution later there's a commit on origin/main the agent didn't have to remember to make. The spine works; now author real loops.

work_done=1 pushed=true

why a harness

Loops fail in famous ways.
The harness owns them.

The agent runs as a subprocess that shares the container's filesystem — so the container, not the model's memory, enforces what must happen. You own the container.

"Deploy-without-commit"

solved

The demo worked; main never heard about it. A Stop hook commits + pushes whatever the agent leaves behind — the persistence guarantee runs even when the agent forgets to.

✓ hooks/ · stop → commit → push

"Groundhog Day"

solved

Every run wakes up with amnesia. Each loop declares a memory: dir — git-backed state, committed after every run. The repo is the loop's memory.

✓ memory: loops/<name>/state

"Trust me, it worked"

solved

A model grading its own homework isn't verification. verify.sh runs in the harness, outside the agent — a check that reads back ground truth, not the model's own summary.

✓ verify.sh → exit 0 = pass, anything else = fail

"The 3 a.m. footgun"

blocked

Nobody's watching at 3 a.m. A PreToolUse guard blocks the irreversiblerm -rf /, force-push, reset --hard — plus any project you declare off-limits.

✓ GUARD_BLOCKED_PROJECTS=prod-*

also in the box bundled Firestore emulator per-loop subagents external skill repos shared commons repos

start a run

Many ways in.
One way through.

Cron, GitHub events, a command from your terminal — every trigger lands on the same Cloud Run Job. The trigger decides when. The spec decides what. The harness guarantees the rest.

schedule

While you sleep

deploy.sh provisions the Cloud Scheduler cron right next to the Job. Nightly refactors, morning digests, hourly sweeps.

trigger: schedule
schedule: "0 3 * * *"
event

When GitHub twitches

A GitHub Action fires the Job through Workload Identity Federation — no long-lived keys. Issue labeled, PR opened, release cut.

trigger: event
# Actions + WIF
manual

Whenever you say

Your terminal, a CI step, even another loop — anything that can call the Cloud Run API can pull the trigger.

gcloud run jobs execute
loop-<name> --wait

how it works

One image. Any loop.
Keys stay outside.

Trigger · start a run

Cloud Scheduler

trigger: schedule

GitHub Action · WIF

trigger: event

gcloud jobs execute

manual

Execute · Verify

Cloud Run Job

LOOP=<name> picks the spec at runtime

Claude Code agent

subprocess · shares the filesystem

egress proxy :8081

injects the credential on the wire

stop hook: commit → push → verify → record

Record

GitHub

commit · PR · state

Cloud Storage

transcript · cost log

Model calls go straight to Vertex AI via the job's service account — never through the proxy.

One task-agnostic image

LOOP=<name> selects the spec. Adding a new kind of loop never touches the runner — it's just a new folder.

Specs read at runtime

Model, turn cap, prompt, verifier, push mode — all read from loop.yaml in the fresh clone. Editing a loop needs no rebuild.

The agent never holds a key

Declared connectors: get credentials injected by the proxy, outside the sandbox. Least privilege by construction.

headless browser

Some loops need
eyes and hands.

curl can hit an endpoint. It can't tell you the button is wired, the form actually submits, or the page threw a JavaScript error. Loops that verify a deployed UI or reproduce a browser-only bug get a real one.

Baked into the image

Headless Chromium plus Google's chrome-devtools MCP server — navigate_page, click, fill, take_snapshot, … Nothing downloads at run time.

Pay only if you use it

Chrome launches on the first tool call. A loop that never names the tools runs byte-identical to a non-browser loop — and a browser walk is turn-expensive, so give a driving verifier roughly double the max_turns.

One line to opt in

Name the tools in the loop's allowed_tools — or in a sub-agent's tools: frontmatter. No flag, no spec field; the runner detects the intent and wires up the MCP server per run.

allowed_tools: "Bash,Read,mcp__chrome-devtools__navigate_page,mcp__chrome-devtools__click,…"
loop-ui-check · agent transcript
navigate_page https://preview.your-app.dev
  · chrome launches on first tool use
take_snapshot form#signup · 42 elements
fill email ← [email protected]
click button "Create account"
wait_for "Welcome aboard"
rendered state matches the API · no console errors
ground truth: grep the archived transcript for mcp__chrome-devtools__ calls

the paper trail

Don't trust the loop.
Read the receipts.

A summary is the loop describing itself. The archive is what actually happened — every agent transcript, turn by turn, unedited, shipped to Cloud Storage at the end of every run.

gs://<project>-loop-sessions
$ gcloud storage ls …/auto-fix/loop-auto-fix-ab12c/
  result.json  run.log  session.jsonl  sessions/
$ python3 loop-runner/view_session.py auto-fix/loop-auto-fix-ab12c
maker · 41 turns · $0.84 · 6m 12s
  · Bash  npm test → 1 failing
  · Edit  src/cart/total.ts
  · Bash  npm test → green
  · Bash  gh pr create → #214
the PR the summary claims, read back from the transcript
one bucket per project · one folder per loop · one per execution

result.json

The CLI's own exit JSON, verbatim: final result, cost, duration, tokens. log_cost.py reads it to append every run to the shared cost log.

run.log

The harness's narration plus your verify.sh verdict — pushed? work done? pass or fail? — without opening a single transcript.

sessions/*.jsonl

Every agent that ran: the maker plus each spawned sub-agent, one transcript each. If a summary claims a sub-agent did something, this is where you check.

replay any run

get_run.py reconnects to an execution; view_session.py renders its transcript readable. Live runs stream to Cloud Logging in real time.

anatomy of a loop

A loop is a folder.
Four files. That's it.

./new-loop.sh my-loop scaffolds it — or let the /loop-runner skill interview you and fill in the decisions.

the whole surface area

loops/nightly-refactor/
├── loop.yaml   ← the spec
├── prompt.md   ← the brief
├── verify.sh   ← exit 0 = pass
├── agents/
│   └── verifier.md ← independent judge
└── state/      ← git-backed memory

push modes — pick the blast radius

push: main · the loop ships push: pr · the loop proposes push: none · read-only
loops/nightly-refactor/loop.yaml no rebuild needed
name: nightly-refactor
trigger: schedule
schedule: "0 3 * * *"       # while you sleep

model: claude-sonnet-4-6
max_turns: 40              # STOP: turn cap
budget_usd: 3              # STOP: cost cap

repo: your-org/your-app     # Two-Repo Mode: PRs land there
prompt: loops/nightly-refactor/prompt.md
verify: loops/nightly-refactor/verify.sh
connectors: []              # least privilege
memory: loops/nightly-refactor/state
push: pr                    # propose, never land

the library

Loops that already run.

hello-world

verified

The canary. Appends one timestamped greeting to its own state file — and proves the whole spine: clone → model → task → verify → commit → push.

push: main connectors: [] 6 turns · $1

auto-fix

verified

Finds the failing test in a work repo, fixes the root cause in the source — never the test — proves npm test green, and opens the PR.

push: pr two-repo mode 50 turns · $5

your-loop

yours

A ticket triager. An error sweep. A Slack-digest bot. An engineer that drains a backlog overnight. Any workflow with a real check and a stopping condition.

./new-loop.sh my-loop
honest boundaries
  • Runs on Google Cloud (Cloud Run Jobs + Scheduler) with Claude on Vertex AI. That's the supported runtime today — it isn't cloud-agnostic yet.
  • A fresh GCP project enables the Claude models in Vertex Model Garden once (accept the EULA).
  • Every loop declares its verification honestly — a check that reads back ground truth beats a model grading its own work. Say which one you have.

recipes

Steal this loop.

Copy-paste starting points for real loops. First up: the GitHub issue-fixer — label an issue before you leave, review the PR over coffee.

loops/issue-fix/loop.yaml
# fix new GitHub issues while you sleep
name: issue-fix
description: >
  Pick the newest open issue labeled agent-ready,
  reproduce it, fix the root cause in the source
  (never the tests), prove the suite green, and
  open a PR that closes the issue. No matching
  issue? Record the check and stop.

runtime: loop-runner
trigger: event            # the Action fires it →

model: claude-sonnet-4-6
max_turns: 60            # STOP: turn cap
budget_usd: 5            # STOP: cost cap

repo: your-org/your-app   # Two-Repo Mode: PRs land here
prompt: loops/issue-fix/prompt.md
verify: loops/issue-fix/verify.sh
allowed_tools: "Bash,Read,Write,Edit,Glob,Grep"

connectors: []           # github is built in
memory: loops/issue-fix/state # dedupe = one branch per issue
push: pr                 # propose, never land
.github/workflows/issue-fix.yml · in the work repo
on:
  issues: { types: [opened, labeled] }
permissions: { id-token: write }  # WIF — no stored keys
jobs:
  fire:
    if: contains(github.event.issue.labels.*.name, 'agent-ready')
    runs-on: ubuntu-latest
    steps:
      - uses: google-github-actions/auth@v2
        with: { workload_identity_provider: "…", service_account: "…" }
      - run: gcloud run jobs execute loop-issue-fix --region=us-central1
  • The agent-ready label is the gate — a human opts each issue in.
  • push: pr means it can never land on main alone — auto-merge is your branch protection's call.
  • One branch per issue (issue-fix/123) is the dedupe ledger — a rerun sees it and skips.
Get the complete loop — prompt.md + verify.sh + trigger, ready to deploy
more on the way error sweep slack digest → issues nightly refactor

Give your agent
a night shift.

Install the skill, deploy the canary, and wake up to commits the agent couldn't forget to make.

npx skills add SaschaHeyer/loop-runner

then ask your agent: "help me set up the loop runner"