Knowledgebase

Claude Code in GitHub Actions: from an empty repository to a pull request

The complete workflow that runs Claude Code on a labelled issue and opens a pull request, with the API key kept secret, the token permissions it needs, what a run costs, and how to cap it.

GitHub Actions can run Claude Code the way it runs your tests: a workflow file in the repository, a runner GitHub starts on an event, and an API key you hold. This guide builds that from nothing. At the end, adding a label to an issue starts a run that reads the issue, changes the code, and opens a pull request for you to review. It takes one workflow file and one secret.

What you need

  • A GitHub repository. A new, empty one is fine, and Actions is on by default.
  • An Anthropic API key from the Console. Every run bills this key per token. A Claude subscription's OAuth token works instead, but it belongs to one person, so a team should use a key.
  • Nothing on your machine. The runner installs Claude Code itself on every run.

How the pieces fit

A workflow is a YAML file under .github/workflows. An event starts a job on a fresh virtual machine, the runner, and the job's steps run in order. This one has two steps:

  • actions/checkout puts the repository on the runner.
  • anthropics/claude-code-action installs Claude Code, hands it your prompt, and runs it in that checkout. With no prompt, the action waits for @claude in a comment; with one, it runs at once. This guide gives it a prompt, so nobody has to type anything.

Two credentials are in play. The API key pays Anthropic. GITHUB_TOKEN is a token GitHub mints for each job, limited to what the workflow's permissions block grants, and it expires when the job ends.

The workflow file

Save this as .github/workflows/agent.yml on the default branch. Everything below explains it.

name: Agent

on:
  issues:
    types: [labeled]

# Push a branch, open a pull request, comment on the issue: nothing more.
permissions:
  contents: write
  pull-requests: write
  issues: write

# One run at a time; a second labelled issue waits its turn.
concurrency:
  group: agent
  cancel-in-progress: false

jobs:
  implement:
    # Every label fires the event; only the agent label starts a run.
    if: github.event.label.name == 'agent'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v6

      - name: Implement the issue and open a pull request
        uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          # The workflow's own token; its pull request runs no CI, see below.
          github_token: ${{ secrets.GITHUB_TOKEN }}
          prompt: |
            Issue #${{ github.event.issue.number }} is yours.
            This checkout is the default branch.
            Read it with `gh issue view ${{ github.event.issue.number }}`.
            Do what it asks, and run the build and tests if there are any.
            Commit on branch agent/issue-${{ github.event.issue.number }}.
            Push it, then open a pull request for it with `gh pr create`.
            Its body ends with `Closes #${{ github.event.issue.number }}`.
            If it cannot be done, say why with `gh issue comment` and stop.
          claude_args: |
            --allowedTools "Bash,Edit,Write,Read,Glob,Grep"
            --max-turns 60
            --max-budget-usd 5

What each part does

  • The trigger. issues with labeled fires whenever any label lands on an issue, and the job's if lets only agent through. Adding a label takes triage access or more, and the action refuses a run started by anyone without write access, so opening an issue on a public repository starts nothing. Who can trigger runs has the exact rules.
  • The permissions block. Names what GITHUB_TOKEN may do in this run. Every scope it leaves out is off.
  • Concurrency. One agent run at a time. A second labelled issue queues behind the first instead of running beside it.
  • The timeout. GitHub stops the job at thirty minutes, whatever Claude is doing.
  • The checkout. Puts the default branch on the runner. The action then drops the checkout's credential and signs git with its own token, so the push works under the permissions above.
  • The action's inputs. anthropic_api_key reads the secret. github_token hands the action the workflow's own token, which it also exports as GH_TOKEN, so gh is signed in for the commands the prompt names. prompt is the whole brief. claude_args are Claude Code flags: the action starts Claude with file tools only, and --allowedTools adds Bash, which it needs to build, test, commit, push, and call gh. The two caps are covered under costs.

Commits carry claude[bot], an identity the action sets itself. The pull request is opened by github-actions, because it was opened with the workflow's token.

Run it

  1. Add the secret: Settings, then Secrets and variables, then Actions, then New repository secret, named ANTHROPIC_API_KEY. A secret is masked wherever a log would print it.
  2. Create a label named agent.
  3. Open an issue that says exactly what to build. In an empty repository, "Add a README that says what this repository is for" is enough. Then add the agent label.
  4. Watch the run under Actions. When it ends, a pull request from agent/issue-1 is waiting, its body ending in Closes #1. Merge it and the issue closes.

