Claude Code GitHub Actions CI/CD permissions DevOps automation 2026

Claude Code in GitHub Actions: How to Fix Permission Errors (3 Battle-Tested Patterns)

The Prompt Shelf ·

If you’ve tried running Claude Code in GitHub Actions and hit permission errors, this guide shows you exactly how to fix it — with 3 battle-tested patterns.

The root cause is almost always the same: Claude Code is designed for interactive use. It asks for confirmation before writing files, running Bash commands, or touching anything sensitive. In a CI environment, there’s nobody home to click “yes.” The workflow hangs, times out, or exits with a cryptic error.

Here’s what’s actually happening, and how to get past it.


Why Permission Errors Happen in CI Environments

When you run claude -p "do something" locally, Claude Code’s permission system works as intended: it pauses before taking actions and waits for your approval. That’s a feature.

In GitHub Actions, that same behavior is a showstopper. The runner has no interactive terminal. Any prompt that waits for user input will block indefinitely until the job hits its timeout limit.

The specific errors engineers hit most often:

Permission prompt hangs the workflow:

Bash command: npm install
Do you want to proceed?
> 1. Yes
> 2. No

The job never moves past this point.

Interactive mode detection failure:

Error: Cannot use interactive mode in a non-TTY environment
Use --output-format text for non-interactive output

File edit approval required:

Edit file: src/index.ts
This will modify the file. Approve?

All three stem from the same issue: Claude Code’s default mode expects a human in the loop. For CI, you need to tell it explicitly that no human is coming.


The cleanest solution is Anthropic’s official claude-code-action, which handles non-interactive setup for you. This is the approach to use if you want @claude mentions in PRs and issues, or automated workflows triggered by GitHub events.

Step 1: Add your API key as a repository secret

Go to your repository’s Settings → Secrets and variables → Actions and add:

Never hardcode this in your workflow file. GitHub will redact secrets from logs, but if you put the key directly in the YAML it will be visible in your repository history.

Step 2: Install the Claude GitHub App

Install from: https://github.com/apps/claude

The app needs Contents, Issues, and Pull requests read/write permissions. These allow Claude to push changes, comment on PRs, and respond to issues.

Step 3: Create the workflow file

# .github/workflows/claude.yml
name: Claude Code

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]

permissions:
  contents: write
  pull-requests: write
  issues: write

jobs:
  claude:
    runs-on: ubuntu-latest
    # Only run when @claude is mentioned
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
    steps:
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

That’s the minimal working configuration. Once it’s live, you can trigger Claude from any PR or issue comment with @claude implement the changes described in this issue.

Adding custom instructions

For project-specific behavior, put a CLAUDE.md file at the repository root. Claude reads it automatically on every run. You can also pass instructions directly in the workflow:

- uses: anthropics/claude-code-action@v1
  with:
    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
    prompt: "Review this PR and flag any security issues"
    claude_args: "--max-turns 10 --model claude-sonnet-4-6"

The claude_args parameter passes flags directly to the Claude Code CLI. Use it to control turn limits, model selection, and allowed tools.


Pattern 2: Pre-Approve Tools with .claude/settings.json

When you’re running claude -p directly (not via the action), the cleanest way to avoid permission prompts is to pre-approve the tools Claude will need in a .claude/settings.json file checked into your repository.

This approach gives you surgical control: instead of skipping all permission checks, you explicitly list what Claude is allowed to do. It’s the right choice when you want CI to work reliably without opening up unnecessary access.

Basic settings.json structure

{
  "permissions": {
    "allow": [
      "Bash(npm install)",
      "Bash(npm run build)",
      "Bash(npm run test)",
      "Bash(npm run test:*)",
      "Bash(git status)",
      "Bash(git diff *)",
      "Edit(/src/**)",
      "Read(/src/**)",
      "Read(/tests/**)"
    ],
    "deny": [
      "Bash(git push *)",
      "Bash(rm -rf *)",
      "Bash(curl *)"
    ]
  }
}

Rules are evaluated in order: deny → ask → allow. The first matching rule wins. A deny rule at any level cannot be overridden by an allow rule at a lower level.

Wildcard patterns

The * wildcard matches across arguments, including spaces. A few patterns worth knowing:

