← All skills

loop-harness

v1.1.0MITlong runs

A loop can tick on schedule for a full day and accomplish nothing, because since v2.1.196 a scheduled fire hands any skill Claude may not invoke itself to the run as plain text, `/verify` and `/code-review` included, with no error anywhere. The other half is that a dynamic loop has no visible process, so a working one and a dead one look identical. This picks the mechanism before it writes anything (an event monitor usually beats polling, and work with a finish line belongs to goal-harness instead), size-checks `.claude/loop.md` against its 25,000-byte cap, and arms the wake signal with a heartbeat, a renewal reminder before the seven-day expiry, and a ledger you can read without interrupting the run.

Install
/plugin install loop-harness@fledgeling-plugins

Needs the marketplace added first — how to do that.

Reach for it when

Turn a rough intent into a hardened, armed /loop that survives being left alone.

Not for

NOT for work with a verifiable finish line (use goal-harness) or a one-off task.

What ships with it

Say any of this

  • loop on this until it's done
  • keep checking the deploy
  • monitor the benchmark runs and fix errors
  • create a loop prompt
  • set up a loop.md
  • harden this loop

Taken from the skill’s own trigger description — these are the phrases it listens for. You do not have to match them exactly.

What comes backIllustrative — written from the skill’s documentation, not captured from a run

keep checking the deploy

Mechanism picked BEFORE anything is written.

  event monitor   ← chosen: the deploy emits status
  polling         rejected: 30s of nothing, 120 times
  goal-harness    rejected: no finish line here

  ✓ .claude/loop.md  8,204 / 25,000 bytes
  ✗ /verify passed as plain text — a scheduled fire hands
    any skill you may not invoke to the run as prose,
    with no error. Rewritten as an explicit instruction.

ARMED
  wake signal   deploy webhook
  heartbeat     every 20 min, writes to the ledger
  renewal       reminder at day 6 of the 7-day expiry

Easily confused with

Why it exists

/loop schedules a cron in the current session. That's the whole mechanism, and every one of its edges is a way for a loop to look fine while doing nothing.

It expires after seven days; the task fires one last time, deletes itself, and nothing warns you. It fires only while the session is idle. It never catches up a fire it missed. It dies on /clear.

Then there's the one that costs the most. Since v2.1.196 a scheduled fire only invokes skills Claude is allowed to invoke on its own. Built-in commands, and any skill marked disable-model-invocation (which includes the bundled /verify and /code-review), arrive at the run as plain text. No error. The run reads the text, treats it as a note, and carries on. A loop whose whole job was "run /code-review every hour" ticks correctly all day and reviews nothing.

The other half of the problem isn't mechanical, it's that a loop has no surface. From the session that armed one:

"what exactly is performing the loop; i expected to see a monitor or script running, did i invoke it incorrectly?" "i expected /loop to be able to be left"

Nothing was wrong. A dynamic loop is a scheduled wakeup with no visible process, so a working loop and a dead one look the same.

What it does

It picks the mechanism first

This is the decision that matters most, and /loop doesn't make it; it schedules whatever you typed. The question that settles it is what has to be true for the next tick to be worth running.

AnswerMechanismWhy
"Something happened" (a log line, CI finishing, a PR comment)Monitor, persistent: trueThe event wakes the session as it happens. No wasted ticks, cheaper, and visible
"The work is finished"goal-harnessThat's a finish line, not a cadence. It routes rather than arming the wrong thing
"Enough time has passed, but how much varies"Dynamic pacingThe run picks 60 to 3,600 seconds per tick from what it just saw
"It's 9am" / "another five minutes"Fixed cronThe only case where polling is actually right
"The same sweep as last time"Bare /loop + .claude/loop.mdThe file replaces the built-in maintenance prompt
"It has to run whether or not my laptop is open"Routines, a Desktop task, or GitHub Actions/loop needs the session open. Better to say so than arm something that won't survive

Most requests phrased "keep doing X until Y" are a goal wearing a loop's clothes.

Then it writes, preflights and arms

Two files, one source of truth. docs/loops/loop-<slug>.md is the full brief, committable and reviewable. .claude/loop.md is the rendered active file Claude Code reads for a bare /loop, and it's capped at 25,000 bytes; past that the tail is truncated with a warning, so the end of your protocol silently stops applying. write-loop-md.sh size-checks and refuses.

The preflight covers the traps: skills that arrive as text, CLAUDE_CODE_DISABLE_CRON, a symlinked .claude or scheduled_tasks.json (which makes scheduling fail outright), the 50-task-per-session cap, loop.md size and precedence, and whether the interval maps to a clean cron. 7m doesn't; it gives uneven gaps at :56 to :00. 90m is an hour and a half, which cron can't express at all.

