Usage Guide

Prerequisites

  • Node.js 18+ and npm installed on your host machine
  • VS Code with the Dev Containers extension and Docker / Colima (optional for isolation)
  • A GitHub account with CLI logged in (optional — only needed for remote push)

Getting Started

Create a new empty project folder, open a terminal inside it, then run:

mkdir my-project && cd my-project
npx ralph-workflow

The wizard will ask, in order:

  1. AI CLI — pick claude, codex, gemini, or opencode. This choice drives the matching rtk init flag below.
  2. Dev Container isolation — set up a VS Code Dev Container so the host's credentials are not forwarded in.
  3. GitHub access — optionally create a fine-grained PAT scoped to one repo. This is now independent of the Dev Container choice, so you can have isolation without a PAT, a PAT without isolation, both, or neither.
  4. Claude extras (only if you picked claude) — opt in to the Caveman debugging plugin and/or the awesome-claude-code-subagents collection. Both default to no.
  5. Dev Container template (only if isolation is enabled) — choose Node, Python, Ubuntu, etc.

If scripts/ already exists in your project, the scaffold step warns you and asks for confirmation before overwriting anything.

Set Up Isolation via Dev Container (Optional)

You will be prompted whether you want to set up a VS Code Dev Container for isolation. If yes, you can select from available Dev Container templates (e.g. Node, Python, Ubuntu). Git access is configured before devcontainer.json is created.

After the wizard completes, open the folder in VS Code and reopen it in a container:

  1. Open the project folder in VS Code.
  2. Install the Dev Containers extension if not already installed.
  3. Click the >< icon in the bottom-left corner, then select Reopen in Container (or use Cmd+Shift+P → Dev Containers: Reopen in Container).

A postStart script runs automatically to clean up any credentials VS Code may have forwarded from your host machine. VS Code's Dev Containers extension automatically forwards git credentials by default. By default ralph-workflow applies aggressive cleanup so you are explicitly in control of what credentials Ralph can access.

Configuring Git Granular Access (Optional)

Giving Ralph access to a remote GitHub repository is optional — Ralph works fine with local git only. If you want Ralph to push to a remote repo, set up a fine-grained PAT during the wizard:

  1. The wizard creates a GitHub repo using your current local git credential (you can retry or skip if it fails).
  2. The shell provides a GitHub link to create a PAT with the recommended minimum permissions pre-filled in the URL parameters.
  3. Open the link, manually select the repository you want Ralph to access, and generate the token.
  4. Paste the token back into the shell — it is written to .ralph/token inside your project directory (mode 0600) and .ralph/ is added to .gitignore automatically so the token is never committed.

This gives Ralph access to only that one repo with granular permissions, reducing blast radius if something goes wrong.

The token is mounted into the container via ${localWorkspaceFolder}/.ralph/token, a devcontainer variable that resolves correctly on macOS, Linux, and Windows — no OS-specific paths are baked into your devcontainer.json. Each project also gets its own isolated Docker volume for gh and Claude config, so switching between multiple ralph projects never overwrites each other's auth state.

(Important) Setting Up Your AI CLI

ralph-workflow is designed to automatically install your chosen AI CLI in the container. You must configure it once before Ralph can run it unattended.

For Claude Code:

# Inside the container terminal:
claude                                     # log in
claude --dangerously-skip-permissions      # accept the warning once

The --dangerously-skip-permissions acceptance only needs to happen once per container.

RTK and Claude-only Extras

Every generated devcontainer.json auto-installs RTK and runs the per-CLI rtk init variant via postCreateCommand. The install script is pinned to a specific commit SHA to avoid supply-chain drift, and the init command is idempotent — re-running a container rebuild won't duplicate anything.

# Per-CLI rtk init selected automatically
claude    → rtk init -g
codex     → rtk init -g --codex
gemini    → rtk init -g --gemini
opencode  → rtk init -g --opencode

If you picked claude as your CLI, two optional extras are available:

  • Caveman — the Caveman plugin is registered and installed via claude plugin install caveman@caveman. Guarded so an existing install in the .claude volume is not reinstalled on rebuild.
  • awesome-claude-code-subagents — a shallow clone of VoltAgent's curated list into /home/vscode/.claude/agents/awesome-subagents, which lives inside the project-scoped .claude Docker volume and persists across rebuilds.

Both extras default to no and are gated on cliName === 'claude'; the prompts are simply skipped for other CLIs.

Writing prd.yaml Stories

Open scripts/ralph/prd.yaml and define your stories. Use your AI assistant or write them manually:

