Just shippedBest skills for each job.See the picks →

ClaudeCI failureNot executed· 1 caveat

vcad: Claude fixes failing CI automatically

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

From event to result

1
Trigger
GitHub starts a run when the workflow named CI finishes.
2
Agent
Claude Code, via anthropics/claude-code-action.
3
What it does
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.
4
What you get
This listing does not state a pull request or a comment. Read the prompt, and treat the result as not executed.

Setup

Where to paste this

  1. Create .github/workflows/vcad-fix-ci.yml
  2. Paste the workflow below into that file. The notes beside the YAML are not part of the file.
  3. Add these repository secrets: GITHUB_TOKEN, ANTHROPIC_API_KEY. The file reads them as secrets.NAME. Do not paste the key into the YAML.

Let your agent do it

Copy a prompt for Claude Code, Codex, or Cursor.

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

Workflow YAML

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-latest

Steps

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

More workflows

All workflows
  • ClaudePull request
    01

    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 executed
  • ClaudePull request
    02

    Claude PR review with progress tracking

    `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 executed
  • ClaudeComment
    03

    @claude mention assistant on issues and PRs

    No `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