Running Claude Code in GitHub Actions: A Practical Setup Guide
Running Claude Code in GitHub Actions means giving the same coding agent you use on your laptop a job inside your CI pipeline: it can respond to an @claude mention on an issue, review a pull request the moment it opens, or run on a schedule to clear a backlog of small fixes. This guide walks through the actual setup: authentication, the official action, trigger patterns, permission scoping, and the guardrails you need so an autonomous agent doesn't burn your CI budget or push changes nobody asked for. Everything here is version-agnostic and works with the current claude-code-action from Anthropic's GitHub org.
Why run Claude Code in GitHub Actions at all
Claude Code on your machine is interactive: you type a prompt, watch it work, approve or reject edits. That's great for active development. But a lot of engineering work is reactive and doesn't need a human sitting at the keyboard:
- A contributor opens an issue titled "Login button misaligned on Safari." Someone has to triage it, reproduce it, and either fix it or explain why it's not a bug.
- A PR comes in from a first-time contributor and needs a first-pass code review before a maintainer spends time on it.
- A dependency bump breaks three unrelated tests and someone has to go fix the assertions.
- Nightly, you want a pass over open issues labeled "good first fix" to see which ones Claude Code can actually close.
Running Claude Code inside GitHub Actions turns these into automated jobs. The agent checks out the repo, reads the issue or diff, makes changes using the exact same tool-use loop as the CLI, and opens a PR or posts a comment. It's still bounded: you control what tools it can use, what it can touch, and whether it can push directly or must go through a PR.
The building blocks
Three pieces make this work:
- The workflow trigger. GitHub Actions decides when the job runs: on
issue_comment, onpull_request, on aschedulecron, or onworkflow_dispatchfor a manual button-press. - The action itself. Anthropic publishes
anthropics/claude-code-action, a GitHub Action that installs Claude Code, wires up authentication, and runs a prompt against the checked-out repository. - Authentication. Claude Code in Actions needs either an Anthropic API key or an OAuth token, stored as a repository or organization secret, never as plaintext in the workflow file.
Step 1: get credentials into GitHub secrets
Go to your repo's Settings > Secrets and variables > Actions and add a secret. If you're using an Anthropic API key:
Name: ANTHROPIC_API_KEY
Value: sk-ant-...If your organization uses Claude Code's OAuth-based setup instead of a raw API key, the equivalent secret is typically named something like CLAUDE_CODE_OAUTH_TOKEN, generated by running the CLI's setup command locally and copying the token it prints, rather than an API key from the console. Either path works with the official action; you just point the action at whichever secret you populated.
Never commit the key to a workflow file, a .env checked into git, or a comment. Treat it like any other credential: rotate it if it leaks, and scope a dedicated key for CI so you can revoke it independently of your personal one.
Step 2: the minimal workflow
Here's a workflow that responds whenever someone mentions @claude in an issue or PR comment. Save this as .github/workflows/claude.yml:
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
respond:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}That's the whole thing for a basic setup. The if: condition means the job only runs when the trigger comment actually contains @claude, so you're not spending Actions minutes or API tokens on every comment in the repo. The action reads the comment, the surrounding issue or PR context, and the checked-out code, then acts.
Step 3: scope permissions tightly
The permissions: block in the job above is doing real work. GitHub Actions jobs run with a GITHUB_TOKEN that's scoped by whatever you declare, and by default a workflow gets broad read/write access unless you restrict it. Set only what the job needs:
permissions:
contents: write # so it can commit and push a branch
pull-requests: write # so it can open or comment on PRs
issues: write # so it can comment on issues
# leave out anything you don't need, e.g. actions: write, packages: writeIf a job only reviews PRs and never pushes code, drop contents: write and use contents: read. The rule of thumb: give the agent exactly the write access it needs to do the specific job this workflow does, nothing more.
Step 4: control which tools Claude Code can use
By default, an agentic coding tool given shell access can, in principle, run any command. In CI that's a real risk: a runner has network access, secrets in the environment, and the ability to push to your repo. The claude-code-action supports an allowed_tools or equivalent input to whitelist what the agent can do inside the job. A conservative starting point for a "review this PR" workflow:
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: |
Read
Grep
Glob
disallowed_tools: |
Bash(curl:*)
Bash(wget:*)For a workflow that's supposed to actually fix bugs and open PRs, you'll need to allow Edit, Write, and a scoped Bash for running the test suite and git commands, but you can still block network-fetching commands like curl or wget so the agent can't exfiltrate your secrets or pull in arbitrary code. Check the current action's README for the exact input names, since these get renamed occasionally, but the pattern (allowlist plus explicit denylist for anything network-facing) holds regardless of the exact syntax.
Step 5: give it a real job with a custom prompt
The mention-triggered workflow above is reactive. For scheduled or dispatch-triggered jobs, you write the prompt directly into the workflow so Claude Code knows exactly what to do without a human typing anything:
name: Nightly issue triage
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch: {}
jobs:
triage:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Look at all open issues labeled "needs-triage".
For each one, read the description, check if it's reproducible
from the code in this repo, and post a comment that either:
1. Confirms the bug and suggests a root cause, or
2. Explains why it's not reproducible and asks for more info.
Do not close issues. Do not push any code changes in this job.
Replace the "needs-triage" label with "triaged" once you've
commented.
allowed_tools: |
Read
Grep
GlobThis job is read-only against the codebase (contents: read, no Edit/Write/Bash in the allowed tools) and only touches issues, which caps the blast radius if the prompt goes sideways.
Step 6: let it open pull requests instead of pushing to main
For a workflow that fixes code, never let the agent push directly to your default branch. Have it create a branch and open a PR, so a human reviews the diff before it merges:
name: Auto-fix on label
on:
issues:
types: [labeled]
jobs:
fix:
if: github.event.label.name == 'claude-fix'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Fix the bug described in issue #${{ github.event.issue.number }}.
Create a new branch named fix/issue-${{ github.event.issue.number }}.
Run the existing test suite and add a regression test if one
doesn't exist. Open a pull request against main that references
this issue. Do not merge the PR yourself.
allowed_tools: |
Read
Edit
Write
Grep
Glob
Bash(npm test:*)
Bash(git:*)The Bash(npm test:*) and Bash(git:*) entries scope exactly which shell commands are permitted; the agent can run your test suite and git operations but nothing else. Branch protection rules on main (requiring at least one human review) are your real backstop here: even if the workflow is misconfigured, nothing lands without a person clicking approve.
Cost and runaway-loop guardrails
Two failure modes matter in practice:
Cost. Every run consumes API tokens. A nightly cron job over dozens of issues, each with a large prompt and repo context, adds up. Set a timeout-minutes on the job so a stuck run doesn't burn hours:
jobs:
triage:
timeout-minutes: 15Also consider a max-turns or step-limit input if the action exposes one, so a single invocation can't loop indefinitely trying to satisfy an ambiguous prompt.
Trigger loops. If your workflow runs on issue_comment and Claude Code posts a comment as part of its response, make sure the trigger condition excludes comments from the bot account itself, or you'll get a job that responds to its own reply forever:
if: |
contains(github.event.comment.body, '@claude') &&
github.event.comment.user.login != 'claude-code-bot'Adjust the bot login to whatever account or app posts the comments in your setup.
Testing the workflow before you trust it
Before wiring this into your main branch protection or nightly schedule, test it on a throwaway repo or a feature branch:
# trigger manually with workflow_dispatch
gh workflow run claude.yml
# watch the run
gh run watchRead the full transcript in the Actions log, not just the final PR diff. Claude Code in Actions runs non-interactively, so if the prompt is ambiguous you'll see it make an assumption rather than asking you, which is exactly the behavior you want to catch in a test run rather than discover in production.
A realistic end-to-end example
Here's what a small open-source repo might actually ship: one workflow for @claude mentions on issues and PRs (reactive, human-initiated), and one narrow scheduled workflow that only relabels stale issues, with no write access to code at all. That combination gets you most of the value, on-demand review and fixes, a periodic sweep, without the risk profile of a fully autonomous agent pushing to your codebase unattended.
# .github/workflows/claude-mention.yml
name: Claude mention responder
on:
issue_comment:
types: [created]
jobs:
respond:
if: >
contains(github.event.comment.body, '@claude') &&
github.event.comment.user.login != 'claude-code-bot'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}Start there. Add the scheduled triage job once you trust the mention-based one, and only add push-to-branch autonomy after you've watched several runs and are comfortable with the diffs it produces.
FAQ
Do I need a paid Claude subscription or an API key to run this in Actions? You need API access, either a standard Anthropic API key billed by usage, or an OAuth token if your organization uses that setup. A personal Claude.ai subscription alone does not give the GitHub Action API access; the credential has to be one the action can authenticate with non-interactively.
Can Claude Code push directly to my main branch? Only if you give the job contents: write and don't ask it to open a PR. Don't do this. Have it push to a feature branch and open a pull request, and put branch protection on main so a human has to approve before merge, even for changes the agent makes.
How do I stop it from running on every single comment in a repo? Use an if: condition on the job or step that checks for a specific trigger phrase like @claude in the comment body, and exclude comments from the bot's own account to avoid it replying to itself. Without this filter, a comment-triggered workflow fires on every comment, which is both expensive and noisy.
Is it safe to give it Bash access in CI? Only with an explicit allowlist. Scope Bash to specific command prefixes like Bash(npm test:*) or Bash(git:*) rather than an unscoped Bash entry, and explicitly deny network tools like curl and wget so a compromised or confused run can't exfiltrate secrets from the runner's environment.
What happens if the API call fails or times out mid-run? The job fails like any other GitHub Actions step: the run shows red, and nothing is merged because you should always be routing changes through a PR rather than a direct push. Set timeout-minutes on the job so a hung run doesn't sit consuming Actions minutes until it hits GitHub's own maximum timeout.
Can I run this on a self-hosted runner instead of GitHub-hosted? Yes. The action itself doesn't care where the runner lives; it just needs network access to the Anthropic API and the repo checked out locally. Self-hosted runners are worth it if you need the agent to reach internal services, private package registries, or infrastructure that isn't reachable from GitHub's hosted runners, but they also mean you're responsible for the runner's security posture, since it's a machine capable of running arbitrary code from a workflow file.
How is this different from Dependabot or other bots? Dependabot and similar bots follow fixed, narrow logic: bump a version, open a PR, done. Claude Code in Actions reads context and writes code with judgment, so it can fix an actual bug described in prose, explain its reasoning in a PR description, or decide an issue isn't reproducible and ask a clarifying question. That flexibility is the entire point, and also why the permission scoping and tool allowlists in this guide matter more than they would for a fixed-logic bot.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
Claude CodeGo from zero to confident with Claude Code, the terminal agent that reads, edits, runs, and verifies real code.
Related reading