Claude Code in GitHub Actions

How do you run a team of Claude Code agents in one workflow?

Several Claude Code agents in one GitHub Actions workflow: the strategy.matrix that fans one job out into many, a brief and a budget for each, needs to make a reviewer wait for them, and how that differs from subagents.

One matrix job fans out into three agent runs, and a reviewer job waits on all of them.

Listen to this guide

The short answer

Write one job, fan it out with strategy.matrix, and put the job that reads their work behind needs. GitHub starts every matrix combination at the same time, each on a runner of its own, so three agents with three briefs is one job block and a list of three.

That is parallelism in the workflow file, not inside the model. Claude Code's own subagents run within a single session on one machine, sharing its transcript and its budget. Matrix jobs are separate runs: separate checkouts, separate caps, separate logs, and one that fails does not take the others down.

One workflow, several agents

  • A job is the unit. Jobs in a workflow start together unless something says otherwise, and each gets a fresh virtual machine. Two agents means two jobs, and needs and matrix is the reference for both keys.
  • A matrix writes those jobs for you. One job block plus an axis of values becomes one job per value, so adding a fourth agent is a fourth list item rather than a fourth copy of the YAML.
  • Nothing is shared between them. Separate runners mean separate checkouts and separate working trees. Nothing one agent writes is visible to another unless it lands in the repository or in a job output.
  • So give each one its own branch. Two agents pushing the same branch race, and the loser's commit is rejected or overwritten. A branch per agent keeps every result reviewable.
  • Split by area, not by step. A team works when each brief owns a different part of the repository. Two agents editing the same files produce two pull requests that conflict with each other.
  • Subagents are a different mechanism. They live in .claude/agents/, run inside one Claude Code session, and answer to that run's --max-turns and its timeout. Useful, but one job, one log, one failure.

The matrix that fans them out

  • The axis holds objects, not just strings. Each entry carries whatever the job needs to read, so matrix.agent.brief and matrix.agent.id come out of the same list the fan-out is built from.
  • fail-fast: false is the setting that matters here. Left at GitHub's default of true, the first agent to fail cancels every other agent mid-run, and you pay for the tokens they spent before the cancel.
  • max-parallel throttles the fan-out. An API key has a rate limit, and ten agents hitting it at once is how a run fails for reasons that have nothing to do with the code.
  • A matrix caps at 256 jobs per workflow run, which is far beyond anything worth paying for. The real limit is the concurrent job allowance on your GitHub plan.
  • Save this as .github/workflows/agent-team.yml on the default branch. Labelling an issue team starts three agents, and the fourth job waits for all of them.
name: Agent team

on:
  issues:
    types: [labeled]

# One team per issue; a second label on the same issue queues behind the first.
concurrency:
  group: agent-team-${{ github.event.issue.number }}
  cancel-in-progress: false

jobs:
  agents:
    if: github.event.label.name == 'team'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: write
      pull-requests: write
    strategy:
      # One agent failing leaves the rest to finish; the default would cancel them mid-run.
      fail-fast: false
      max-parallel: 2
      matrix:
        agent:
          - id: api
            area: src/api
            brief: Change the HTTP handlers and their tests. Leave the client and the docs alone.
          - id: web
            area: src/web
            brief: Change the client and its tests. Leave the handlers and the docs alone.
          - id: docs
            area: docs
            brief: Change the documentation to match the issue. Change no source file.
    steps:
      - uses: actions/checkout@v7

      - name: Work on ${{ matrix.agent.area }}
        uses: anthropics/claude-code-action@v1
        with:
          # A Claude subscription signs in with claude_code_oauth_token and secrets.CLAUDE_CODE_OAUTH_TOKEN instead of this line.
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          github_token: ${{ secrets.GITHUB_TOKEN }}
          prompt: |
            Issue #${{ github.event.issue.number }} in ${{ github.repository }} is the work, and this checkout is the default branch.
            Read it with `gh issue view ${{ github.event.issue.number }}`, then do only your own part of it.
            Your part: ${{ matrix.agent.brief }}
            Touch nothing outside ${{ matrix.agent.area }}; another agent owns the rest and is working right now.
            Finished: commit on branch agent/${{ github.event.issue.number }}-${{ matrix.agent.id }}, push it, and open a pull request whose body says which part you did.
            Nothing worth changing in your area: say so and stop, without an empty pull request.
          claude_args: |
            --allowedTools "Bash,Edit,Write,Read,Glob,Grep"
            --max-turns 30
            --max-budget-usd 4

  review:
    # Every agent above has to finish before this reads what they landed.
    needs: agents
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: read
      pull-requests: read
      issues: write
    steps:
      - uses: actions/checkout@v7

      - name: Comment the team's result on the issue
        uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          github_token: ${{ secrets.GITHUB_TOKEN }}
          display_report: true
          prompt: |
            Three agents worked on issue #${{ github.event.issue.number }} in ${{ github.repository }} and each opened its own pull request.
            List them with `gh pr list --search "agent/${{ github.event.issue.number }}-"` and read each diff with `gh pr diff`.
            Comment once on the issue: what each part changed, where two of them disagree, and what is still missing.
            Judge only what is in the diffs; open no pull request and change no file.
          claude_args: |
            --allowedTools "Bash,Read,Glob,Grep"
            --max-turns 20
            --max-budget-usd 2

