ralph-loop – Deep-Dive-Guide
ralph-loop ist Anthropics offizielle Implementierung des Ralph-Wiggum-Patterns als Claude-Code-Plugin. Es nutzt einen Stop-Hook, der denselben Prompt erneut einspeist, sobald Claude die Session beenden will – kein externes Skript, kein zweites Terminal, keine Abhängigkeiten. Ein Plugin-Install und du hast eine autonome Schleife.
Repo: anthropics/claude-plugins-official → ralph-loop
1. Wie der Stop-Hook funktioniert
Die meisten Ralph-Varianten laufen als externer Bash-/Bun-/Go-Prozess, der den Agent in einer Schleife aufruft. ralph-loop ist anders: Der Loop läuft innerhalb der Claude-Code-Session.
┌─────────────────────────────────────────────────────────┐
│ Claude-Code-Session │
│ │
│ 1. /ralph-loop "…" startet die Session │
│ 2. Claude arbeitet am Prompt │
│ 3. Claude versucht, die Session zu beenden │
│ └─ Stop-Hook (stop-hook.sh) fängt den Exit ab │
│ 4. Hook speist den GLEICHEN Prompt erneut ein │
│ 5. Claude liest seinen eigenen Git-Diff + Log → weiter │
│ 6. Wiederholen bis: │
│ • Output enthält das Completion-Promise │
│ • ODER max-iterations erreicht │
│ • ODER /cancel-ralph aufgerufen │
└─────────────────────────────────────────────────────────┘
Da der Zustand im Filesystem und in der Git-History liegt, ist jeder Neustart kein „von vorn" – Claude liest, was bereits gebaut wurde, und macht dort weiter.
2. Plugin-Struktur
- README.md
3. Installation & Grundbedienung
# Plugin installieren
/plugin install ralph-loop
# Loop starten
/ralph-loop "<dein Prompt>" \
--completion-promise "COMPLETE" \
--max-iterations 50
# Loop jederzeit abbrechen
/cancel-ralph
Pflichtparameter
| Parameter | Bedeutung |
|---|---|
<prompt> | Die Aufgabe – wird in jeder Iteration erneut ausgeführt |
--completion-promise | String, den Claude ausgeben muss, um den Loop zu stoppen |
--max-iterations | Hartes Limit für Wiederholungen (immer setzen) |
Optionale Parameter
| Parameter | Standard | Bedeutung |
|---|---|---|
--branch | aktuell | Git-Branch für die Arbeit |
--no-commit | false | Auto-Commit pro Iteration deaktivieren |
--model | claude-sonnet-4-5 | Modell überschreiben |
4. Windows-Stolperfalle
Auf Windows kann der Stop-Hook mit wsl: Unknown key 'automount.crossDistro' oder execvpe(/bin/bash) failed scheitern.
Fix: ~/.claude/plugins/cache/.../hooks/hooks.json anpassen und den Hook-Command explizit auf Git Bash setzen:
"command": "\"C:/Program Files/Git/bin/bash.exe\" ${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh"
Git/bin/bash.exe verwenden (mit PATH-Wrappern), nicht Git/usr/bin/bash.exe (raw MinGW).
5. Prompt-Schreib-Handbuch
Das ist die Kernkompetenz. Ein ralph-loop-Prompt läuft dutzende Male. Ein vager Prompt produziert dutzende falsche Iterationen. Ein präziser Prompt liefert ein funktionierendes Ergebnis, meist in 5–15 Iterationen.
Aufbau eines guten ralph-loop-Prompts
[1] KONTEXT — was bereits existiert, welcher Stack, welche Constraints
[2] AUFGABE — was genau gebaut/geändert werden muss
[3] KRITERIEN — spezifische, testbare Abnahmekriterien
[4] VALIDIERUNG — der/die Befehl(e), die Erfolg beweisen
[5] MEMORY — Claude auffordern, AGENTS.md / progress.txt zu lesen
[6] ABSCHLUSS — wann das Promise ausgegeben werden soll
Nicht jeder Prompt braucht alle sechs, aber je komplexer die Aufgabe, desto mehr davon werden gebraucht.
6. Schlechte Prompts vs. gute Prompts
6.1 REST-API
Bau eine API für Benutzer.
Output COMPLETE wenn fertig.
Warum er scheitert:
Keine Endpunkte spezifiziert. Keine Authentifizierung. Keine Validierung. Keine Test-Anforderung. Claude produziert zwar etwas – aber jede Iteration produziert etwas anderes, und „fertig" ist undefiniert.
Stack: Laravel 11, Sanctum, PostgreSQL, PHPUnit.
Baue eine User-Management-REST-API:
POST /api/users — User anlegen (name, email, password; E-Mail eindeutig)
GET /api/users/{id} — User anzeigen (Auth erforderlich)
PUT /api/users/{id} — User updaten (Auth erforderlich, nur eigener User)
DELETE /api/users/{id} — Soft-Delete (Auth erforderlich, nur eigener User)
Abnahmekriterien:
1. Alle Routen antworten mit den korrekten HTTP-Status-Codes (201, 200, 403, 404).
2. Validierungsfehler geben 422 mit einem "errors"-Key zurück.
3. `php artisan test --filter UserApiTest` besteht mit ≥ 90 % Coverage auf dem Controller.
4. Eine OpenAPI-Doku wird unter /api/documentation erzeugt.
Vor jeder Iteration AGENTS.md lesen.
Wenn alle Kriterien erfüllt → <promise>COMPLETE</promise> ausgeben.
6.2 Laravel-Feature
Füge meiner Laravel-App Authentifizierung hinzu.
COMPLETE wenn fertig.
Warum er scheitert:
„Authentifizierung" könnte alles bedeuten: Sanctum, Passport, Breeze, Fortify, eigenes JWT. Keine Routen, keine Tests, kein Session-vs.-Token-Unterschied definiert.
Stack: Laravel 11, Sanctum, MySQL, Pest.
Token-basierte API-Authentifizierung einbauen:
POST /api/auth/register — name, email, password; gibt Bearer-Token zurück
POST /api/auth/login — email, password; gibt Bearer-Token zurück
POST /api/auth/logout — macht aktuellen Token ungültig (auth:sanctum)
GET /api/auth/me — gibt den authentifizierten User zurück (auth:sanctum)
Regeln:
- E-Mail muss eindeutig sein; doppelte Registrierung → 409.
- Passwort: min. 8 Zeichen, mindestens ein Großbuchstabe, eine Ziffer.
- Tokens laufen nach 24 h ab (in Sanctum-Config setzen).
- Bestehende Routen oder Migrationen NICHT anfassen.
Tests: `php artisan test --filter AuthTest` muss bestehen (Test erstellen falls nicht vorhanden).
Vor jeder Iteration AGENTS.md lesen.
Wenn alle Tests grün → <promise>COMPLETE</promise> ausgeben.
6.3 Laravel-Refactoring
Refactore den UserController, damit er sauberer wird.
Output COMPLETE wenn fertig.
Warum er scheitert:
„Sauberer" ist subjektiv. Der Agent wird endlos refactoren und in der nächsten Iteration seine eigene Arbeit wieder rückgängig machen.
Refactore UserController (app/Http/Controllers/UserController.php) nach diesen Regeln:
1. Business-Logik in UserService auslagern (app/Services/UserService.php).
2. Validierungsregeln in UserRequest auslagern (app/Http/Requests/UserRequest.php).
3. Jede Controller-Methode darf max. 10 Zeilen haben.
4. Keine direkten Eloquent-Calls im Controller – nur über UserService.
5. Alle bestehenden Tests in tests/Feature/UserTest.php müssen weiterhin bestehen.
Die öffentlichen Methodensignaturen von UserController NICHT ändern.
Keine neuen Routen anlegen.
Validierungsbefehl: `php artisan test --filter UserTest`
Wenn alle Tests bestehen und der Controller alle 5 Regeln erfüllt → <promise>COMPLETE</promise> ausgeben.
6.4 TypeScript / Node.js API
Erstelle eine Todo-API in TypeScript.
Sag DONE wenn fertig.
Warum er scheitert:
Framework? Validierung? Datenbank? Tests? Fehlerbehandlung? Alles undefiniert.
Stack: Hono, Zod, Drizzle ORM, SQLite (Dev), Vitest.
Todo-API bauen:
POST /todos — { title: string (min 3), done?: boolean }
GET /todos — alle Todos auflisten, optional ?done=true|false Filter
GET /todos/:id — einzelnes Todo oder 404
PATCH /todos/:id — partielles Update { title?, done? }
DELETE /todos/:id — 204 oder 404
Regeln:
- Alle Eingaben mit Zod validieren; ungültig → 400 mit { error, details }.
- Timestamps: created_at, updated_at (ISO 8601 in Responses).
- Drizzle-Migration in /drizzle/migrations/.
- Tests: `pnpm vitest run` muss bestehen; ≥ 80 % Branch-Coverage auf den Route-Handlern.
Vor jeder Iteration AGENTS.md lesen.
Wenn alle Tests bestehen → <promise>COMPLETE</promise> ausgeben.
6.5 Business-Ziel / Performance
Mach die App schneller.
Output COMPLETE wenn sie schnell ist.
Warum er scheitert:
„Schneller" ist nicht messbar. Der Loop weiß nie, wann er fertig ist.
Performance-Ziel: Antwortzeit von GET /api/products (bei 10.000 Zeilen) von aktuell ~1.200 ms auf < 200 ms reduzieren.
Kontext:
- Stack: Laravel 11, PostgreSQL, Redis vorhanden.
- Aktuelle Query: Product::with('category','tags')->paginate(20) — keine Indizes.
Erlaubte Änderungen:
1. DB-Indizes über eine neue Migration hinzufügen.
2. Redis-Caching mit 5-Minuten-TTL auf das Query-Ergebnis implementieren.
3. Eager Loading durch gezieltes select() ersetzen falls nötig.
Bestehende Tests oder Response-Shape NICHT ändern.
Validierung:
`php artisan tinker --execute="echo app(\App\Services\ProductBenchmark::class)->measure();"``
muss eine Zahl unter 200 ausgeben.
Wenn der Benchmark < 200 liefert → <promise>COMPLETE</promise> ausgeben.
6.6 Hosting / Infrastruktur / DevOps
Füge Docker zu diesem Projekt hinzu.
COMPLETE wenn fertig.
Warum er scheitert:
Dev-Docker oder Prod-Docker? Welche Services? Welche Ports? Welche Umgebungsvariablen?
Docker Compose für lokale Entwicklung dieser Laravel-11-App einrichten.
Benötigte Services:
app — PHP 8.3-FPM, intern auf Port 9000
nginx — bedient app auf Host-Port 8080, Config in docker/nginx/default.conf
db — Postgres 16, user=laravel, password=secret, db=laravel, Port 5432
redis — Redis 7, Port 6379
Zu erstellende Dateien:
Dockerfile — php:8.3-fpm-alpine, composer install, APP_ENV=local
docker-compose.yml — alle vier Services mit Named Volumes
docker/nginx/default.conf
Regeln:
- `docker compose up -d` muss alle Services ohne Fehler starten.
- `docker compose exec app php artisan migrate --force` muss erfolgreich laufen.
- Keine bestehenden Quelldateien dürfen verändert werden.
- .env.example muss Einträge für DB_HOST=db, REDIS_HOST=redis erhalten.
Validierung: `docker compose up -d && sleep 5 && curl -s http://localhost:8080 | grep -q "Laravel" && echo OK`
Wenn Validierung OK ausgibt → <promise>COMPLETE</promise> ausgeben.
6.7 KI-Agent / Prompt Engineering
Bau einen KI-Agent, der Fragen zu unserer Doku beantworten kann.
COMPLETE wenn fertig.
Warum er scheitert:
Welches LLM? Welches Retrieval? Welche Tools? Keine Evaluierungskriterien. Kein Test für „es funktioniert."
RAG-Agent für unsere Markdown-Dokumentation (docs/-Ordner) bauen.
Stack: Python 3.12, LangChain 0.3, OpenAI (gpt-4o-mini für Embeddings + gpt-4o für Antworten), Chroma (lokal).
Schritte:
1. Alle .mdx/.md-Dateien in docs/ rekursiv chunken (chunk_size=800, overlap=80).
2. Via text-embedding-3-small embedden, in chroma_db/ persistieren (lokal).
3. Bei Anfrage: Top-5-Chunks holen, als Kontext an gpt-4o übergeben, Antwort + Quellen ausgeben.
4. CLI: `python agent.py "Was ist ralph-loop?"` → Antwort gedruckt, Quellen aufgelistet.
Abnahmekriterien:
1. `python agent.py "Was ist das Completion-Promise bei ralph-loop?"` gibt eine Antwort zurück, die das Wort „COMPLETE" enthält.
2. `python agent.py "Was ist snarktank/ralph?"` gibt eine Antwort zurück, die „Bash" erwähnt.
3. `pytest tests/test_agent.py -v` besteht (Tests schreiben falls nicht vorhanden).
Vor jeder Iteration AGENTS.md lesen.
Wenn alle drei Kriterien erfüllt → <promise>COMPLETE</promise> ausgeben.
6.8 Frontend / React-Komponente
Bau eine Datentabellen-Komponente in React.
COMPLETE wenn sie gut aussieht.
Warum er scheitert:
„Gut aussieht" ist kein Test. Ralph kann keinen visuellen Check durchführen. Jede Iteration produziert eine andere UI und der Loop endet nie.
Stack: React 19, TypeScript, TanStack Table v8, Tailwind CSS 4, Vitest + Testing Library.
<DataTable<T>>-Komponente in src/components/DataTable.tsx bauen:
Props:
data: T[]
columns: ColumnDef<T>[]
pageSize?: number (Standard 20)
onRowClick?: (row: T) => void
Features:
- Client-seitige Paginierung (pageSize Zeilen pro Seite, Zurück/Weiter-Buttons)
- Klick auf Spaltenheader → aufsteigend sortieren; zweiter Klick → absteigend
- Suchinput über der Tabelle filtert alle String-Spalten (case-insensitiv)
- Leerzustand: „Keine Ergebnisse gefunden" zentriert im Tabellenkörper
Tests in src/components/DataTable.test.tsx:
1. Rendert die korrekte Anzahl Zeilen pro Seite.
2. Sortierung auf Spalte „name": aufsteigende Reihenfolge entspricht Array.sort().
3. Suche „alice" mit Fixture-Daten gibt nur die Alice-Zeile zurück.
4. onRowClick wird mit der korrekten Zeile aufgerufen beim Klick.
`pnpm vitest run DataTable` muss alle 4 Tests bestehen.
TypeScript muss ohne Fehler kompilieren: `pnpm tsc --noEmit`.
Vor jeder Iteration AGENTS.md lesen.
Wenn Tests + Typecheck bestehen → <promise>COMPLETE</promise> ausgeben.
6.9 Datenbank-Migration
Füge eine Payments-Tabelle hinzu.
COMPLETE wenn fertig.
Warum er scheitert:
Welche Spalten? Welche Foreign Keys? Welche Indizes? Welche Validierung im Model?
Stack: Laravel 11, PostgreSQL.
Payments-Tabelle über eine neue Migration erstellen:
Spalten:
id uuid, Primary Key
order_id uuid, Foreign Key → orders.id (Cascade Delete)
amount decimal(10,2), not null
currency char(3), not null, Standard 'EUR'
status enum('pending','completed','failed','refunded'), Standard 'pending'
provider varchar(50), not null (z. B. "stripe", "paypal")
provider_ref varchar(255), nullable, unique
paid_at timestamp, nullable
created_at / updated_at timestamps
Indizes:
- orders(order_id)
- (status, created_at) Composite
- provider_ref unique
Model: app/Models/Payment.php
- $guarded = []
- Casts: amount → decimal:2, status → enum, paid_at → datetime
- Beziehung: belongsTo(Order::class)
- Scopes: scopePending(), scopeCompleted()
Tests: `php artisan test --filter PaymentTest` muss bestehen (Test erstellen).
`php artisan migrate --pretend` muss ohne Fehler laufen.
Wenn Migration + Tests bestehen → <promise>COMPLETE</promise> ausgeben.
6.10 Ziel ohne Code – Planung / Recherche
Schreibe ein technisches Spec für unser neues Feature.
COMPLETE wenn es fertig ist.
Warum er scheitert:
Keine Länge, keine Pflichtabschnitte, keine Definition von „fertig." Der Agent gibt in der ersten Iteration etwas Beliebiges aus und gibt sofort COMPLETE aus.
Technisches Spec für ein „Warteliste"-Feature in SPEC.md schreiben.
Pflichtabschnitte (alle müssen vorhanden und nicht leer sein):
## Überblick — 2–3 Sätze, das User-Problem
## User-Stories — ≥ 3 Abnahmekriterien-Geschichten
## Datenmodell — Tabellendefinitionen mit Spaltenname und Typ
## API-Endpunkte — HTTP-Methode, Pfad, Request-Body, Response
## Edge-Cases — ≥ 5 konkrete Edge-Cases mit Lösung
## Nicht im Scope — explizite Liste was NICHT enthalten ist
## Offene Fragen — ≥ 2 ungelöste Entscheidungen, die ein Mensch treffen muss
Regeln:
- Nur das Warteliste-Feature beschreiben; kein anderes Feature specen.
- Keinen Code schreiben.
- Gesamtlänge: 400–800 Wörter.
Validierung: `wc -w SPEC.md` muss eine Zahl zwischen 400 und 800 ausgeben.
Alle 7 Abschnitte müssen vorhanden sein: `grep -c "^## " SPEC.md` muss 7 ausgeben.
Wenn beide Validierungsbefehle bestehen → <promise>COMPLETE</promise> ausgeben.
7. Prompt-Checkliste
Vor dem Starten eines Loops diese Fragen mit Ja beantworten:
- Stack angegeben — Sprache, Framework, Version, Test-Tool
- Scope begrenzt — was NICHT geändert werden darf, ist explizit genannt
- Abnahmekriterien sind Aussagen, keine Adjektive — „Antwortzeit < 200 ms" nicht „schnell"
- Ein Shell-Befehl beweist Completion — Tests, grep, curl, wc
- AGENTS.md referenziert — „Vor jeder Iteration AGENTS.md lesen" im Prompt
- Completion-Promise ist eindeutig —
<promise>COMPLETE</promise>, nicht „fertig" oder „done" -
--max-iterationsgesetzt — verhindert endlose Loops bei kaputten Tasks
8. Token- und Kostenkontrolle
| Technik | Ersparnis |
|---|---|
--model claude-haiku-4-5 für Explorations-Iterationen, Sonnet/Opus nur für finale | 5–10× günstiger pro Iteration |
--max-iterations 10 beim ersten Lauf; nur bei Bedarf erhöhen | Verhindert unkontrollierte Kosten |
| Prompt unter 2.000 Token halten | Kürzerer Kontext = günstiger pro Aufruf |
--no-commit + später squashen | Saubere History, kein Kosteneinfluss |
| Große Features in 3–5 kleinere Loops aufteilen | Schnellere Konvergenz, mehr Kontrolle |
9. ralph-loop mit anderen Tools kombinieren
# 1. Mit snarktank/ralph prd.json aus einer Konversation generieren
/skill prd
# 2. Die Stories mit ralph-loop abarbeiten
/ralph-loop "$(cat prd.json)" --completion-promise "COMPLETE" --max-iterations 30
# 3. Ergebnis zur automatisierten Review an ralphex übergeben
ralphex --review --branch feature/mein-feature
# 4. Für parallele Tasks auf demselben Ergebnis → ralphy
ralphy --prd prd.json --parallel --max-parallel 3 --create-pr
Den empfohlenen Tages-Stack findest du in der Ralph-Übersicht.