DEV Community

Cover image for Part 3: A Loop Whose Job Is to Do Nothing
Anvay Singh
Anvay Singh

Posted on

Part 3: A Loop Whose Job Is to Do Nothing

TL;DR

  • This loop runs on a schedule and succeeds by doing nothing almost every night.
  • The pass/fail check is plain deterministic code, with no AI in the decision.
  • It can run entirely free on your own machine. Only the cloud/CI version needs a paid API key. Plus the one bug that broke all three loops.

The first two loops in this series work the same way from your side: you start them and watch. This last one runs on a schedule, like a nightly job, while you are not looking. That changes what success even means.

A scheduled maintenance loop is doing its job when it does nothing. It should run every night, find nothing wrong, cost almost nothing, and still be there on the night something actually breaks. This part covers that loop, the hook mechanism that the whole series relies on, and a bug that broke all three loops in the least convenient place possible.

The definition one more time, since this is Part 3 of 3: a loop is a trigger that runs the agent, a check the agent cannot edit that decides pass or fail, and a repeat, or here a wait until the next run. The only new thing this time is the trigger. A timer starts it instead of you.

The problem this loop solves

The browser's poster data is baked ahead of time into JSON files and images. In a real deployment that data goes stale as films are added and metadata changes, so you want to regenerate it every so often and confirm it is still valid before it ships:

on a timer -> regenerate the data -> validate it -> green ships, red shouts
Enter fullscreen mode Exit fullscreen mode

The gate: a plain check with no model in it

The check is a Node script. For every region file it confirms three things and exits non-zero if any of them fail:

  1. it matches the expected JSON schema,
  2. it has at least the minimum film count, and
  3. every poster file it points to actually exists on disk.

There is no language model in that list. The regeneration step might use Claude, but the decision about whether the data is good is plain, deterministic code. That is on purpose. You do not want the thing deciding "is this safe to publish" to be a model that could confidently say yes when the answer is no. Regeneration can involve a model; validation should not.

Hooks, the piece that ties the series together

The same check is also wired into a Claude Code hook, which is worth explaining, because it is the piece that makes the loops in this series trustworthy.

A hook is a shell command attached to a point in Claude's lifecycle. This one runs when a session tries to stop, and it can block the stop by exiting non-zero:

{ "hooks": { "Stop": [ { "hooks": [ {
  "type": "command",
  "command": "node_modules/.bin/tsx scripts/validate-regions.ts >&2 || exit 2"
} ] } ] } }
Enter fullscreen mode Exit fullscreen mode

If a session ends with the data in a broken state, the hook fails the session and the work does not go through. The point is that enforcement does not depend on the model deciding to behave. A plain shell command has the final say. That is the same idea as Part 1's protected-files check and Part 2's read-only reviewer, in a different form.

Two ways to run it, and who pays

This is where the loop got practical in a way I did not expect. There are two reasonable ways to run something nightly, and they differ mainly in who pays.

The cloud way is a GitHub Actions workflow. It runs on GitHub's cron, bakes the data headlessly, runs the gate, and opens a pull request when the data is good or an issue when it is not:

- name: Regenerate datasets (Claude, headless)
  run: |
    claude -p "Run npm run bake. Baking only." \
      --bare --permission-mode acceptEdits --allowedTools "Bash(npm run bake)"
- name: Validate datasets (deterministic gate)
  run: npm run validate
Enter fullscreen mode Exit fullscreen mode

--bare is the scripted mode. It skips hooks, plugins, and CLAUDE.md discovery, and it authenticates with an API key. That key is the pay-as-you-go Anthropic API, which has a $5 minimum and no free tier, because a CI runner has no interactive login to fall back on.

The local way is free. This loop does not actually need a model, since the bake is deterministic and the gate is a plain script, so the free version is a local job that macOS launchd runs every night. Because the output is byte-for-byte stable, the normal result is a clean no-op:

✓ korean.json (120 films, all posters present)
✓ tamil.json  (200 films, all posters present)
✓ nothing to refresh — data byte-stable, gate green. Loop cost: ~nothing.
Enter fullscreen mode Exit fullscreen mode

A note on cost that caught me out

I got this wrong at first, so let me state it plainly. There are two separate ways to pay for Claude:

  • The Claude Code subscription (Pro or Max) covers claude running locally. Loops 1 and 2 use this, which is why Loop 2 stopped on a session limit rather than a bill.
  • The Anthropic API key is the metered, pay-as-you-go one. Only a cloud or CI run needs it.

I made the local job the default and kept the cloud workflow in the repo as the documented option for a real unattended setup. Same gate, different tradeoff, and it helps to know which one a given loop is spending.

The bug that broke all three loops

The best bug in the project belongs here, because it did not only hit this loop. It hit all three. The first time any loop ran inside a fresh git worktree, which I used so a runaway loop could not touch my main checkout, it died right away:

sh: playwright: command not found
Enter fullscreen mode Exit fullscreen mode

A git worktree does not share node_modules with your main checkout, because those files are gitignored, and gitignored files are not shared between worktrees. So the loop had no dependencies installed and could not even start. The fix, now in every loop driver, is a one-line guard that installs them if they are missing:

if [ ! -x node_modules/.bin/tsx ]; then npm ci; fi
Enter fullscreen mode Exit fullscreen mode

This is where the project's motto comes from: get the harness solid before you make the loop long. Until the environment a loop runs in is reliable, you do not really have a loop. And an unattended job at 3 AM is the worst place to find out your setup was broken.

What I took from Part 3

A scheduled loop succeeds quietly. Most nights it should do nothing and cost almost nothing, and the whole design is really about making sure the one loud failure gets through on the night it matters.

Here are the three loops side by side:

Loop Trigger Gate Success looks like
1. Goal you run it a number (FPS) from a test the number goes green
2. Maker-checker you run it a rubric applied by a separate read-only agent the reviewer says APPROVED
3. Scheduled a timer deterministic data validation it quietly does nothing

They all rely on the same rule from Part 1: the thing being checked cannot edit the check, and only a person moves the target. When that holds, the loop is something you can leave running. When it does not, it only looks like it is working, which is worse than not having it.

And that is the series. The movie site was only ever a way to build and test the loops.

The full project, including the drivers, gates, agents, and hooks, is on GitHub:

MovieBrowse — a loop-engineering learning project

A small, deliberately-boring regional cinema browser: a fast, virtualized grid of film posters for one "region" (a language / film industry, not a country), with a manual region selector and a geo-IP default.

But the website is not the point. It's a vehicle for learning and demonstrating loop engineering with Claude Code — building automated loops where an AI agent makes a change, a deterministic check decides whether that change is good, and the loop repeats until the check passes. The interesting part is the loops, not the movie site. If the website ever became the hard part, something went wrong.

This repo ships three loops, each a different shape, each anchored in a real config and a real failure. There's one write-up per loop in blog/.


What is a "loop"?

A loop here means:

run something → check a verifiable

If you build a loop of your own, I would like to hear what you had it check. Feedback and suggestions are always welcome, and thanks for reading all three parts.

Top comments (0)