branchName: ralph/feature
userStories:
  - id: US-001
    title: Add login form
    acceptanceCriteria:
      - Email/password fields
      - Validates email format
      - typecheck passes
    priority: 1
    passes: false
    notes: ""
  • branchName — Ralph will commit to this branch.
  • priority — lower number = implemented first.
  • passes — Ralph sets this to true when the story is complete.
  • acceptanceCriteria — concrete, testable conditions Ralph must satisfy.

Defining Project Direction with main_prd.md (Optional)

A main_prd.md template is scaffolded for you. It acts as your "product manager" document — containing basic guidelines, user identification, requirements, and high-level objectives.

If you have rough ideas, ask your AI assistant to help turn them into a main_prd.md. Polish this document yourself before running Ralph so it doesn't build in the wrong direction. If you already have clear requirements or just want to dive in, you can ignore this file.

Once your direction is clear, break goals into smaller stories in scripts/ralph/prd.yaml.

Seeding progress.txt

Before the first run, seed scripts/ralph/progress.txt with codebase context. Ralph appends a new entry after every story — patterns accumulate across iterations and are visible on the next run.

# Ralph Progress Log

## Codebase Patterns
- Migrations: IF NOT EXISTS
- Types: Export from actions.ts

## Key Files
- db/schema.ts
- app/auth/actions.ts
---

Running ralph.sh

Inside the container terminal, make the script executable and run it:

chmod +x scripts/ralph/ralph.sh
./scripts/ralph/ralph.sh 25   # up to 25 iterations

Each iteration Ralph will:

  1. Read prd.yaml for the next incomplete story.
  2. Read progress.txt for accumulated codebase patterns.
  3. Implement the story, run typechecks and tests.
  4. Commit the change.
  5. Mark the story passes: true and append learnings to progress.txt.
  6. Output <promise>COMPLETE</promise> when all stories are done.

The loop exits with code 0 on completion, or code 1 if MAX_ITERATIONS is reached first.

Scaffolded Files

scripts/ralph/
├── ralph.sh          # the iteration loop
├── prompt.md         # per-iteration instructions for Claude
├── prd.yaml          # your task list (edit this before running)
└── progress.txt      # accumulated learnings (start with codebase context)

The prompt.md instructs Ralph on each iteration:

# Ralph Agent Instructions

## Your Task

1. Read `scripts/ralph/prd.yaml`
2. Read `scripts/ralph/progress.txt`
3. Pick highest priority story where `passes: false`
4. Implement that ONE story
5. Run typecheck and tests
6. Commit: `feat: [ID] - [Title]`
7. Update prd.yaml: `passes: true`
8. Append learnings to progress.txt

Using pnpm in Dev Containers

If your project uses pnpm, you'll hit a common friction point: every time the container is rebuilt, pnpm re-downloads and reinstalls all packages from scratch. Dev Containers are ephemeral — the container filesystem is discarded on rebuild, so nothing innode_modules or the pnpm store persists between rebuilds unless you explicitly mount a volume.

Option 1 — Named Docker Volume (easiest)

Mount a named volume for both the pnpm store and node_modules. Docker manages the volume lifecycle; it survives rebuilds automatically.

# Inside .devcontainer/devcontainer.json
"mounts": [
  "source=project-pnpm-store,target=/home/node/.local/share/pnpm/store,type=volume",
  "source=project-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
],
"postCreateCommand": "pnpm install"

Option 2 — Bind Mount Your Host's pnpm Store

Reuse the pnpm store that already lives on your host machine. This means packages already cached on the host are available immediately inside the container — no re-download needed.

# Inside .devcontainer/devcontainer.json
"mounts": [
  "source=${localEnv:HOME}/.local/share/pnpm/store,target=/home/node/.pnpm-store,type=bind,consistency=cached"
],
"postCreateCommand": "pnpm config set store-dir /home/node/.pnpm-store && pnpm install"

Key Considerations

  • Hard link limitations — pnpm normally saves disk space by hard-linking packages from its store into node_modules. Hard links only work within the same filesystem. When the pnpm store and node_modules are on different mounts inside the container, pnpm falls back to copying files instead. To avoid this, keep both on the same mount point.
  • Permissions — if you bind-mount a host directory, the container user (usually node or vscode) must own it. Add a postCreateCommand to fix ownership if needed:
    sudo chown -R node:node /home/node/.pnpm-store
  • Named volume vs bind mount — named volumes are simpler and require no host-side setup, but you lose visibility into the store contents. Bind mounts give you full access from the host but require the host path to exist and have correct permissions.

Tips

Running Docker

Use Colima to provide a Docker runtime — it's open source and commercially safe. Docker Desktop works too, but be aware of its commercial licensing terms.

Container Resource Defaults

When running in container mode, resources are often restricted by default. It's recommended to bump up CPU and memory allocation to prevent a slow isolated development environment.

Honestly, we all need to learn from Ralph. It may be clueless, but it is very persistent.