Skip to content

srdjan/looper

Repository files navigation

Looper Logo

Looper - Extensible Improvement Loop for Claude Code

Claude Code stops when it thinks it's done. Looper keeps it going until it's actually done. It's a native plugin that runs your quality checks - build, lint, tests - every time Claude says "finished," and pushes it back with the failures if anything is red. The code you get compiles, passes lint, and has green tests, because Claude kept iterating until those things were true. No external wrappers, no log scraping - it runs inside Claude Code's own hook system.

A minimal kernel dispatches hook events to packages that define every step of the loop: what to check, how to score, when to stop. The bundled quality-gates package reproduces the classic behavior (typecheck, lint, test, coverage gates), but you can create packages for TDD cycles, security audits, documentation verification, or anything else.

Install

Quick start

curl -fsSL https://raw.githubusercontent.com/srdjan/looper/main/install.sh | bash

This clones the repo to ~/.claude/plugins/looper, checks that jq and claude are available, and prints the command to start Claude Code with the plugin. Or clone manually with git clone https://github.com/srdjan/looper.git ~/.claude/plugins/looper.

Marketplace (coming soon)

claude plugin install looper@claude-plugins-official

Requirements: jq (brew install jq on macOS, apt install jq on Debian/Ubuntu). Bundled shell packages only need jq. SDK-authored packages declare their own runtime in package.json; the current supported value is deno. If a configured package requires a missing runtime, Looper fails closed and blocks edit tools until the runtime is installed or the package is removed from .claude/looper.json.

What Happens on First Run

  1. Start claude in any project.
  2. Looper inspects repo truth - stack markers, package.json scripts, lockfiles, tool configs - and writes .claude/looper.json with matching gates.
  3. A bootstrap confidence summary shows what was verified versus inferred.
  4. Ask Claude for a code change.
  5. After Claude finishes, the Stop hook runs your quality gates.
  6. If any required gate fails, Claude gets the failure output and tries again.
  7. The loop ends when all gates pass or the iteration budget is reached.

Run /looper:bootstrap to verify your setup. Run /looper:doctor to compare your config against the repo's actual tooling. Run /looper:looper-config for guided fine-tuning.

Disable / Uninstall

claude plugin disable looper@claude-plugins-official    # stop hooks from firing
claude plugin uninstall looper@claude-plugins-official  # remove the plugin entirely

For source installs, remove the plugin directory:

bash ~/.claude/plugins/looper/uninstall.sh

Project config (.claude/looper.json) and state (.claude/state/) are preserved unless explicitly cleaned up during uninstall.

Usage

claude

> implement a user avatar upload endpoint with validation

# Claude works. After each response, the kernel dispatches to package stop
# handlers. If any package is unsatisfied, Claude gets another turn with
# feedback. When all packages are satisfied (or budget is reached), Claude stops.

Configuration

Edit .claude/looper.json:

{
  "max_iterations": 10,
  "packages": ["quality-gates"],
  "quality-gates": {
    "gates": [
      {
        "name": "typecheck",
        "command": "npx tsc --noEmit --pretty false",
        "weight": 30,
        "skip_if_missing": "tsconfig.json"
      },
      {
        "name": "lint",
        "command": "npx eslint .",
        "weight": 20,
        "skip_if_missing": "node_modules/.bin/eslint"
      },
      { "name": "test", "command": "npm test", "weight": 30 },
      {
        "name": "coverage",
        "command": "$LOOPER_PKG_DIR/lib/check-coverage.sh",
        "weight": 20,
        "required": false
      }
    ],
    "checks": [
      {
        "name": "format",
        "command": "npx prettier --check {file}",
        "fix": "npx prettier --write {file}",
        "pattern": "*.ts,*.tsx",
        "skip_if_missing": "node_modules/.bin/prettier"
      }
    ]
  }
}

Top-level keys max_iterations and packages are kernel config. Everything under a package name key is that package's config.

