sagloop
Simple agentic loop. Run a markdown checklist one task at a time,
with a completely fresh agent session for each task.
npx sagloop plan.md
sagloop is a while loop over claude -p (or opencode run). Each iteration
starts a brand new harness process, hands it your document, and tells it to
complete the first unchecked box. When the process exits, sagloop re-reads the
document, confirms the box got checked, prints a status line, and starts the
next iteration with zero carried-over context.
The document is the state. Stop it, edit it, run it again and it picks up
where it left off.
Why
Long agent sessions get worse as context grows. The fix is to stop carrying
the context: finish one step, throw the session away, start the next one
clean.
Claude Code's own repetition tools are built to keep a session going rather
than to start a new one. /loop re-fires your prompt into the same
conversation on a timer. /goal keeps that same conversation working until a
separate evaluator agrees the condition is met. Todo lists track progress
inside one context window. Each of them accumulates the context you were
trying to shed. Subagents do get a clean window per task, but the plan stays
in the parent session, so you cannot stop it, edit the steps that are left,
and pick up from there.
Ralph-style loops do reset between steps, but a shell loop around claude -p
has no way of telling whether a step actually happened. sagloop keeps the
reset and adds the parts that make it usable unattended: a contract for what
counts as done, failure handling that separates a crash from a refusal, and a
run you can read afterwards.
Writing a document
Any markdown file works. Every task-list item in it is a task, in document
order, nested items included. Everything else is context the agent reads
before it starts.
# Add a --json flag
## Context
The CLI lives in src/cli.ts. Tests use node:test. Commit after each task
with a conventional commit message.
## Tasks
- [ ] Add a failing test for parsing --json
- [ ] Make it pass
- [ ] Document the flag in the README
Tips that make docs go well:
- Put everything the agent needs in the context section: file paths, test
commands, constraints, conventions. Each iteration starts from nothing.
- Make each task small enough to finish in one sitting and verifiable on its
own. "Add a failing test" then "make it pass" beats "implement the feature".
- Tell the agent to commit if you want commits. sagloop never touches git.
- Checkboxes inside fenced code blocks are ignored, so you can show examples.
- Work the document up with your agent first. Something like Matt Pocock's
grill-me skill produces exactly this shape.
Check what sagloop sees before running anything:
sagloop plan.md --dry-run
The contract
Each iteration the agent receives a fixed prompt with the absolute path to
your document:
- Read the entire document. The prose is context, the checkboxes are tasks.
- Complete the first unchecked task and only that task.
- Check it off by changing
- [ ] to - [x].
- Optionally add a short note as an indented bullet under the task, for
anything a later task needs to know (files created, decisions, gotchas).
Edit nothing else.
- If you cannot complete it, leave it unchecked and explain why.
- Stop.
Then sagloop compares the document before and after:
| First unchecked box is now checked | Prints a status line, starts the next iteration |
| More boxes flipped, or a different one | Accepts it, prints a warning, continues |
| Nothing flipped, harness exited cleanly | Stops, exit 1, prints the agent's final message |
| Harness exited non-zero | Retries up to 3 times with backoff, then exit 1 |
| Harness hit the timeout | Stops, exit 1. Not retried |
| All boxes checked | Exit 0 |
The agent leaving a box unchecked is the "I'm blocked" signal. sagloop does
not retry that, because re-asking an agent that just read the task and
declined is how loops get expensive. Fix the document and run again.
A timeout is not retried either. It means the task did not fit in the limit,
and a second attempt spends the same wall clock to reach the same place.
Raise --timeout or split the task.
Usage
sagloop <doc.md> [options] [-- <harness args>]
-H, --harness <name> claude | opencode (default: $SAGLOOP_HARNESS or claude)
-m, --model <name> passed to the harness's model flag (default: $SAGLOOP_MODEL)
-n, --max-iterations <n> stop after n completed tasks
--once same as --max-iterations 1
-t, --timeout <duration> per-task wall clock limit, e.g. 90s, 30m, 1h (default: 1h)
-P, --permissions <mode> auto | bypass | harness (default: $SAGLOOP_PERMISSIONS or the
harness default: auto for claude, bypass for opencode)
--log-dir <dir> write a transcript and the raw harness output per run
--dry-run show the parsed tasks and the command, then exit
--no-color disable ANSI colors
Anything after -- goes to the harness verbatim:
sagloop plan.md -H opencode -m anthropic/claude-opus-4
sagloop plan.md -- --add-dir ../shared
Permissions
Headless runs cannot answer permission prompts, so something has to decide.
--permissions picks what, and sagloop prints the choice on startup.
auto | Default. --permission-mode auto plus --permission-prompts none: Claude Code's classifier approves or denies each action, and anything it would have asked a human about is denied. | Not available. |
bypass | --dangerously-skip-permissions | Default. --dangerously-skip-permissions |
harness | No flags. Your configured allow and deny rules apply, and anything that would prompt is denied. | No flags. |
When the harness reports that tool calls were denied, sagloop prints a warning
with the tool names. If an agent declines a task right after a denial, that is
usually why.
Logs and sessions
Every iteration prints the command that reopens that agent's session, so you
can go and read what it actually did:
↺ claude --resume 5ed0a8ef-721c-4935-80e3-eeb482974247
When a run stops early - declined, failed, timed out, interrupted - sagloop
repeats that command as its last line, where you will actually notice it.
--log-dir keeps the whole run on disk. Each invocation gets its own
timestamped directory, so re-running never overwrites the last one:
logs/2026-09-04T05-27-37/
sagloop.log everything sagloop printed, minus the colors
001.jsonl the harness's raw stream for iteration 1
002.jsonl
002.attempt2.jsonl a retry, after the process crashed
002.stderr.log written only if the harness wrote to stderr
The raw streams carry the session id on every line, so a loop left running
overnight is still inspectable in the morning.
Stopping
Ctrl-C forwards SIGINT to the harness, waits five seconds for it to wind
down, then kills it. Press Ctrl-C again to skip the wait. Exit code 130. The
document is whatever the agent left, so re-running resumes correctly.
Exit codes
| 0 | All tasks done, nothing to do, or max iterations |
| 1 | Agent declined a task, timed out, or retries exhausted |
| 2 | Usage error, missing document, or harness not found |
| 130 | Interrupted |
Harnesses
claude | claude -p --output-format stream-json | Default. Supports auto permissions. Cost reported per iteration. |
opencode | opencode run --format json | No auto mode, defaults to bypass. Model names look like provider/model. |
Codex is not supported yet.
Development
npm install
npm test
npm run typecheck
npm run build
Integration tests run the loop against a fake harness that speaks the real
stream-json protocol and edits the document. To run one real iteration
against an installed harness (costs tokens):
SAGLOOP_SMOKE=claude node --test test/smoke.test.ts
License
MIT