Giving each one its own brief and caps

  • The brief comes from the matrix, not from three prompts. One prompt block reads matrix.agent.brief, so the shared instructions stay written once and only the part that differs is in the list.
  • Say what the agent must not touch. A brief that names an area but not its boundary produces three agents that all fix the same typo, and three pull requests that conflict.
  • --max-budget-usd caps the money and --max-turns caps the loop. They are per job, so a matrix of three multiplies both, and the number to plan against is the total.
  • timeout-minutes caps the runner. Without it a hung agent holds a machine for GitHub's default six hours; timeouts and concurrency covers that key and the concurrency block above it.
  • --allowedTools is per agent too. The reviewer above never gets Edit or Write, so the job that judges the work cannot quietly rewrite it.
  • Permissions belong on the job, not the workflow. The agents need contents: write to push; the reviewer needs issues: write and nothing else. Permissions and secrets sets out the scopes.

Making one wait for another

  • needs is the whole mechanism. A job naming another in needs starts only after that one finishes, and a job naming a matrix job waits for every combination of it.
  • By default it waits for success. If any agent fails, the reviewer is skipped. if: ${{ !cancelled() }} on the waiting job runs it anyway, which is what you want when two agents out of three landing is still worth reading.
  • Outputs are the only thing that crosses. A job passes a value on through outputs, read as needs.<job>.outputs.<name>. Files do not cross; a branch or an artifact does.
  • A planner in front is the other shape. One agent reads the issue, writes the split as a job output, and the matrix reads that output, so the team's size is decided per issue rather than in the file.
  • concurrency stops two teams colliding. Grouped by issue number, a second label on the same issue queues instead of putting six agents on the same branches.

What goes wrong with a team

  • The bill multiplies, not the clock. Three agents cost three agents' tokens whether they run at once or in turn. Parallelism buys wall-clock time and nothing else.
  • Overlapping briefs are the usual failure. Two agents given a whole repository will both find the same obvious thing, and neither pull request merges cleanly.
  • The default fail-fast throws work away. One agent hitting a rate limit cancels two agents that were nearly finished, and you are billed for what they spent.
  • A shared branch loses commits. Two jobs pushing the same ref means one push is rejected, and an agent told to retry may force it instead.
  • The logs are per job, not per team. Nothing joins three transcripts into one story, so the reviewer job exists to write that summary while the run is still fresh.
  • Reruns are per job. A failed matrix combination reruns on its own, which is cheap, but it reruns against a repository the other agents have since changed.
  • A team is worth it when the parts are genuinely separate. For one change to one area, one agent with a longer timeout beats three agents negotiating through pull requests. Claude Code in GitHub Actions is that single-agent file.

What Repo Fleet does about it

Repo Fleet is where you build workflows like this one and watch them run, across every repository you connect. The file stays what this guide built: GitHub Actions in your repository, on your runners, billed to your own Anthropic key. Repo Fleet holds no key and meters nothing.

  • The matrix is a section of the job card: the axes as rows, with fail-fast and max-parallel beside them and a count of the jobs they come to.
  • Each job's Waits for chips are needs, so the shape of the fan-out and the fan-in is visible without reading the YAML.
  • Every edit is checked as you type by the same checks that pass the workflow on this page, including a job missing its timeout and a grant wider than the run needs.
  • The runs page lists each repository's runs newest first, so a team that started failing shows up without opening the Actions tab.

Install it on GitHub, pick the repositories it may see, and open the workflow in the editor. The matrix is the list; the team is what it fans out to.