hello-world
verifiedThe canary. Appends one timestamped greeting to its own state file — and proves the whole spine: clone → model → task → verify → commit → push.
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
skill-first
Loop Runner ships as a Claude Code skill. One command teaches your agent the whole runbook — setup, canary, authoring, deploy, debug.
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
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"
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.
why a harness
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.
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
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
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
Nobody's watching at 3 a.m. A PreToolUse guard blocks the irreversible — rm -rf /, force-push, reset --hard — plus any project you declare off-limits.
✓ GUARD_BLOCKED_PROJECTS=prod-*
start a run
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.
deploy.sh provisions the Cloud Scheduler cron right next to the Job. Nightly refactors, morning digests, hourly sweeps.
trigger: schedule
schedule: "0 3 * * *"
A GitHub Action fires the Job through Workload Identity Federation — no long-lived keys. Issue labeled, PR opened, release cut.
trigger: event
# Actions + WIF
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
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.
LOOP=<name> selects the spec. Adding a new kind of loop never touches the runner — it's just a new folder.
Model, turn cap, prompt, verifier, push mode — all read from loop.yaml in the fresh clone. Editing a loop needs no rebuild.
Declared connectors: get credentials injected by the proxy, outside the sandbox. Least privilege by construction.
headless browser
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.
Headless Chromium plus Google's chrome-devtools MCP server — navigate_page, click, fill, take_snapshot, … Nothing downloads at run time.
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.
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,…"
the paper trail
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.
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.
The harness's narration plus your verify.sh verdict — pushed? work done? pass or fail? — without opening a single transcript.
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.
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
./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
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
The canary. Appends one timestamped greeting to its own state file — and proves the whole spine: clone → model → task → verify → commit → push.
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.
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
recipes
Copy-paste starting points for real loops. First up: the GitHub issue-fixer — label an issue before you leave, review the PR over coffee.
# 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
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
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"