Arming happens in an order, so the loop has a wake signal before it has a schedule:

  1. The Monitor, if events drive the ticks, with a filter covering failure as well as success.
  2. The loop itself, running the first tick immediately rather than waiting for the first fire.
  3. A heartbeat at 1,200 to 1,800 seconds. In dynamic pacing a tick that neither reschedules nor stops gets one fallback fire about twenty minutes later and then the loop ends; the heartbeat is what catches that.
  4. A renewal reminder on day six, because of the seven-day expiry.
  5. The ledger, one row per tick, via tick.sh.

Important A monitor is only as good as its filter, and the failure is always the same shape: it watches for the success marker and goes quiet on a crash, which is indistinguishable from still running. Before arming one, ask whether it would emit anything if the process died right now. If not, widen it. Noise is cheaper than a missed failure.

Note --resume restores unexpired crons, but Monitor and background Bash tasks are never restored. A loop whose wake signal was a Monitor comes back deaf, so the tick protocol calls TaskList first and re-arms it. Arming is idempotent, so that costs nothing when it's already running.

Installing

/plugin marketplace add fledgeling-co/fledgeling-plugins
/plugin install loop-harness@fledgeling-plugins

Using it

On its own:

/loop-harness monitor the benchmark runs, resolve any harness or config errors,
and restart the benchmarks as needed

As a follow-up to the built-in:

/loop /loop-harness keep checking the deploy and fix what breaks

After a loop has gone quiet, or when you can't tell whether it's running. It reads the actual scheduled tasks and the ledger.

scripts/status.sh answers "how's it going" without interrupting the run, including a warning when the expiry is within a day. scripts/disarm.sh removes the active loop.md and prints what still has to be cancelled in the session, because a cron job and a Monitor are session state rather than files and no script can remove them.

The rules that keep it useful

Prefer an event to a poll. A Monitor that streams a line when something happens beats a cron asking every five minutes whether anything happened, on cost, on latency, and on ticks wasted.

Bound the loop. A stop condition in the tick protocol, plus a seven-day expiry you know the date of.

One loop per concern. Two loops in one session compete for idle time and both get slower.

Give every tick a no-op branch. Without one, a tick with nothing to do invents work to justify itself.

What's in the box

skills/loop-harness/
  SKILL.md
  references/
    mechanics.md          how /loop actually works, with the binary and doc citations
    mechanism-choice.md   the decision table, worked cases, monitor filters, intervals
    failure-modes.md      ten observed failures, each mapped to its fix
    templates.md          the brief, the active loop.md, the ledger, the arming sequence
  scripts/
    preflight.sh          the traps; read-only, exits 1 if anything blocks
    write-loop-md.sh      renders and size-checks .claude/loop.md; --dry-run shows the diff
    tick.sh               appends one ledger row per tick
    status.sh             reads the ledger, warns near the expiry
    disarm.sh             removes the active loop.md, prints what's left to cancel
evals/evals.json          six cases plus the process evals
EVALS.md                  the measured result against the no-skill baseline

Does it earn its place

Measured the same way as its sibling: each case run twice from an identical fixture, once with the skill and once with nothing, graded blind. Across both, 32 of 33 assertions against the baseline's 12, and 8 preferences out of 8.

The two cases that carry it are the ones where the baseline was not merely thinner but wrong. Asked what performs a no-interval loop, it said there is no timer and the loop ended with the turn; dynamic pacing schedules a real pending wakeup, so acting on that answer means re-arming something already scheduled. Told to run /verify every 30 minutes, it concluded /verify does not exist, missing that it is a bundled skill a scheduled fire delivers as plain text.

Against that, one honest tie: on "don't arm a loop with a dead verifier" the baseline matched it 3/3. EVALS.md has the table, the tie, the three defects the run found in these skills, and what was never measured.

What it doesn't do

It can't keep a loop running without the session. /loop needs the conversation open and the machine on. Backgrounding the session carries the tasks over and keeps them firing without a terminal, which is usually the fix; where the work genuinely has to run unattended, the skill routes you to Routines, a Desktop scheduled task, or GitHub Actions instead of arming something that won't last the night.

It doesn't survive /clear. A new conversation drops every session-scoped task. The brief and the ledger stay on disk, so re-arming is quick, but the loop itself is gone.

It can't make a tick correct. It can make a tick visible, bounded, and honest about whether it ran. What the tick does is still down to the protocol you wrote.