RuleWhat it matches
Bash(npm run *)npm run build, npm run test:unit, any npm script
Bash(git * main)git checkout main, git push origin main
Edit(/src/**)Any file anywhere under /src/
Read(.env).env at any depth in the current directory

Important: Bash(npm run build) matches that exact command only. Bash(npm run *) matches any npm run invocation. Be specific where you want to be specific.

Permission modes

Beyond individual rules, you can set a default mode in settings:

{
  "permissions": {
    "allow": [
      "Bash(npm run *)",
      "Bash(git status)",
      "Edit(/src/**)"
    ],
    "defaultMode": "acceptEdits"
  }
}

The acceptEdits mode auto-approves file edits and common filesystem commands (mkdir, touch, mv, cp) for paths in the working directory. This cuts down on prompts without fully disabling the permission system.

Running Claude non-interactively with pre-approved tools

# .github/workflows/claude-ci.yml
name: Claude Code CI

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Run Claude Code review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          claude -p "Review the changes in this PR for correctness and potential bugs. 
          Output a summary of findings." \
            --output-format text \
            --max-turns 5

The --output-format text flag switches Claude to non-interactive output mode, which is compatible with CI environments that don’t have a TTY.


Pattern 3: bypassPermissions Mode for Isolated Containers

When your job runs in a fully isolated container with no production credentials and no access to sensitive infrastructure, you can use bypassPermissions mode to skip all permission prompts.

This is the equivalent of --dangerously-skip-permissions in older documentation. The current approach is to set it in .claude/settings.json:

{
  "permissions": {
    "defaultMode": "bypassPermissions"
  }
}

Or pass it via the command line:

- name: Run Claude Code
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
  run: |
    claude -p "Fix the failing tests and update the implementation" \
      --output-format text \
      --max-turns 20

With bypassPermissions set in settings.json, Claude will execute commands without pausing for approval.

What bypassPermissions still blocks

Even with bypass mode active, Claude Code keeps two circuit breakers:

  • rm -rf / and rm -rf ~ — filesystem root and home directory removal still prompt
  • Writes to .git, .config/git, .claude, .vscode, .husky, and similar config directories still prompt (this is a known pain point — see the Known Issues section below)

The safety tradeoff

bypassPermissions is only appropriate when the environment itself provides containment. In practice, this means:

  • Running on a fresh GitHub Actions runner (ephemeral, wiped after each job)
  • No production credentials in the environment
  • No access to sensitive external services
  • Workflow permissions scoped to the minimum necessary

A GitHub Actions job on ubuntu-latest typically satisfies these conditions. The runner is a fresh VM, it’s destroyed after the job completes, and you control which secrets it has access to.

What it does not satisfy: running Claude on your local machine with bypass mode and your full credentials in the environment. That’s where people get burned.

# .github/workflows/claude-autofix.yml
name: Claude Code Autofix

on:
  push:
    branches: [main]

permissions:
  contents: write
  pull-requests: write

jobs:
  autofix:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Run automated fix
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          claude -p "Run npm test. If tests are failing, analyze the failures and fix them. 
          Commit any changes with a descriptive message." \
            --output-format text \
            --max-turns 15

      - name: Push changes
        run: |
          git config --global user.name "Claude Code Bot"
          git config --global user.email "[email protected]"
          git push

Bonus: ANTHROPIC_API_KEY Setup and Cost Management

Storing the API key

GitHub Secrets is the only acceptable place for your API key in a CI workflow. The key goes in Settings → Secrets and variables → Actions → New repository secret.

Reference it in workflows:

env:
  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

Or as an action input:

with:
  anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

For organizations running Claude across many repositories, you can store the secret at the organization level and share it selectively across repositories. This is simpler than managing the key in each repository individually.

Controlling costs in CI

Each Claude Code run in GitHub Actions consumes API tokens. On a busy repository, this adds up. The main levers:

Set --max-turns: This caps how many back-and-forth exchanges Claude makes per job. For review tasks, 5-10 turns is usually sufficient. For implementation tasks, 15-20. Without a limit, Claude can run many more turns than you need.

claude_args: "--max-turns 10"

Use workflow concurrency limits: If multiple PRs open simultaneously, you might not want all of them triggering Claude at once.

concurrency:
  group: claude-${{ github.ref }}
  cancel-in-progress: true

Trigger selectively: The if condition on jobs lets you limit when Claude runs. The @claude mention pattern is useful because it means Claude only runs when explicitly requested, not on every push.

Set job timeouts: Add timeout-minutes to prevent runaway jobs.

jobs:
  claude:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      # ...

Which model to use

The default model for claude-code-action is Sonnet. For most CI tasks — code review, test fixing, documentation — Sonnet is the right choice: it’s fast and the cost per run is manageable.

For complex architectural work where you want maximum reasoning quality:

claude_args: "--model claude-opus-4-8"

Opus costs roughly 5x more per token than Sonnet. Reserve it for tasks where the quality difference justifies the cost.


Known Issues with --dangerously-skip-permissions in Recent Versions

If you’re running Claude Code outside the official action and using the --dangerously-skip-permissions CLI flag, be aware of two open bugs as of mid-2026:

Versions newer than v2.1.77 may ignore the flag. Users have reported that bypass mode is effectively broken in recent releases — Claude still prompts for every file edit and Bash command despite the flag being set. The workaround is to pin to v2.1.77 or use .claude/settings.json with "defaultMode": "bypassPermissions" instead of the CLI flag.

Config directory writes still prompt. Even with bypass mode active, writes to ~/.claude/ paths trigger a separate permission category that bypass mode doesn’t cover. This is tracked as a bug in the Claude Code repository. The workaround is to ensure Claude doesn’t need to write to its own config directory during CI runs — which is usually fine for code tasks.

The official claude-code-action handles these edge cases internally. If you’re hitting these bugs with a custom setup, the action is worth switching to.


Choosing the Right Pattern

SituationRecommended pattern
Team wants @claude in PRs and issuesPattern 1: claude-code-action
Automated review on every PR, no trigger neededPattern 1 with prompt parameter
Custom claude -p script in CI, specific tools neededPattern 2: settings.json allowlist
Fully isolated container, broad automationPattern 3: bypassPermissions mode
Maximum control over exactly what Claude can touchPattern 2: fine-grained allowlist rules

Most teams should start with Pattern 1. It handles the CI environment setup correctly, plays well with GitHub’s permission model, and is maintained by Anthropic. The settings.json approach (Pattern 2) is useful when you’re building a more custom pipeline. Pattern 3 is for when you need maximum autonomy in a genuinely isolated environment.


If you’re setting up Claude Code in CI, you’ll likely want to read these next:

The permission system documentation is also worth reading directly at code.claude.com/docs/en/permissions — the wildcard pattern reference is particularly useful for writing tight allowlist rules.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules