Claude auto PR review
Copied from the official Solutions Guide basic example (prompt shortened).
pull_request: opened, synchronize
Est. cost per run~$0.18Checked Oct 16, 2026
Not executedJust shippedOur pick for the best security, implementation, and planning skills.Best skills for each job.See the picks →
When CI fails on a pull request that has the auto-fix label, Claude pushes a fix commit to the branch. The number of auto-fix attempts per branch is capped.
workflow_run: CI completed (failure), opted-in PRs only
How it works
Setup
.github/workflows/vcad-fix-ci.ymlGITHUB_TOKEN, ANTHROPIC_API_KEY. The file reads them as secrets.NAME. Do not paste the key into the YAML.Let your agent do it
Paste it into your coding agent in this repo. It does the setup and asks before anything leaves your machine.
Runs in GitHub Actions on your Claude Pro or Max plan through an OAuth token from claude setup-token. No API credits.
Set up the "vcad: Claude fixes failing CI automatically" GitHub workflow from AgentFlows (https://agentflows.sh/flows/vcad-fix-ci) so it runs on my Claude Pro or Max subscription instead of API credits.
Install these agent skills first. Download each SKILL.md from its source (respect its license) into .claude/skills/<name>/SKILL.md, or wherever this agent loads skills from:
- Debug: https://github.com/anthropics/knowledge-work-plugins/blob/main/engineering/skills/debug/SKILL.md (Apache-2.0), notes at https://agentflows.sh/skills/debug
- Iterate on a PR until CI passes and review feedback is addressed: https://github.com/getsentry/skills/tree/main/skills/iterate-pr (Apache-2.0), notes at https://agentflows.sh/skills/getsentry-iterate-pr
Save this file as .github/workflows/vcad-fix-ci.yml:
```yaml
name: Fix CI
# Auto-fix failing CI on PRs. Budgeted: at most MAX_ATTEMPTS auto-fix
# commits per PR branch. Opt in by adding the `auto-fix` label to the
# PR (flip from opt-out to opt-in so a hostile PR can't summon a
# Claude-authored commit onto its own branch just by failing CI).
on:
workflow_run:
workflows: ["CI"]
types: [completed]
permissions:
contents: write
pull-requests: write
issues: write
actions: read
# anthropics/claude-code-action@v1 mints an OIDC token to identify the
# runner to Anthropic's API; without id-token: write it fails with
# "Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable".
id-token: write
concurrency:
# Serialize attempts per PR so we never have two fix-ci jobs racing
# to push to the same branch.
group: fix-ci-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
env:
# Ceiling on how many times the bot will try to fix the same PR.
MAX_ATTEMPTS: "3"
# Commit-subject prefix used both to author and to count auto-fix
# commits. Changing this breaks the attempt counter.
COMMIT_MARKER: "fix(ci-auto)"
jobs:
fix:
# Only run on real CI failures that came from a PR.
if: >-
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Resolve PR number and branch
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
# workflow_run doesn't directly carry the PR number; look it up
# from the head SHA.
pr_json=$(gh api "repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" --jq '.[0] // empty')
if [ -z "$pr_json" ]; then
echo "No PR associated with ${HEAD_SHA}; skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
pr_number=$(echo "$pr_json" | jq -r '.number')
pr_branch=$(echo "$pr_json" | jq -r '.head.ref')
pr_repo=$(echo "$pr_json" | jq -r '.head.repo.full_name')
pr_labels=$(echo "$pr_json" | jq -r '[.labels[].name] | join(",")')
echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"
echo "pr_branch=${pr_branch}" >> "$GITHUB_OUTPUT"
echo "pr_repo=${pr_repo}" >> "$GITHUB_OUTPUT"
echo "pr_labels=${pr_labels}" >> "$GITHUB_OUTPUT"
# Opt-in: only run when a maintainer has explicitly labeled the
# PR with `auto-fix`. Without this, any PR whose CI fails would
# summon the agent — and the agent pushes to the PR branch.
if ! echo "${pr_labels}" | tr ',' '\n' | grep -qx 'auto-fix'; then
echo "PR #${pr_number} is not labeled auto-fix; skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Don't auto-fix PRs from forks — pushing to a fork's branch
# requires the forker's token, which we don't have.
if [ "${pr_repo}" != "${{ github.repository }}" ]; then
echo "PR #${pr_number} comes from fork ${pr_repo}; skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Checkout PR branch
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@v6
with:
ref: ${{ steps.pr.outputs.pr_branch }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check attempt budget
if: steps.pr.outputs.skip != 'true'
id: budget
run: |
set -euo pipefail
# Count auto-fix commits already on this branch since it
# diverged from main.
base=$(git merge-base origin/main HEAD)
attempts=$(git log --format='%s' "${base}..HEAD" \
| grep -c "^${COMMIT_MARKER}" || true)
echo "Found ${attempts} prior auto-fix commits on this branch."
echo "attempts=${attempts}" >> "$GITHUB_OUTPUT"
if [ "${attempts}" -ge "${MAX_ATTEMPTS}" ]; then
echo "over_budget=true" >> "$GITHUB_OUTPUT"
else
echo "over_budget=false" >> "$GITHUB_OUTPUT"
fi
- name: Comment and stop (over budget)
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr comment "${{ steps.pr.outputs.pr_number }}" --repo "${{ github.repository }}" --body \
"🤖 auto-fix budget exhausted (${{ steps.budget.outputs.attempts }}/${{ env.MAX_ATTEMPTS }} attempts). Next step is manual — push a fix or re-run after resolving the failure root cause."
- name: Fetch failed CI logs
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
set -euo pipefail
mkdir -p .ci-fix
# `--log-failed` grabs stderr of only the failed steps, which
# keeps the payload small enough to fit in a prompt.
gh run view "${RUN_ID}" --repo "${{ github.repository }}" --log-failed > .ci-fix/failed.log || true
gh run view "${RUN_ID}" --repo "${{ github.repository }}" --json name,displayTitle,conclusion,jobs > .ci-fix/run.json
wc -l .ci-fix/failed.log
- name: Sibling repos (mirror ci.yml)
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
run: |
git clone --depth 1 https://github.com/ecto/tang.git ../tang
git clone --depth 1 https://github.com/ecto/phyz.git ../phyz
git clone --depth 1 https://github.com/ecto/loon.git ../loon
- uses: dtolnay/rust-toolchain@master
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
with:
toolchain: stable
targets: wasm32-unknown-unknown
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
- uses: actions/setup-node@v6
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
with:
node-version: 22
cache: npm
- name: Install wasm-pack
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
run: cargo install wasm-pack --locked --version 0.13.1
- name: Install apt deps
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
run: |
sudo apt-get update && sudo apt-get install -y \
libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev
- name: npm ci
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
run: npm ci
- name: Configure git
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
run: |
git config user.name "vcad-agent[bot]"
git config user.email "vcad-agent@users.noreply.github.com"
- name: Run agent
if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'
uses: anthropics/claude-code-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
You are fixing a failing CI run on pull request #${{ steps.pr.outputs.pr_number }}
(branch `${{ steps.pr.outputs.pr_branch }}`). Attempt ${{ steps.budget.outputs.attempts }}+1 of ${{ env.MAX_ATTEMPTS }}.
Read CLAUDE.md first for project conventions (Z-up coordinates,
workspace layout, commands CI runs).
The raw log of failed CI steps is at `.ci-fix/failed.log`.
The run metadata is at `.ci-fix/run.json`.
Your job:
1. Read `.ci-fix/failed.log` and identify the root cause of
every failed job. Multiple jobs can fail for unrelated
reasons — handle each.
2. Fix the root cause in source. Don't paper over real test
failures by disabling tests, loosening assertions, or adding
`--no-verify`. If a test is flaky, say so in the commit
message but still try a real fix first.
3. Run the exact commands that failed, locally, and confirm
they now pass. Use the sibling repos already cloned at
../tang, ../phyz, ../loon. Apt deps are installed.
Typical commands to try:
cargo fmt -p <crate> --check
cargo clippy --workspace --exclude vcad-desktop -- -D warnings
cargo test --workspace --exclude vcad-desktop
cargo build --workspace --exclude vcad-desktop --examples
cargo doc --workspace --exclude vcad-desktop --no-deps
npm run build --workspaces
npm test --workspaces --if-present
4. Delete `.ci-fix/` before committing so it never lands in
the tree.
5. Commit with subject prefix `${{ env.COMMIT_MARKER }}:` so
the attempt counter can find it. Include a short
description of what broke and what you changed.
6. Push to branch `${{ steps.pr.outputs.pr_branch }}`.
If you cannot fix the failure — it's genuinely ambiguous, the
test needs product input, or the fix is larger than a CI-fix
ought to be — do NOT commit. Instead leave a `gh pr comment`
on PR #${{ steps.pr.outputs.pr_number }} explaining what
broke, what you tried, and why it needs human attention.
```
It authenticates with CLAUDE_CODE_OAUTH_TOKEN. Tell me to run `claude setup-token` in my own terminal to create it, then run `gh secret set CLAUDE_CODE_OAUTH_TOKEN` and let me paste it. Never write the token into a file.
It runs on: workflow_run: CI completed (failure), opted-in PRs only. Runs count against my subscription's usage limits, and GitHub Actions minutes still apply.
Show me the diff, then commit on a new branch and open a pull request.The file
Copy the file as-is. The notes beside each block explain it.
.github/workflows/vcad-fix-ci.yml
Name
The name GitHub shows in the Actions tab. The file path is under Where to paste this.
name: Fix CI# Auto-fix failing CI on PRs. Budgeted: at most MAX_ATTEMPTS auto-fix# commits per PR branch. Opt in by adding the `auto-fix` label to the# PR (flip from opt-out to opt-in so a hostile PR can't summon a# Claude-authored commit onto its own branch just by failing CI).Trigger
GitHub starts a run when the workflow named CI finishes.
on: workflow_run: workflows: ["CI"] types: [completed]Permissions
This token may push commits, comment on pull requests and open them, comment on issues and change labels, read Actions logs, request a GitHub OIDC token, cancel-in-progress: false, MAX_ATTEMPTS: "3", and COMMIT_MARKER: "fix(ci-auto)". Anything not listed is not granted.
permissions: contents: write pull-requests: write issues: write actions: read # anthropics/claude-code-action@v1 mints an OIDC token to identify the # runner to Anthropic's API; without id-token: write it fails with # "Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable". id-token: writeconcurrency: # Serialize attempts per PR so we never have two fix-ci jobs racing # to push to the same branch. group: fix-ci-${{ github.event.workflow_run.head_branch }} cancel-in-progress: falseenv: # Ceiling on how many times the bot will try to fix the same PR. MAX_ATTEMPTS: "3" # Commit-subject prefix used both to author and to count auto-fix # commits. Changing this breaks the attempt counter. COMMIT_MARKER: "fix(ci-auto)"Jobs
Each job is one runner. The steps inside it run top to bottom.
jobs: fix: # Only run on real CI failures that came from a PR. if: >- github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latestSteps
These steps run in order on that job.
steps: - name: Resolve PR number and branch id: pr env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} HEAD_SHA: ${{ github.event.workflow_run.head_sha }} run: | set -euo pipefail # workflow_run doesn't directly carry the PR number; look it up # from the head SHA. pr_json=$(gh api "repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" --jq '.[0] // empty') if [ -z "$pr_json" ]; then echo "No PR associated with ${HEAD_SHA}; skipping." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi pr_number=$(echo "$pr_json" | jq -r '.number') pr_branch=$(echo "$pr_json" | jq -r '.head.ref') pr_repo=$(echo "$pr_json" | jq -r '.head.repo.full_name') pr_labels=$(echo "$pr_json" | jq -r '[.labels[].name] | join(",")') echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT" echo "pr_branch=${pr_branch}" >> "$GITHUB_OUTPUT" echo "pr_repo=${pr_repo}" >> "$GITHUB_OUTPUT" echo "pr_labels=${pr_labels}" >> "$GITHUB_OUTPUT" # Opt-in: only run when a maintainer has explicitly labeled the # PR with `auto-fix`. Without this, any PR whose CI fails would # summon the agent — and the agent pushes to the PR branch. if ! echo "${pr_labels}" | tr ',' '\n' | grep -qx 'auto-fix'; then echo "PR #${pr_number} is not labeled auto-fix; skipping." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi # Don't auto-fix PRs from forks — pushing to a fork's branch # requires the forker's token, which we don't have. if [ "${pr_repo}" != "${{ github.repository }}" ]; then echo "PR #${pr_number} comes from fork ${pr_repo}; skipping." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi echo "skip=false" >> "$GITHUB_OUTPUT" - name: Checkout PR branch if: steps.pr.outputs.skip != 'true' uses: actions/checkout@v6 with: ref: ${{ steps.pr.outputs.pr_branch }} fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Check attempt budget if: steps.pr.outputs.skip != 'true' id: budget run: | set -euo pipefail # Count auto-fix commits already on this branch since it # diverged from main. base=$(git merge-base origin/main HEAD) attempts=$(git log --format='%s' "${base}..HEAD" \ | grep -c "^${COMMIT_MARKER}" || true) echo "Found ${attempts} prior auto-fix commits on this branch." echo "attempts=${attempts}" >> "$GITHUB_OUTPUT" if [ "${attempts}" -ge "${MAX_ATTEMPTS}" ]; then echo "over_budget=true" >> "$GITHUB_OUTPUT" else echo "over_budget=false" >> "$GITHUB_OUTPUT" fi - name: Comment and stop (over budget) if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr comment "${{ steps.pr.outputs.pr_number }}" --repo "${{ github.repository }}" --body \ "🤖 auto-fix budget exhausted (${{ steps.budget.outputs.attempts }}/${{ env.MAX_ATTEMPTS }} attempts). Next step is manual — push a fix or re-run after resolving the failure root cause." - name: Fetch failed CI logs if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RUN_ID: ${{ github.event.workflow_run.id }} run: | set -euo pipefail mkdir -p .ci-fix # `--log-failed` grabs stderr of only the failed steps, which # keeps the payload small enough to fit in a prompt. gh run view "${RUN_ID}" --repo "${{ github.repository }}" --log-failed > .ci-fix/failed.log || true gh run view "${RUN_ID}" --repo "${{ github.repository }}" --json name,displayTitle,conclusion,jobs > .ci-fix/run.json wc -l .ci-fix/failed.log - name: Sibling repos (mirror ci.yml) if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' run: | git clone --depth 1 https://github.com/ecto/tang.git ../tang git clone --depth 1 https://github.com/ecto/phyz.git ../phyz git clone --depth 1 https://github.com/ecto/loon.git ../loon - uses: dtolnay/rust-toolchain@master if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' with: toolchain: stable targets: wasm32-unknown-unknown components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' - uses: actions/setup-node@v6 if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' with: node-version: 22 cache: npm - name: Install wasm-pack if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' run: cargo install wasm-pack --locked --version 0.13.1 - name: Install apt deps if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' run: | sudo apt-get update && sudo apt-get install -y \ libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev - name: npm ci if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' run: npm ci - name: Configure git if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' run: | git config user.name "vcad-agent[bot]" git config user.email "vcad-agent@users.noreply.github.com" - name: Run agent if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true'Agent
Runs Claude Code with anthropics/claude-code-action@v1. Inputs: anthropic_api_key (the Claude API key secret); prompt (the task).
uses: anthropics/claude-code-action@v1 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} prompt: | You are fixing a failing CI run on pull request #${{ steps.pr.outputs.pr_number }} (branch `${{ steps.pr.outputs.pr_branch }}`). Attempt ${{ steps.budget.outputs.attempts }}+1 of ${{ env.MAX_ATTEMPTS }}. Read CLAUDE.md first for project conventions (Z-up coordinates, workspace layout, commands CI runs). The raw log of failed CI steps is at `.ci-fix/failed.log`. The run metadata is at `.ci-fix/run.json`. Your job: 1. Read `.ci-fix/failed.log` and identify the root cause of every failed job. Multiple jobs can fail for unrelated reasons — handle each. 2. Fix the root cause in source. Don't paper over real test failures by disabling tests, loosening assertions, or adding `--no-verify`. If a test is flaky, say so in the commit message but still try a real fix first. 3. Run the exact commands that failed, locally, and confirm they now pass. Use the sibling repos already cloned at ../tang, ../phyz, ../loon. Apt deps are installed. Typical commands to try: cargo fmt -p <crate> --check cargo clippy --workspace --exclude vcad-desktop -- -D warnings cargo test --workspace --exclude vcad-desktop cargo build --workspace --exclude vcad-desktop --examples cargo doc --workspace --exclude vcad-desktop --no-deps npm run build --workspaces npm test --workspaces --if-present 4. Delete `.ci-fix/` before committing so it never lands in the tree. 5. Commit with subject prefix `${{ env.COMMIT_MARKER }}:` so the attempt counter can find it. Include a short description of what broke and what you changed. 6. Push to branch `${{ steps.pr.outputs.pr_branch }}`. If you cannot fix the failure — it's genuinely ambiguous, the test needs product input, or the fix is larger than a CI-fix ought to be — do NOT commit. Instead leave a `gh pr comment` on PR #${{ steps.pr.outputs.pr_number }} explaining what broke, what you tried, and why it needs human attention.Keep going
Copied from the official Solutions Guide basic example (prompt shortened).
pull_request: opened, synchronize
Est. cost per run~$0.18Checked Oct 16, 2026
Not executed`track_progress: true` posts a checkbox tracking comment (per Solutions Guide).
pull_request: opened, synchronize, ready_for_review, reopened
Est. cost per run~$0.22Checked Oct 16, 2026
Not executedNo `prompt` → the action runs in tag/interactive mode and responds to the comment. `trigger_phrase` default is `@claude` (usage.md).
issue_comment / pull_request_review_comment containing @claude
Est. cost per run~$0.27Checked Oct 16, 2026
Not executed· 1 caveat