Gate Options

Field Default Description
name required Gate identifier
command required Shell command; exits 0 = pass
weight required Points awarded on pass
skip_if_missing - File/binary path; gate skipped with full points if absent
required true If false, gate failure doesn't block completion
run_when - Array of glob patterns; gate skipped if no files_touched match
timeout 300 Seconds before the gate command is killed
enabled true Set false to disable without removing

Baseline-Aware Gating

Enable baseline capture to distinguish pre-existing failures from failures Claude introduces:

{
  "quality-gates": {
    "baseline": true,
    "baseline_timeout": 60,
    "gates": [...]
  }
}

When baseline is true, all gates run at SessionStart before Claude makes any changes. The results are stored as a pass/fail snapshot. On each Stop evaluation, failures that match the baseline are marked as pre-existing (~) and do not force another iteration. Only new failures Claude introduces (x) cost iteration budget.

Field Default Description
baseline false Enable baseline capture at session start
baseline_timeout 60 Per-gate timeout in seconds during baseline capture

Post-Edit Checks

Field Default Description
name required Check identifier
command required Shell command; {file} is replaced with the edited file path
fix - Auto-fix command run silently after a failing check
pattern - Comma-separated globs; check runs only on matching files
skip_if_missing - File/binary path; check skipped if absent
enabled true Set false to disable

Context and Coaching

{
  "quality-gates": {
    "context": [
      "This project uses Deno with Oak framework.",
      "Never modify the API contract in docs/api.md."
    ],
    "discover": {
      "test_files": "find . -name '*.test.*' | head -20",
      "runtime": "deno --version 2>/dev/null || echo 'not installed'"
    },
    "coaching": {
      "urgency_at": 3,
      "on_failure": "Fix the specific failures. Do not refactor unrelated code.",
      "on_budget_low": "Only {remaining} passes left. Fix failing gates only."
    }
  }
}

Bundled Packages

quality-gates

The default package. Runs typecheck, lint, test, and coverage gates after each response. Auto-detected per stack. See Configuration above.

scope-guard

Prevents Claude from editing files outside a declared scope. Uses PreToolUse to block edits to protected files in real time, and Stop (post phase) to report scope compliance.

{
  "packages": ["quality-gates", "scope-guard"],
  "scope-guard": {
    "blocked": ["package-lock.json", ".env*", "*.config.js"],
    "allowed": ["src/**/*", "tests/**/*"]
  }
}
Field Type Description
blocked string[] Glob patterns for files Claude must never edit. Edits are blocked in real time via PreToolUse.
allowed string[] Glob patterns for files Claude may edit. Out-of-scope edits are flagged at Stop.

blocked patterns are enforced immediately: Claude's edit is denied before it happens. allowed patterns are checked at the end: if Claude edited files outside the allowed set, the loop continues until the violation is resolved.

scope-guard runs in the post phase, so it only evaluates after quality-gates passes.

acceptance-flows

Verifies user-visible behavior after core quality gates pass. Runs named smoke or acceptance commands, stores stdout/stderr artifacts in .claude/state/acceptance-flows/, and forces another loop only when required flows fail.

{
  "packages": ["quality-gates", "acceptance-flows"],
  "acceptance-flows": {
    "flows": [
      {
        "name": "api-smoke",
        "command": "npm run smoke:api",
        "timeout": 120,
        "run_when": ["src/api/**/*"],
        "required": true
      }
    ]
  }
}
Field Type Description
flows object[] Acceptance commands to run after core packages pass
name string Stable flow identifier used in output and artifact filenames
command string Shell command run with bash -lc
timeout number Seconds before the flow is killed. Defaults to 120
run_when string[] Optional globs matched against files_touched; skip when nothing relevant changed
required boolean Defaults to true. Optional failures are reported but do not block completion
enabled boolean Defaults to true
tail_lines number Number of output lines to include in failure feedback. Defaults to 40