If no pull request appears, the run log says why: the label was not agent, the actor had no write access, the key was rejected, or Claude explained on the issue why it stopped.

Keep the key secret

  • The key lives only in the secret. The file references it, and nothing else ever holds it. Never paste a key into a workflow file, and never interpolate secrets.ANTHROPIC_API_KEY into a run script, where it lands in the command line and in the log of a failed command. Pass it through an input or env, as the file does.
  • Make a key for this use alone, in its own Console workspace with a spend limit, so a leak or a runaway is bounded and revocable without touching anything else.
  • Leave show_full_output off. It prints every tool result, which can include secrets, and the Actions log of a public repository is public.
  • The issue text is input to the model, and on a public repository a stranger can write it. The gates above mean a collaborator reads it and adds the label first, and the permissions block bounds what any instruction hidden in it could do. The action's security notes cover the rest, including why never to check out a pull request head under pull_request_target before this action.

Give the token only what the run needs

GITHUB_TOKEN starts each job with the repository's default permissions, which can be read-only or broad. The block replaces them with exactly what this run does: contents: write pushes the branch, pull-requests: write opens the pull request, and issues: write reads the issue and comments on it. A prompt that later needs CI logs adds actions: read; nothing else opens without a line here. Workflow syntax lists every scope.

One consequence to know before the first run: GitHub starts no workflow from anything done with GITHUB_TOKEN, so the pull request the agent opens runs no CI. To get CI on it, install the Claude GitHub App, delete the github_token line, and add id-token: write to the permissions block. The action then acts as the App, its pushes and pull requests start workflows like anyone's, and the App's own permission set applies.

What a run costs, and how to stop it running away

Each run draws on two meters.

  • GitHub Actions minutes. Free on a public repository; a private one draws on the plan's included minutes, then pays per minute.
  • Tokens. Every file Claude reads and every line it writes is tokens, priced per million by model. A run ends with a result naming its cost in dollars, its turns, and its duration. The action keeps it in the execution log it exposes as the step's execution_file output, and display_report: true on the step writes a report with the cost to the job summary. The Console shows the total across runs.

A concrete issue on a small repository typically costs cents to a couple of dollars in tokens. A vague issue on a large codebase costs several, most of it reading. The caps in the file, from the inside out:

  • --max-budget-usd 5 stops Claude Code once the run's API spend reaches five dollars.
  • --max-turns 60 caps how many times Claude calls the model. At the limit the run exits with an error.
  • timeout-minutes: 30 is GitHub's hard stop, and it caps the minutes bill.
  • The concurrency group keeps runs to one at a time, so the worst hour costs one run.
  • The label is the gate. Nothing runs until a person decides it should.

Outside the file, a spend limit on the Console workspace and a budget on GitHub Actions catch what the file cannot. To pay less per run, write sharper issues, keep the CLAUDE.md Claude reads on every run short, and pick the model with --model in claude_args.

Where this breaks down

Everything above is one file in one repository. Copy it into a second repository, then a fifth, and the file stops being a setup and becomes something to operate.

  • Every repository holds its own copy of the file, the prompt, the label, the secret, and the caps. A better prompt is one pull request per repository, each reviewed and merged by hand, and the copies drift the moment one is merged and another is not.
  • Nothing shows the runs together. Each repository's Actions tab lists its own, and the cost of each run sits inside its log.
  • Each repository sets its own secret, or an organization secret pools the spend into one number no repository can see.
  • The prompt is the agent, and it lives in a block scalar. Changing its behaviour means editing YAML by hand, in every copy, with nothing checking the file before the next run finds the mistake.

What Repo Fleet does about it

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

  • A visual editor for the workflow. Triggers, permissions, jobs, steps, and the action's inputs are plain fields, with the YAML one tab away, so a prompt is never edited through its indentation. Every edit is checked as you type, by the same checks that pass every YAML block in this guide.
  • Templates that start whole. The starred one ships the oldest open issue labelled fleet every fifteen minutes; another answers a comment with an agent.
  • Saving as a commit to the branch you are on, or as a branch and a pull request for review.
  • A runs page for each repository, newest first, saying what each run shipped.

Open the app, sign up, connect a GitHub account and choose its repositories, pick a template, and save it. The next labelled issue is the agent's.