A GitHub Action that implements the Ralph loop pattern: iterative work/review/ship cycles on GitHub issues using Claude Code CLI.
When you label an issue, Ralph:
- Creates a branch and reads the issue description
- Runs a worker agent that writes code to address the issue
- Runs a reviewer agent that evaluates the changes
- If the reviewer says REVISE, loops back to step 2 with feedback
- If the reviewer says SHIP and
security_gate_enabledistrue(default):- Runs a security gate agent that audits the diff for vulnerabilities
- If the gate says FAIL, loops back to step 2 with security findings as feedback
- If the gate says PASS, pushes the branch and opens a PR
- If
security_gate_enabledisfalse, ships immediately after the reviewer approves
-
Add
ANTHROPIC_API_KEYas a repository secret -
Create
.github/workflows/ralph.yml:
name: Ralph Loop
on:
issues:
types: [labeled, edited]
issue_comment:
types: [created]
pull_request:
types: [labeled]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
reject-pr:
if: github.event_name == 'pull_request' && github.event.label.name == 'ralph'
runs-on: ubuntu-latest
steps:
- name: Comment on PR
env:
GH_TOKEN: ${{ github.token }}
run: |
existing="$(gh pr view "${{ github.event.pull_request.number }}" \
--repo "${{ github.repository }}" \
--json comments --jq '.comments[].body' \
| grep -c '🤖 \*\*Ralph\*\*' || true)"
if [[ "${existing}" -eq 0 ]]; then
gh pr comment "${{ github.event.pull_request.number }}" \
--repo "${{ github.repository }}" \
--body "🤖 **Ralph** can only work on issues, not pull requests. Please create an issue and label it with \`ralph\` instead."
fi
ralph:
if: >-
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'ralph') ||
(github.event_name == 'issues' && github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'ralph')) ||
(github.event_name == 'issue_comment' && github.event.action == 'created' && contains(github.event.issue.labels.*.name, 'ralph') && github.event.comment.user.type != 'Bot' && !contains(github.event.comment.body, '<!-- ralph-comment-') && !github.event.issue.pull_request) ||
(github.event_name == 'issue_comment' && github.event.action == 'created' && github.event.issue.pull_request && (github.event.comment.body == '/ralph-review' || startsWith(github.event.comment.body, '/ralph-review ')) && github.event.comment.user.type != 'Bot')
runs-on: ubuntu-latest
timeout-minutes: 60
concurrency:
group: ralph-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
# Pin to an immutable SHA for supply chain security (SLSA/SSDF compliance).
# To find the SHA: gh release view v1 --repo mdelapenya/claude-ralph-github-action --json targetCommitish
# Example: mdelapenya/claude-ralph-github-action@abc1234def5678 # v1
- uses: mdelapenya/claude-ralph-github-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}- Create a
ralphlabel in your repository - Label any issue with
ralphto trigger the loop
| Input | Required | Default | Description |
|---|---|---|---|
anthropic_api_key |
Yes | — | Anthropic API key for Claude CLI |
github_token |
No | ${{ github.token }} |
GitHub token for PR and issue operations |
worker_model |
No | sonnet |
Claude model for the worker phase |
reviewer_model |
No | sonnet |
Claude model for the review phase |
max_iterations |
No | 5 |
Maximum number of work/review cycles |
max_turns_worker |
No | (Claude CLI default) | Maximum agentic turns per worker invocation |
max_turns_reviewer |
No | (Claude CLI default) | Maximum agentic turns per reviewer invocation |
trigger_label |
No | ralph |
Issue label that triggers the loop |
base_branch |
No | — | Branch to create the PR against (auto-detected from repository default branch if not specified) |
worker_allowed_tools |
No | Bash,Read,Write,Edit,Glob,Grep,Task,WebFetch,WebSearch |
Comma-separated tools the worker can use |
reviewer_tools |
No | Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,Task |
Comma-separated tools the reviewer can use |
merge_strategy |
No | pr |
Merge strategy: pr (create a pull request) or squash-merge (squash and push directly to default branch) |
default_branch |
No | — | Default branch to merge into when using squash-merge strategy (auto-detected from repo if not specified) |
worker_tone |
No | — | Personality/tone for the worker agent (e.g., "pirate", "formal", "enthusiastic"). If set, the worker will respond with this personality |
reviewer_tone |
No | — | Personality/tone for the reviewer agent (e.g., "pirate", "formal", "enthusiastic"). If set, the reviewer will respond with this personality |
commit_author_name |
No | claude-ralph[bot] |
Git author name for commits |
commit_author_email |
No | claude-ralph[bot]@users.noreply.github.com |
Git author email for commits |
ralph_review_command |
No | /ralph-review |
Slash command to trigger a re-review run on a Ralph-created PR (e.g., /ralph-review focus on tests) |
security_gate_enabled |
No | true |
Enable the security gate. Set to false to skip the security audit before shipping (not recommended for production) |
security_gate_model |
No | sonnet |
Claude model for the security gate phase |
max_turns_security_gate |
No | (Claude CLI default) | Maximum agentic turns per security gate invocation |
security_gate_tools |
No | Bash,Read,Write,Glob,Grep |
Comma-separated tools the security gate can use |
security_gate_tone |
No | — | Personality/tone for the security gate agent (e.g., "Agent Smith", "HAL 9000") |
| Output | Description |
|---|---|
pr_url |
URL of the created/updated pull request, or merge commit SHA when using squash-merge |
iterations |
Number of work/review iterations completed |
final_status |
SHIPPED, MAX_ITERATIONS, or ERROR |
Ralph creates a .ralph/ directory in the working tree (never committed to the branch) to pass state between agents:
task.md— The issue title and bodypr-info.txt— Repo, branch, issue title, and existing PR number (if any)work-summary.txt— Worker's summary of changes madereview-result.txt—SHIPorREVISEreview-feedback.txt— Reviewer's feedback for the next iterationpr-title.txt— PR title in conventional commits format (set by reviewer)iteration.txt— Current iteration numberissue-number.txt— GitHub issue number (set once at startup)security-result.txt—PASSorFAIL(written by security gate; defaults toFAILif missing)security-feedback.txt— Security gate findings for the worker (present only onFAIL)final-status.txt— Final loop outcome:SHIPPED,MAX_ITERATIONS, orERRORpush-error.txt— Last push failure details (cleared on success)audit.log— Append-only structured log of all phase transitions*.sha256— SHA-256 checksum sidecars for tamper-detection onreview-result.txtandsecurity-result.txt
The worker agent merges the base branch (resolving any conflicts), implements the task, and commits changes directly. The reviewer agent evaluates the changes, runs tests and linters independently, and decides whether to SHIP or REVISE. When the reviewer decides SHIP, the security gate performs an independent read-only audit of the branch diff before the loop exits. If the worker makes no commits in an iteration, the loop continues to the next iteration with feedback instead of aborting.
The security gate is a separate Claude agent that runs after every SHIP decision. It audits the branch diff for:
- Secrets: hardcoded API keys, tokens, passwords (including in git history)
- Injection: command injection, SQL injection, path traversal, XSS, SSRF
- Auth: missing authentication, broken access control, insecure session management
- Cryptography: broken algorithms (MD5, SHA-1, DES), disabled TLS verification
- Shell safety: unquoted variables,
evalwith external input, insecure temp files - Dependencies: unpinned versions, known-vulnerable packages
- Information disclosure: stack traces, debug endpoints, sensitive data in logs
- Privilege: world-writable files, unnecessary root operations
Any finding of MEDIUM severity or higher writes FAIL and forces another worker iteration with the findings as feedback. The gate defaults to FAIL if it crashes or produces no output (fail-safe). It also detects prompt injection attempts in any file it reads and treats them as CRITICAL findings.
Disable with security_gate_enabled: false. See SECURITY.md for the full threat model.
PR titles follow conventional commits format. The reviewer agent infers the type from the changes and sets the title:
feat: add input validation to entrypoint
fix: resolve git safe directory error
chore: update dependencies
refactor: simplify state management
Supported types: feat, fix, chore, refactor, docs, test, style, perf, build, ci, revert.
If a PR already exists (on re-runs), the reviewer updates the title directly via gh pr edit. On the first run, the reviewer writes the title to .ralph/pr-title.txt and the orchestration uses it when creating the PR.
Ralph triggers in four ways:
- Label added: When the
ralphlabel is added to an issue (first run or re-trigger by removing and re-adding the label). - Issue edited: When an issue that already has the
ralphlabel is edited (title or body changed). This lets you refine requirements and have Ralph re-process the updated task. - Comment added: When a new comment is posted on an issue that has the
ralphlabel. This enables a conversational workflow where you can give Ralph follow-up instructions via comments. Ralph's own comments (identified by<!-- ralph-comment-* -->markers) do not retrigger the workflow. This works with the standardGITHUB_TOKEN— no PAT is required. /ralph-reviewon a Ralph PR: When you post/ralph-reviewas a comment on a Ralph-created pull request, Ralph re-runs the loop incorporating all PR review feedback. See PR Review Workflow below.
In all cases, Ralph detects the existing branch if one exists, checks it out, and continues from where it left off. The worker re-reads the task from the issue (which may have changed) and the branch's commit history to understand what was already done. New commits are added on top — Ralph never force-pushes.
Ralph supports two merge strategies:
Creates or updates a pull request. The PR remains open for human review and must be manually merged. This is the recommended approach for most use cases.
When the reviewer approves (SHIP), Ralph squashes all commits into a single commit and pushes directly to the default branch. The issue is automatically closed. The commit message uses the PR title set by the reviewer (in conventional commits format).
Example workflow configuration for squash-merge:
# Pin to an immutable SHA for supply chain security (SLSA/SSDF compliance).
# To find the SHA: gh release view v1 --repo mdelapenya/claude-ralph-github-action --json targetCommitish
# Example: mdelapenya/claude-ralph-github-action@abc1234def5678 # v1
- uses: mdelapenya/claude-ralph-github-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
merge_strategy: squash-mergeNote: With squash-merge, if the reviewer requests revisions, max iterations is reached, or the squash-merge fails for any reason, Ralph falls back to creating a PR for human review.
Security consideration: The squash-merge strategy pushes directly to the default branch, bypassing pull request reviews and any branch protection rules. Only use this for low-risk, well-scoped tasks where you trust the automated review process.
You can configure the personality and tone of both the worker and reviewer agents. This allows agents to communicate in a specific style while still performing their tasks correctly.
Example workflow configuration:
# Pin to an immutable SHA for supply chain security (SLSA/SSDF compliance).
# To find the SHA: gh release view v1 --repo mdelapenya/claude-ralph-github-action --json targetCommitish
# Example: mdelapenya/claude-ralph-github-action@abc1234def5678 # v1
- uses: mdelapenya/claude-ralph-github-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
worker_tone: "pirate"
reviewer_tone: "professional and concise"When tone is configured, the agent will respond with that personality throughout its work. For example, a worker with worker_tone: "pirate" might write commit messages and summaries in pirate speak, while still producing correct, functional code.
Use cases:
- Fun team projects where personality adds engagement
- Formal corporate environments requiring professional tone
- Educational contexts where enthusiastic encouragement is helpful
The tone instruction is appended to the system prompt, so agents maintain their core capabilities while adopting the requested personality.
Ralph requires the following GitHub Actions permissions:
contents: write— Required to create branches, commit changes, and push codepull-requests: write— Required to create and update pull requestsissues: write— Required to comment on issues
By default, the GITHUB_TOKEN cannot modify workflow files in .github/workflows/. This is a GitHub security restriction that cannot be overridden via the permissions block (workflows is not a valid permission scope).
If you need Ralph to edit workflow files, use a Personal Access Token (PAT) with the workflow scope:
- Create a fine-grained or classic PAT with the
workflowscope - Add it as a repository secret (e.g.,
GH_PAT_TOKEN) - Pass it to the action:
# Pin to an immutable SHA for supply chain security (SLSA/SSDF compliance).
# To find the SHA: gh release view v1 --repo mdelapenya/claude-ralph-github-action --json targetCommitish
# Example: mdelapenya/claude-ralph-github-action@abc1234def5678 # v1
- uses: mdelapenya/claude-ralph-github-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GH_PAT_TOKEN }}Without a PAT: Ralph can modify all files except those in .github/workflows/. Push attempts that include workflow changes will fail.
With a PAT (workflow scope): Ralph can modify any file including workflows. Use this when tasks specifically require workflow changes.
Once Ralph opens a pull request, you can close the feedback loop without leaving GitHub:
- Review the PR normally — leave inline comments, an overall review, or both
- Post
/ralph-reviewas a comment on the PR to trigger another Ralph run - Ralph re-runs the full loop, incorporating all review feedback into the task context:
- Inline code comments (file, line, and body)
- Overall review bodies and their state (e.g.,
CHANGES_REQUESTED)
- Ralph commits the fixes and pushes to the same branch, updating the PR automatically
Passing reviewer instructions: You can add extra guidance after the command. Everything after /ralph-review (separated by a space or newline) is passed to the worker as "Reviewer Instructions":
/ralph-review focus on error handling and add unit tests for the new functions
Customizing the slash command: If you want to use a different command (e.g., /review), change the literal string in both the job if condition and the ralph_review_command action input. The env context is not available in job-level if conditions (a GitHub Actions limitation), so the command string must be hardcoded there:
# Job-level if: hardcode the literal string
if: >-
...
(github.event_name == 'issue_comment' && github.event.action == 'created' && github.event.issue.pull_request && (github.event.comment.body == '/ralph-review' || startsWith(github.event.comment.body, '/ralph-review ')) && github.event.comment.user.type != 'Bot')
# Action step: pass as input so entrypoint.sh knows the command
# Pin to an immutable SHA for supply chain security (SLSA/SSDF compliance).
# To find the SHA: gh release view v1 --repo mdelapenya/claude-ralph-github-action --json targetCommitish
# Example: mdelapenya/claude-ralph-github-action@abc1234def5678 # v1
- uses: mdelapenya/claude-ralph-github-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
ralph_review_command: '/ralph-review'Note: Only non-bot users can trigger /ralph-review. Ralph's own comments are never used as triggers.
Ralph only works on issues. If the ralph label is added to a pull request, Ralph will post a comment explaining it can only work on issues, and exit without making changes.
Ralph supports splitting complex tasks into multiple subtasks that can be processed in parallel. If the worker agent determines that a task is too complex or would benefit from parallel execution, it can create multiple GitHub issues labeled with ralph. Each issue will be processed by a separate Ralph action instance concurrently.
How it works:
- The worker agent assesses the task complexity during implementation
- If appropriate, it creates temporary files describing each subtask (title and body)
- It invokes the
/scripts/create-subtask-issues.shhelper script - The script creates GitHub issues with the
ralphlabel for each subtask - GitHub Actions spawns separate Ralph workflows to process each issue in parallel
When to split tasks:
- Multiple independent features or components
- Different areas of the codebase that don't depend on each other
- Large scope that would be clearer as separate, focused tasks
Example:
If an issue requests "Add user authentication with login, registration, and password reset," the worker might split it into:
- Issue 1: "Add login API endpoint with JWT authentication"
- Issue 2: "Add registration API endpoint with validation"
- Issue 3: "Add password reset flow with email verification"
Each subtask is then processed independently and can be merged separately, allowing for faster parallel execution and clearer code reviews.
TL;DR: Always pin to a full commit SHA. Never use
@main,@latest, or a mutable tag like@v1in production workflows.
This action runs with access to your ANTHROPIC_API_KEY and github_token. A compromised maintainer account or Docker Hub credential could replace the code or image under a mutable reference without any visible change to your workflow file.
Pin to an immutable SHA:
# UNSAFE — tag can be silently moved to a different commit
- uses: mdelapenya/claude-ralph-github-action@v1
# SAFE — SHA cannot be retroactively changed
- uses: mdelapenya/claude-ralph-github-action@f8a8ef2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f # v1.2.3Get the SHA for the latest release:
gh release view --repo mdelapenya/claude-ralph-github-action --json tagName,targetCommitishUse Dependabot to get automatic SHA-update PRs when a new release is published — add package-ecosystem: "github-actions" to your .github/dependabot.yml.
See SECURITY.md for the full threat model, Dependabot setup, and recommended permission scoping.
Ralph includes unit and integration tests that validate the scripts without calling the Claude API.
# Run all unit + integration tests (no API key needed, completes in seconds)
bash test/run-all-tests.sh
# Lint all shell scripts
shellcheck --severity=warning entrypoint.sh scripts/*.sh test/**/*.sh test/*.sh| Category | Files | What it tests |
|---|---|---|
| Unit tests | test/unit/test-state.sh |
state.sh read/write helpers, including security result/feedback helpers |
test/unit/test-output-format.sh |
Action output format validation (pr_url, iterations, final_status) |
|
test/unit/test-git-config.sh |
Git author config is set correctly from INPUT_COMMIT_AUTHOR_* |
|
test/unit/test-workflow-patch.sh |
workflow-patch.sh helper functions generate correct patch comments |
|
| Integration tests | test/integration/test-shipped-flow.sh |
Full SHIP path: worker commits, reviewer approves, gate passes, PR URL written |
test/integration/test-max-iterations.sh |
REVISE loop exhausts INPUT_MAX_ITERATIONS, exits with code 2 |
|
test/integration/test-error-handling.sh |
Worker failure triggers ERROR exit with code 1 | |
test/integration/test-squash-merge.sh |
Squash-merge strategy writes merge-commit.txt instead of PR URL |
|
test/integration/test-pr-review-comment.sh |
/ralph-review slash command runs loop with PR review context in task.md |
|
test/integration/test-pr-review-submitted.sh |
pull_request_review event triggers loop with review body as context |
|
test/integration/test-reaction.sh |
Issue reactions trigger the Ralph loop correctly | |
test/integration/test-push-error-recovery.sh |
Push failure is recorded and fed back to the worker as review feedback | |
test/integration/test-rerun-already-pushed.sh |
Re-run on a branch already pushed to remote continues without force-pushing | |
test/integration/test-workflow-push-fallback.sh |
Workflow file push failures generate a patch comment and retry without .github/workflows/ |
|
test/integration/test-security-gate-pass.sh |
Reviewer SHIPs, security gate PASSes → SHIPPED; audit log records gate phases | |
test/integration/test-security-gate-fail-then-pass.sh |
Gate FAILs on first SHIP, forces REVISE with findings; gate PASSes on iteration 2 → SHIPPED | |
test/integration/test-security-gate-disabled.sh |
security_gate_enabled: false skips gate, ships normally, no security-result.txt written |
Integration tests exercise the real ralph-loop.sh -> worker.sh -> reviewer.sh pipeline with mock binaries:
- Mock
claude(test/helpers/mocks.sh): A standalone script placed onPATHthat inspects the prompt to determine worker vs reviewer vs security gate mode. The worker mock creates a file and commits it. The reviewer mock writesSHIPorREVISEto state files. The security gate mock writesPASSorFAIL. Behavior is configurable via env vars:MOCK_REVIEW_DECISION—SHIP(default) orREVISEMOCK_WORKER_FAIL— Set totrueto simulate worker failureMOCK_MERGE_STRATEGY— Set tosquash-mergefor squash-merge testsMOCK_SECURITY_GATE_DECISION—PASS(default),FAIL, orFAIL_ONCE(fails the first invocation, passes subsequent ones)
- Mock
gh: Returns mock PR URLs and no-ops for issue comments - Isolated workspaces (
test/helpers/setup.sh): Each test runs in a temp directory with its own git repo and bare remote, sogit pushworks without network access
To run tests in your CI workflow, copy the job definitions from test/ci-example.yml into your .github/workflows/ci.yml. The example includes separate jobs for unit and integration tests.
# Requires Docker and an Anthropic API key
ANTHROPIC_API_KEY=sk-... ./test/run-local.sh
# With verbose Claude CLI output (⚠️ security: logs include full tool call payloads and file contents;
# do not enable in workflows where runner logs are publicly visible)
RALPH_VERBOSE=true ANTHROPIC_API_KEY=sk-... ./test/run-local.sh
# Override defaults
INPUT_WORKER_MODEL=haiku INPUT_MAX_ITERATIONS=1 ANTHROPIC_API_KEY=sk-... ./test/run-local.sh