cc-loopkit
v0.1.1
Published
A self-correcting harness around Claude Code — guardrails, post-edit hygiene, and loops that won't let a task finish until it's actually done. Installs into any project.
Downloads
19
Maintainers
Readme
Claude Code Loopkit
____ ____ _ ___ ___ ____ _ _____ _____
/ ___/ ___| | | / _ \ / _ \| _ \| |/ /_ _|_ _|
| | | | _____| | | | | | | | | |_) | ' / | | | |
| |__| |__|_____| |__| |_| | |_| | __/| . \ | | | |
\____\____| |_____\___/ \___/|_| |_|\_\___| |_|A self-correcting harness around Claude Code: guardrails that stop bad actions before they happen, hygiene that runs after every edit, and a loop that won't let a task "finish" until it's actually done.
Quickstart
Prerequisites: jq (the guardrails parse Claude Code's JSON with it) and the
claude CLI. npx needs Node, which you almost certainly already have. On macOS:
brew install jq. Elsewhere: sudo apt install jq / sudo dnf install jq /
sudo pacman -S jq — the installer prints the right command for your system if it's
missing.
# Fastest — from inside the project you want to work in (no clone needed):
cd ~/code/my-project
npx cc-loopkit init # installs the harness + scaffolds a loop
# From source — to hack on the harness itself:
git clone https://github.com/ksed8/cc-loopkit.git && cd cc-loopkit
./install.sh --init ~/code/my-projectinit (equivalently --init) also drops a PROMPT.md +
.claude/IMPLEMENTATION_PLAN.md into the project so you can fill them in and go. Both
entry points are the same installer and take the same flags (--link, --force,
--no-mcp, --strict) — npx cc-loopkit … just runs the bundled install.sh for
you. --strict makes a missing prerequisite a hard error (handy in CI).
Usage
First, tailor .claude/CLAUDE.md (installed as CLAUDE.md.example if the project
already had memory) to your stack, commands, and "Never" rules — the guardrails
enforce those rules. From then on the per-tool guardrails and post-edit format/lint
run automatically in every session. The two loops below are opt-in, armed per task.
Inner loop — drive one session to "done"
- Fill in
PROMPT.md(scaffolded at the project root byinit): the task, a checkable Definition of done, and any constraints. - Start Claude Code in the project and hand it the task as usual —
session-start.shfeeds itPROMPT.mdautomatically. - Claude can't end a turn while typecheck/tests are red: the Stop gate feeds the failures back and makes it keep going until green.
- To confirm it did the right thing (not just that it compiles), ask: "Use the verifier subagent to check the diff against PROMPT.md."
- When it's shipped, delete
PROMPT.mdto disarm the loop and go back to normal interactive use.
Outer loop — run it autonomously to "done"
- Fill in
PROMPT.mdand.claude/IMPLEMENTATION_PLAN.md(both scaffolded byinit) — break the Definition of done into concrete, checkable steps. - Run
./run.sh(cap iterations with e.g../run.sh 10; default 20). Each iteration is a freshclaude -pthat does the next unchecked step, commits on green, verifies, and appends a note to.claude/MEMORY.md. - It stops on its own:
STATUS: done(all steps pass the mechanical + semantic checks),blocked(same failure 3+ times — it surfaces the Verify log for you), or the iteration cap. - Watch progress in
.claude/IMPLEMENTATION_PLAN.md(Verify log) and.claude/MEMORY.md. Delete both plusPROMPT.mdwhen the task ships.
The rest of this README explains how each gate works under the hood.
Two loops, two shapes of statelessness
There are two independent ways to drive a task to done. Pick based on how much you trust one long session vs. how much you want a hard reset between steps.
Inner loop — one continuous session
Armed by putting a PROMPT.md (goal spec) at the project root — copy
PROMPT.template.md. While it exists:
- Mechanical gate —
hooks/stop.sh(aStophook). When Claude tries to end a turn, it runs typecheck + tests (viascripts/run-checks.sh). If they're red, it returns{"decision":"block","reason":…}, which feeds the failures back and forces another iteration. Green (or nothing to run) → the turn is allowed to end. Guarded against infinite loops bystop_hook_active. - Semantic gate — the
verifiersubagent. Mechanical checks prove the code runs; the verifier reads the diff againstPROMPT.mdand judges whether it does the right thing. Invoke it after a change:Use the verifier subagent to check the diff against PROMPT.md.
No PROMPT.md → this loop is dormant and Claude stops normally.
Outer loop — fresh context every iteration
../run.sh drives this. Instead of one session iterating on itself, every step is
a brand-new claude -p process with zero memory of prior turns. Use this when
you want a hard boundary between steps — no context creep, no accumulated
confusion from a long session talking itself in circles.
State lives on disk, split across two locations by who owns it:
PROMPT.md(project root) — same goal spec as the inner loop. The one file in this whole system a human actually authors, so it stays at the root, maximally visible — every "is a loop armed" check throughout the harness keys off this file's presence..claude/IMPLEMENTATION_PLAN.md— copyIMPLEMENTATION_PLAN.template.md. Holds the step checklist, aSTATUS:line (not_started/in_progress/blocked/done), and a Verify log.run.shgrepsSTATUS:to decide when to stop — get that line's exact text right..claude/MEMORY.md— free-form, append-only, dated entries. The only continuity a fresh-context iteration has with what previous iterations learned or decided. Append at the end (chronological); never rewrite past entries.
IMPLEMENTATION_PLAN.md and MEMORY.md live under .claude/ rather than the
root because nothing ever hand-authors them beyond the initial template copy —
they're pure machine state, written and read only by run.sh and /loop-verify.
Keeping them there also keeps two fairly generic filenames out of collision range
with a project's own files (a project can easily have its own reasons to want a
root-level MEMORY.md or PLAN.md unrelated to this harness).
Each iteration of run.sh is two claude -p calls:
- Implement — reads
PROMPT.md+.claude/IMPLEMENTATION_PLAN.md+.claude/MEMORY.md, does the next unchecked step, commits on green, appends a note to.claude/MEMORY.md. /loop-verify(skill, run withCLAUDE_VERIFY_MODE=1) — a read-only reporter, never a fixer. Runs the same mechanical checks as the inner loop's Stop gate, invokes theverifiersubagent for the semantic check, and records the combined verdict: it updates.claude/IMPLEMENTATION_PLAN.md's Verify log and, only on a full pass, flipsSTATUS:todone. Two hooks enforce "reporter, not fixer" —pre-tool-use.shhard-denies Edit/Write on anything except.claude/IMPLEMENTATION_PLAN.md/.claude/MEMORY.md(full-path match, not just filename) whileCLAUDE_VERIFY_MODE=1, andstop.shexempts that same mode from the fix-it pressure the inner loop normally applies.
run.sh reads STATUS: after each iteration — done exits clean, blocked stops
and surfaces the Verify log to a human (the plan itself is wrong, not just
unfinished), and it caps at ./run.sh [max-iterations] (default 20) so a stuck
loop can't run unattended forever.
/loop-verifyis deliberately not named/verify— this environment already ships a general-purposeverifyskill with different behavior (run the app, observe it) and the same name would silently shadow one or the other depending on scope resolution.
Either way: sessions see loop state automatically
hooks/session-start.sh injects whatever's armed into context at the start of
every session — the PROMPT.md goal spec, .claude/IMPLEMENTATION_PLAN.md, and
the tail of .claude/MEMORY.md (each capped so a bloated file can't flood the
context window). This matters most for the outer loop, where it's the only reason
a fresh, memory-less claude -p process learns any of this without the invoking
prompt having to spell it out. Nothing armed → the hook prints nothing and costs
nothing.
The guardrails (per-tool)
| Hook | Fires on | Does |
| --- | --- | --- |
| hooks/pre-tool-use.sh | Edit, Write, Bash | Denies actions that break the CLAUDE.md "Never" rules (editing merged db/migrations/*) and blocks destructive shell commands (bare --force, reset --hard, unbounded DELETE, curl \| sh, …). Also hard-restricts Edit/Write to .claude/IMPLEMENTATION_PLAN.md/.claude/MEMORY.md when CLAUDE_VERIFY_MODE=1. |
| hooks/post-tool-use.sh | Edit, Write | Formats the edited file (prettier) and lints it (eslint); surfaces lint errors back to Claude immediately. Fast, file-local only. |
| hooks/stop.sh | Stop | The inner-loop gate described above. Exempts CLAUDE_VERIFY_MODE=1 sessions. |
| hooks/session-start.sh | Session start | Injects whatever loop state exists (PROMPT.md, .claude/IMPLEMENTATION_PLAN.md, .claude/MEMORY.md tail) into context; silent (zero cost) when nothing's armed. |
All hooks fail open: any internal error allows the action rather than wedging the
session. They need jq; the guardrails no-op with a warning if it's missing.
Division of labor: per-edit hooks stay fast (format/lint one file); the expensive whole-project checks (typecheck/tests) run once, at the Stop gate.
Config
settings.json— wires the four hooks and sets the permission allow/deny list. The deny list +pre-tool-use.share belt-and-suspenders: static denials for the obvious cases, the script for the context-dependent ones.scripts/run-checks.sh— the mechanical check runner (typecheck + tests). Single source of truth used by bothhooks/stop.sh(inner loop) and the/loop-verifyskill (outer loop) so the two can't silently disagree about what "green" means.mcp.json— MCP servers (github, context7). The installer places this at the project root as.mcp.json— the location Claude Code actually reads.CLAUDE.md— project facts and the "Never" rules the guardrails enforce.../run.sh— the outer-loop runner (see above). Lives at the project root, not under.claude/, since it's an executable entry point, not config.
Tuning knobs
CLAUDE_VERIFY_CMD— override whatscripts/run-checks.shruns (e.g.export CLAUDE_VERIFY_CMD="pnpm typecheck && pnpm test --run"). Otherwise it autodetectspnpm typecheck/tsc --noEmitandpnpm testfrompackage.json.CLAUDE_CHECK_TIMEOUT_SECS— per-check kill timer (default 240). A hung check is killed and reported red rather than letting the Stop hook itself time out — a killed hook would silently fail open. Enforced by a built-in bash watchdog; no GNUtimeoutneeded (stock macOS doesn't have it).- Bump the verifier's
model:fromhaikutosonnetfor stricter semantic review. ./run.sh [max-iterations](default 20) /LOOP_SLEEP_SECS— outer-loop bounds.
Skills
skills/ holds task playbooks Claude loads on demand: agent-llm, debug,
security, testing, data, git-ops, loop-verify (outer-loop internal — not
for ad hoc use), plus docs, frontend, refactor. Each is a SKILL.md with a
description: whose trigger phrases decide when it loads.
