Running Autonomous Claude Code Agents in GitHub Actions: What I Learned the Hard Way

March 10, 2026

I recently set up a system where three specialized Claude Code agents run autonomously every day via GitHub Actions. Each one audits a different aspect of our codebase (performance, accessibility, and security) and opens a PR if it finds something worth fixing.

The idea is simple. The execution was not.

Here's everything I learned getting anthropics/claude-code-action@v1 to work reliably in CI, so you don't have to hit the same walls.

The Setup

The architecture is straightforward: a single workflow file with three parallel jobs, each running a Claude Code agent with a focused prompt. The prompts live in .github/prompts/ as markdown files, and the workflow just tells Claude to read and follow them.

name: Claude Code Agents

on:
  schedule:
    - cron: "0 12 * * *"
  workflow_dispatch:
    inputs:
      agents:
        description: "Which agents to run (comma-separated or 'all')"
        required: false
        default: "all"
        type: string

Each job follows the same pattern: checkout, setup node, clean up local configs (more on this later), and run the agent. Simple enough in theory.

Lesson 1: The OAuth Token vs API Key Distinction

If you're on a Claude Max plan and you run claude setup-token, you get an OAuth token. This is not the same as an Anthropic API key (the sk-ant-... kind you get from console.anthropic.com).

The action has two separate inputs for these:

# If you have a Console API key:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

# If you have a Claude Code OAuth token (Max plan):
claude_code_oauth_token: ${{ secrets.ANTHROPIC_API_KEY }}

Use the wrong input and you'll get Invalid API key - Fix external API key with zero cost and zero tokens used. The error is clear once you see it, but getting to that error message required its own debugging journey.

Lesson 2: MCP Servers Will Crash Your CI

If your repo has a .mcp.json file (for local MCP servers like Supabase, PostHog, etc.), the action will try to start those servers in CI. They will fail. Claude Code will crash with exit code 1 and dump a wall of minified JavaScript that tells you absolutely nothing.

Here's the fun part: the action hardcodes enableAllProjectMcpServers = true after merging any settings you provide. So this does nothing:

# This gets overwritten by the action. Don't bother.
settings: '{"enableAllProjectMcpServers": false}'

I found this by reading the action's source code:

// Always set enableAllProjectMcpServers to true
settings.enableAllProjectMcpServers = true;

The fix is to delete .mcp.json from the checkout before the agent runs:

- name: Clean local dev configs
  run: |
    rm -f .mcp.json
    rm -f .claude/settings.local.json

Lesson 3: Local Settings Will Also Crash Your CI

Your .claude/settings.local.json likely references MCP tools, local file paths, and tool permissions that only make sense on your machine. The action loads project settings via settingSources: ["user", "project", "local"], so this file gets picked up in CI.

If it references tools like mcp__supabase__execute_sql or paths like /Users/you/Library/Android/sdk/..., Claude Code will choke on them during initialization. Same symptom: exit code 1, minified JS dump, zero tokens used.

Remove it in CI alongside .mcp.json.

Lesson 4: Tool Availability != Tool Permissions

This one cost me the most time. The action passes allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"] to the SDK, which tells Claude Code which tools are available. But Claude Code has a separate internal permission system that controls whether those tools can be used without approval.

In CI, there's nobody to click "approve" when Claude wants to write a file. Without explicit permissions, every Edit and Write call fails with:

Error: Claude requested permissions to write to .../file.tsx,
but you haven't granted it yet.

The fix is to pass permissions via the settings input:

settings: '{"permissions": {"allow": ["Edit", "Write", "Read", "Bash(*)", "Glob", "Grep"], "deny": []}}'

Note Bash(*) with the wildcard. Plain Bash won't cover commands with arguments.

Lesson 5: Re-running a Workflow Uses the Old Code

When you re-run a failed workflow in GitHub Actions, it uses the workflow file from the original commit that triggered the run. If you've pushed a fix to the workflow file since then, the re-run won't pick it up.

You need to trigger a completely new run:

gh workflow run claude-agents.yml --field agents=bolt

I wasted a few iterations re-running the same broken workflow wondering why my fixes weren't taking effect.

Lesson 6: Enable Full Output Early

By default, the action hides Claude's output for security. When things go wrong, you see minified JavaScript stack traces instead of actual error messages. Add this from day one:

show_full_output: true

This is what let me finally see the Invalid API key and permissions not granted messages instead of guessing from AJV schema validation dumps.

Lesson 7: The id-token Permission

The action needs OIDC tokens for authentication. Without this permission, you get a clear error, but it's easy to miss when setting up:

permissions:
  contents: write
  pull-requests: write
  issues: write
  id-token: write  # Required for authentication

The Final Working Workflow

After all the debugging, here's what a working job looks like:

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0

  - uses: actions/setup-node@v4
    with:
      node-version: 20

  - name: Clean local dev configs
    run: |
      rm -f .mcp.json
      rm -f .claude/settings.local.json

  - name: Run agent
    uses: anthropics/claude-code-action@v1
    with:
      claude_code_oauth_token: ${{ secrets.ANTHROPIC_API_KEY }}
      show_full_output: true
      settings: '{"permissions": {"allow": ["Edit", "Write", "Read", "Bash(*)", "Glob", "Grep"], "deny": []}}'
      prompt: |
        Read the file .github/prompts/your-agent.md for your full instructions.
        Follow every instruction in that file precisely.
        The current repository is: ${{ github.repository }}
        The default branch is: ${{ github.ref_name }}

The key pieces that aren't obvious from the docs:

  1. Delete .mcp.json and .claude/settings.local.json before running
  2. Use claude_code_oauth_token (not anthropic_api_key) if you're on a Max plan
  3. Pass permissions.allow in settings to auto-approve tool usage
  4. Set show_full_output: true so you can actually debug failures
  5. Always trigger new runs instead of re-running old ones after workflow changes

Structuring Agent Prompts

One pattern that worked well: keep the prompt file self-contained. The workflow just says "read this file and follow it." The prompt file handles everything: discovering the repo structure, installing dependencies, running the audit, deciding whether to open a PR, and formatting the branch name and PR body.

This means the workflow file is identical across all your repos. The agents adapt at runtime by reading CLAUDE.md, detecting which directories exist, and adjusting their approach accordingly.

Was It Worth It?

The debugging was painful. Seven iterations to get from "action installed" to "PR opened." But now I have three agents that autonomously review every repo daily and surface real improvements. The first successful run found a missing useMemo in a component that was re-computing a filtered list on every render.

Not every run will produce a PR, and that's by design. The agents are instructed to only open one when there's a genuine, verified improvement. No busywork PRs, no trivial changes.

If you're setting this up yourself, save this post. You'll hit at least three of these issues, and the error messages won't help you.