acceptance-flows runs in the post phase, so it only starts once quality-gates is satisfied.

loop-memory

Reads accumulated quality-gates data and injects predictive context at the start of each new session. It computes gate difficulty profiles, file-gate failure correlations, convergence shape, and oscillation patterns from historical pass traces and session summaries. At PreToolUse, it warns when Claude edits a file that has historically correlated with gate failures.

{
  "packages": ["quality-gates", "loop-memory"],
  "loop-memory": {
    "min_sessions": 3,
    "max_context_lines": 18,
    "lookback_sessions": 20,
    "correlation_threshold": 0.3,
    "enable_file_warnings": true
  }
}
Field Default Description
min_sessions 3 Minimum completed sessions before generating priors
max_context_lines 18 Cap on injected context lines at SessionStart
lookback_sessions 20 Number of recent sessions to analyze
correlation_threshold 0.3 Minimum failure rate for a file-gate correlation to be reported
enable_file_warnings true Emit PreToolUse context when editing historically correlated files

loop-memory runs in the core phase alongside quality-gates. It never blocks edits or forces loop continuation - it only injects advisory context.

Architecture

Two layers: a kernel and packages.

looper.json (user config)
    |
    v
  KERNEL  (loop mechanics, hook dispatch, state, circuit breakers)
    |
    v
  PACKAGES  (quality-gates, loop-memory, scope-guard, acceptance-flows, ...)
    |
    v
  Claude Code hooks  (SessionStart, PreToolUse, PostToolUse, Stop)

The kernel is registered via the plugin's hooks/hooks.json. It receives hook events from Claude Code and dispatches to package handlers. Packages define the behavior.

Kernel

Two files: kernel.sh (dispatcher) and pkg-utils.sh (state helpers). The kernel owns:

  • Iteration tracking and budget enforcement
  • Circuit breakers (stop_hook_active re-entry guard, budget cap)
  • Hook dispatch to package handlers
  • Package discovery and loading
  • Shared state: files_touched

Packages

A package is a directory with a manifest and optional handler scripts:

packages/quality-gates/
  package.json          # manifest
  hooks/
    session-start.sh    # SessionStart handler
    post-tool-use.sh    # PostToolUse handler
    stop.sh             # Stop handler
  lib/                  # helper scripts
  presets/              # stack-specific default configs

Convention over configuration: if hooks/stop.sh exists, the package handles Stop events. Missing handler = package has nothing to do for that event.

Package manifests can also declare an optional runtime requirement:

{
  "name": "scope-guard",
  "version": "1.0.0",
  "description": "Prevent edits outside a declared scope",
  "runtime": "deno",
  "phase": "post"
}

Supported runtime values:

Field Type Description
runtime string Optional package-level runtime contract. deno is supported today. Missing runtimes put the kernel into config_blocked and block edits until fixed.

Multi-Package Composition

Multiple packages run in declaration order. The kernel aggregates their stop/continue votes:

  • All packages must vote "done" (exit 0) for the loop to stop
  • Any package voting "continue" (exit 2) forces another iteration
  • A two-phase model (core and post) lets secondary checks run only after primary packages are satisfied
{
  "packages": ["quality-gates", "scope-guard"],
  "quality-gates": { "gates": [...] },
  "scope-guard": { "blocked": ["package-lock.json", ".env*"], "allowed": ["src/**/*"] }
}

Package Discovery

Packages are resolved from three search paths (first match wins):

  1. $CLAUDE_PROJECT_DIR/.claude/packages/<name>/ (project-local override)
  2. $HOME/.claude/packages/<name>/ (user-global)
  3. $CLAUDE_PLUGIN_ROOT/packages/<name>/ (bundled with the plugin)

Creating a Package

Minimal package (three files):

my-package/
  package.json
  hooks/
    stop.sh

package.json:

{
  "name": "my-package",
  "version": "1.0.0",
  "description": "Verify documentation accuracy"
}

hooks/stop.sh:

#!/usr/bin/env bash
set -euo pipefail
source "$LOOPER_HOOKS_DIR/pkg-utils.sh"

CMD=$(pkg_config '.verify_command // "echo ok"')
if output=$(eval "$CMD" 2>&1); then
  echo "Docs verified." >&2
  exit 0
else
  echo "Documentation issues:" >&2
  echo "$output" | tail -10 >&2
  exit 2
fi

Package Manifest

{
  "name": "quality-gates",
  "version": "2.0.0",
  "description": "Quality gate loop",
  "matchers": {
    "PreToolUse": "Edit|MultiEdit|Write",
    "PostToolUse": "Edit|MultiEdit|Write"
  },
  "phase": "core"
}
  • matchers: regex for tool name filtering. Absent = all tools.
  • phase: "core" (default) or "post". Post-phase packages only run after all core packages are satisfied.

Handler Environment

Every handler receives these environment variables:

Variable Value
LOOPER_PKG_NAME Package name
LOOPER_PKG_DIR Absolute path to package directory
LOOPER_PKG_STATE Absolute path to this package's state directory
LOOPER_STATE_DIR Absolute path to shared state root
LOOPER_HOOKS_DIR Absolute path to kernel hooks directory
LOOPER_CONFIG Absolute path to looper.json
LOOPER_ITERATION Current iteration number
LOOPER_MAX_ITERATIONS Budget cap
CLAUDE_PROJECT_DIR Project root

stdin: raw hook input JSON from Claude Code.

State Utilities

Source $LOOPER_HOOKS_DIR/pkg-utils.sh in your handlers:

kernel_read '.iteration'          # read kernel state (read-only)
kernel_read '.files_touched[]'

pkg_state_read '.scores'          # read own state
pkg_state_write '.last_score' '85'
pkg_state_append '.scores' '85'

pkg_read "other-pkg" '.satisfied' # read another package's state

pkg_config '.gates'               # read own config from looper.json

Circuit Breakers

  1. stop_hook_active: prevents infinite re-entry when Claude is pushed back on the same turn.
  2. iteration >= max_iterations: hard budget cap. PreToolUse also blocks edits when exhausted.
  3. All packages satisfied: all package stop handlers exit 0.

Feedback Channels

Hook When Channel Claude sees
SessionStart once stdout loop rules, package context
PreToolUse per tool JSON additionalContext "Pass 3/10. Editing: src/foo.ts"
PostToolUse per edit stdout per-file lint/type errors
Stop per attempt stderr gate results, failures, coaching

Session History

Each completed session appends a one-line JSON summary to .claude/state/sessions.jsonl. Budget-exhausted sessions are promoted to the log on the next SessionStart. Run /looper:status to view session history, aggregate stats, current config, and recommendation hints.

The log is local-only, gitignored, and contains: status, iterations, score, baseline savings, and timestamp. When recent history suggests a clear next move, Looper shows recommendations when the signal is clear: enable baseline, adjust max_iterations, or add scope-guard. The Stop hook also shows a short Suggestions: block during failing sessions when the signal is strong enough.

The quality-gates package also records per-pass traces in .claude/state/quality-gates/passes.jsonl. Each row captures the pass number, score, gate statuses, and the files edited during that pass. When a gate turns red after being green, or stays red across multiple passes, Stop feedback adds a short PROVENANCE: block showing when the failure first appeared and which files changed around it. /looper:status shows the same signal as Failure Introduction Points: for the most recent session.

If the generated config stops matching the repo, run /looper:doctor. It re-runs Looper's bootstrap detection, compares the proposed gates and checks to your current .claude/looper.json, and points you to /looper:looper-config when guided repair is the better move.

Built By

Claude & Srdjan

License

MIT

About

Looper - Extensible Native Improvement Loop for Claude Code

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors