Ralph is an autonomous AI coding loop CLI that runs Claude Code repeatedly until a task is complete. Named after Ralph Wiggum's naive, relentless persistence.
Package: @yazangineering/ralph on npm
Install: bun add -g @yazangineering/ralph
Run: ralph "Your prompt here"
- Runtime: Bun (build & dev), Node.js (production)
- UI Framework: Ink (React for terminal)
- Language: TypeScript (strict mode)
- CLI Parsing: Commander.js
- Build: Bun bundler (single-file output to
dist/cli.js)
src/
├── cli.tsx # Entry point, argument parsing (Commander.js)
├── app.tsx # Main Ink application, phase management
├── types/index.ts # All TypeScript interfaces and types
├── components/ # React/Ink UI components
│ ├── Splash.tsx # Startup splash screen
│ ├── Header.tsx # Status bar with model/iteration info
│ ├── IterationPanel.tsx # Progress and iteration display
│ ├── OutputPreview.tsx # 50-line rolling output preview
│ ├── TimingStats.tsx # Performance metrics
│ ├── Spinner.tsx # Loading indicator
│ └── Logger.tsx # Notification boxes
├── hooks/ # React hooks
│ ├── useClaudeLoop.ts # Core loop orchestration (most important)
│ ├── useTiming.ts # Performance tracking with deltas
│ ├── useOutputCapture.ts # Rolling output buffer
│ └── useExitHandler.ts # Safe exit with double Ctrl+C
└── lib/ # Utilities (non-React)
├── claude.ts # Claude CLI process spawning
├── promiseParser.ts # Promise tag parsing (NO context injection - canonical ralph)
├── headlessRunner.ts # TUI-free execution for AFK/background runs
├── history.ts # Save runs to ~/.ralph/history/
├── notifications.ts # Desktop notifications + sound
├── preflight.ts # Pre-flight validation checks
└── logger.ts # Colored console logging
Claude signals its state using semantic XML tags:
<promise>COMPLETE</promise> # Task finished successfully
<promise>BLOCKED: reason</promise> # Needs human intervention
<promise>DECIDE: question</promise> # Needs user decision
Parsed by src/lib/promiseParser.ts.
Ralph follows the canonical pattern where prompts are STATIC:
- No iteration numbers injected
- No PROJECT_ROOT injected
- No growing context
- Only the completion suffix is appended (to teach Claude about promise tags)
Why? LLMs get worse as context grows. By keeping prompts static and having Claude read/write state via files (progress.txt, task files), each iteration starts fresh with maximum cognitive capacity.
The TUI displays iteration progress for the USER - but this info is NOT sent to Claude.
See preparePrompt() in src/lib/promiseParser.ts.
Managed in src/app.tsx:
splash→starting→running→complete- Can also transition to:
paused,error
Defined in src/types/index.ts as LoopStatus:
idle,running,pausedcompleted,blocked,decidemax_reached,cancelled,error
# Development
bun run dev "test prompt" # Run without building
bun run build # Bundle to dist/cli.js
bun run typecheck # TypeScript validation
bun run lint # ESLint
bun run format # Prettier
# Testing
bun test # Run all tests
bun test --watch # Watch mode| File | Purpose |
|---|---|
src/hooks/useClaudeLoop.ts |
Core loop logic - start, pause, resume, stop |
src/lib/claude.ts |
Spawns claude CLI process, captures stdout/stderr |
src/lib/promiseParser.ts |
Parses completion tags (NO context injection) |
src/lib/headlessRunner.ts |
TUI-free execution mode |
src/app.tsx |
Main UI, phase transitions, keyboard handling |
src/types/index.ts |
All TypeScript interfaces |
Extract complex conditionals into named functions:
// Good
function getCompletionTitle(status: LoopStatus): string {
switch (status) {
case 'completed': return '✓ Task Complete!';
case 'max_reached': return '! Max Iterations Reached';
default: return '✕ Loop Stopped';
}
}
// Avoid nested ternaries in JSXAlways use callback form for state that depends on previous value:
setState((prev) => ({ ...prev, output: [...prev.output, chunk] }));Use refs for values that shouldn't trigger re-renders:
const isRunningRef = useRef(false);
const isPausedRef = useRef(false);Auto-publishes to npm on merge to master via GitHub Actions.
To release a new version:
- Update
versioninpackage.json - Commit and push to master
- GitHub Action builds and publishes if version is new
Default config in src/types/index.ts:
export const DEFAULT_CONFIG = {
maxIterations: 200,
unlimited: false, // Run indefinitely until completion
completionSignal: '<promise>COMPLETE</promise>',
model: 'opus',
dangerouslySkipPermissions: false,
verbose: false,
showSplash: true,
enableNotifications: true,
enableSound: true,
sandbox: false,
headless: false, // TUI-free mode for AFK/background runs
};| Option | Default | Description |
|---|---|---|
maxIterations |
200 | Max iterations (ignored if unlimited) |
unlimited |
false | Run until completion signal |
headless |
false | No TUI, console output only |
sandbox |
false | Run in Docker sandbox |
autoCommit |
true | Commit after each iteration |
Ralph automatically commits changes after each successful iteration (default: ON).
Related files:
src/lib/git.ts- Git operations (isGitRepo, getGitStatus, commitChanges)src/lib/promiseParser.ts- Parses<commit_message>tag from Claude output
Safety: Uses git add -u (not -A) to only stage modifications to tracked files, preventing accidental commits of .env files or other untracked secrets.
Commit message sources (in order of preference):
- Claude's
<commit_message>tag in output - Auto-generated from
git diff --stat
Run Claude inside a Docker container for isolated execution:
- Requires Docker Desktop 4.50+ with sandbox plugin
- Enable with
--sandboxflag - Uses
docker sandbox run --credentials hostto pass credentials - Related files:
src/lib/docker.ts,src/lib/prompt.ts
Runs are saved to ~/.ralph/history/{id}.json with full iteration records.
Runtime:
ink- React for CLIreact- UI frameworkcommander- CLI argument parsingchalk- Terminal colorsnode-notifier- Desktop notificationsbeeper- Sound alertsnanoid- ID generation
Dev:
typescript- Type checkingeslint- Lintingprettier- Formatting@types/*- Type definitions
Tests are in tests/ directory using Bun's test runner.
bun test # Run all
bun test promiseParser # Run specific file- Add to
program.option()insrc/cli.tsx - Add to
RalphConfiginterface insrc/types/index.ts - Add default to
DEFAULT_CONFIG - Use in
src/app.tsxor pass to hooks
- Create in
src/components/ - Export from
src/components/index.ts - Import and use in
src/app.tsx
Edit src/hooks/useClaudeLoop.ts:
runIteration()- Single iteration logicexecuteLoop()- Main loop control flow- Promise tag handling in the completion checks
For AFK/background operation, use --headless:
- No TUI, structured console output
- Stops on BLOCKED/DECIDE (no way to resume interactively)
- Exit codes: 0 (completed/cancelled), 1 (error)
- Implementation in
src/lib/headlessRunner.ts
Edit preparePrompt() in src/lib/promiseParser.ts.
Note: The canonical ralph pattern means prompts are STATIC. Only the completion suffix is appended to teach Claude about promise tags. No iteration numbers, no PROJECT_ROOT, no growing context.