← All posts
·12 min read

Claude Code with GitHub Actions: CI/CD Workflows

Claude CodeGitHub ActionsCI/CDWorkflow
Claude Code with GitHub Actions: CI/CD Workflows

Using Claude Code to write GitHub Actions workflows

GitHub Actions sits in an awkward spot for AI coding tools. The YAML looks simple. The semantics are not. A misplaced if, a missing permissions block, or a quoted secret reference that should not be quoted will fail silently, run on the wrong trigger, or leak credentials into logs. The failure mode is rarely a syntax error. It is a workflow that runs but does the wrong thing.

Claude Code knows the action ecosystem, expression syntax, context objects, matrix and reusable patterns, and the deploy targets. What it does not know is which actions a team has approved, how secrets are scoped, which environments require approvals, and what the deploy contract looks like. Without that, Claude generates workflows that are technically valid but operationally wrong.

This guide covers the CLAUDE.md configuration and templates that produce reliable GitHub Actions output. If you have not set up Claude Code yet, the Claude Code setup guide covers installation first.

The GitHub Actions CLAUDE.md

The CLAUDE.md at your project root is read at the start of every session. For a repo where workflows are a first-class concern, it needs to answer: which actions are pre-approved, where secrets live, what the runner conventions are, and what the YAML lint rules are. The CLAUDE.md explained guide covers the broader file structure, but here is the workflow-specific section to add.

# GitHub Actions rules

## Location and structure
- All workflows in .github/workflows/, one file per pipeline
- File names: lowercase-with-dashes.yml (deploy-production.yml, test-pr.yml)
- Reusable workflows: .github/workflows/_reusable-*.yml (underscore prefix)
- Composite actions: .github/actions/{name}/action.yml

## Approved actions (pin to SHA, not version tag)
- actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a
- actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9
- actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
- actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16
- cloudflare/wrangler-action@v3 (CF Workers/Pages only)
- aws-actions/configure-aws-credentials@v4 (OIDC only, no long-lived keys)

## Hard rules
- ALL third-party actions pinned to full commit SHA, not v1 / v2 / @main
- ALL workflows declare `permissions:` at top level (default-deny)
- Secrets via ${{ secrets.NAME }}, never echoed, never passed as command-line arg
- Environment-scoped secrets for production, repo-scoped for CI
- `pull_request_target` forbidden unless explicitly approved (fork RCE risk)
- Every workflow has `concurrency:` to cancel superseded runs
- Every job has `timeout-minutes:` (default 10, max 30 unless documented)

## YAML formatting
- 2-space indent, no tabs
- Quote secret references and any string with `:` or `#`
- Job IDs kebab-case, step names sentence-case ("Run tests", not "run-tests")
- No anchors (&) or aliases (*), GitHub Actions does not fully support them

## Lint and validation
- Local: `actionlint .github/workflows/*.yml` before commit
- Pre-commit hook runs actionlint, blocks commit on violations
- CI runs actionlint on every PR touching .github/

## Runners
- Default: ubuntu-latest (ubuntu-22.04 if pinning required)
- macOS: macos-14 (Apple Silicon), macos-13 (Intel-only)
- Self-hosted: tagged `self-hosted, linux, x64` minimum

Three rules in this CLAUDE.md prevent the failure modes that account for most workflow incidents.

The SHA pinning rule is the most important. A floating tag like actions/checkout@v4 resolves to whatever commit the maintainer points it to. If that account is compromised, every workflow using the floating tag picks up malicious code on the next run. Pinning to a full SHA freezes the action at a known state. Claude pins correctly when given the SHA list, but defaults to floating tags otherwise.

The default-deny permissions rule matches the GitHub-recommended posture for GITHUB_TOKEN. Without an explicit permissions: block, the token gets the repo's default permissions, usually more than the workflow needs. Declaring permissions per workflow (or per job) limits blast radius when a step is compromised or a dependency runs untrusted code.

The pull_request_target rule addresses one of the most-exploited Actions vulnerabilities. The event runs on the base repo with full access to secrets, even when triggered by a fork's PR. If such a workflow checks out the PR's code and runs it, the fork can run arbitrary code with secrets in scope. The rule blocks the trigger by default.

Workflow file patterns

The bulk of a CI/CD setup is a small number of workflow files repeated across projects: PR validation, push-to-main testing, scheduled jobs, and deploys. Establishing the canonical shape in CLAUDE.md gives Claude a template to riff on.

Add to CLAUDE.md:

## Standard workflow shape

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

permissions:
  contents: read
  pull-requests: write  # for posting test results as PR comments

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

jobs:
  test:
    name: Test (Node ${{ matrix.node }})
    runs-on: ubuntu-latest
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        node: [20, 22]
    steps:
      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - name: Setup Node.js
        uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a
        with:
          node-version: ${{ matrix.node }}
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test -- --reporter=github-actions
      - name: Upload coverage
        uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
        with:
          name: coverage-node-${{ matrix.node }}
          path: coverage/
          retention-days: 7

The concurrency block is small but load-bearing. Without it, a developer who pushes three commits in two minutes triggers three full CI runs, each running to completion. With cancel-in-progress: true, GitHub cancels the older runs when a newer one starts, saving minutes and surfacing the latest result faster. Claude generates this consistently when it is in the template.

The timeout-minutes per job catches hung tests and runaway processes. The default GitHub timeout is 6 hours, which means a deadlocked test run consumes 360 free minutes per job before it fails. Setting a 15-minute cap ensures fast feedback and predictable cost. The Claude Code testing guide covers test runner conventions that pair well with these workflow constraints.

The --reporter=github-actions flag makes test output render as proper annotations on the PR file diff. Without it, you get a wall of stdout that nobody reads. Claude will add the right reporter flag for common test runners (Vitest, Jest, pytest with pytest-github-actions-annotate-failures) when the convention is established.

Secrets and environment variables

Secret handling is where most workflow incidents originate. A secret echoed in a debug step ends up in run logs. A secret passed as a command-line argument shows up in process listings. A secret declared at the wrong scope is available to workflows that should not see it. Claude Code respects scoping rules when they are explicit, but defaults to the loosest pattern when they are not.

Add to CLAUDE.md:

## Secret handling

### Scope hierarchy (most-restrictive first)
1. Environment-scoped secrets (Settings -> Environments -> {env} -> Secrets)
   - Use for: production deploy keys, billing API keys, signing certificates
   - Required pattern: `environment: production` on the deploy job
2. Repository-scoped secrets (Settings -> Secrets and variables -> Actions)
   - Use for: CI tokens (Codecov, Sentry release uploads)
3. Organization-scoped secrets
   - Use only when shared across 3+ repos. Document in OWNERS file.

### Hard rules
- NEVER `echo $SECRET` or `echo "${{ secrets.X }}"`, log masking is best-effort
- NEVER pass secrets as positional command args (visible in process tables)
- ALWAYS use `env:` block to pass secrets to a step, never inline interpolation
- ALWAYS reference protected environments for any prod-affecting secret
- Use OIDC for cloud providers (AWS, GCP, Azure), no long-lived keys in secrets

### Standard pattern
- name: Run deploy script
  env:
    DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
    API_BASE: ${{ vars.API_BASE }}
  run: |
    ./scripts/deploy.sh

The env: block versus inline interpolation distinction matters. Inline interpolation expands the secret directly into the shell command before it runs, which means the expanded value can appear in set -x debug output and may be visible in error messages. The env: block sets the secret as an environment variable, which is masked by the runner's log redaction layer and never appears in the rendered command. Claude generates the env: pattern reliably when CLAUDE.md establishes it as the standard. The Claude Code environment variables guide covers the broader patterns for env handling across local development and CI.

The OIDC requirement for cloud providers replaces long-lived access keys with short-lived tokens issued at workflow run time. For AWS, this means aws-actions/configure-aws-credentials with a role-to-assume ARN and no aws-access-key-id in the secrets. The setup is more involved on the cloud side (configuring the OIDC trust relationship), but it eliminates the class of incident where a leaked workflow log exposes a key that is valid for years.

Matrix builds and reusable workflows

Matrix builds and reusable workflows compound or fragment a CI/CD setup. Done well, they keep things fast and DRY. Done poorly, they produce 500-line YAML files nobody can debug.

Add to CLAUDE.md:

## Matrix patterns

### Cross-platform matrix
strategy:
  fail-fast: false
  matrix:
    os: [ubuntu-latest, macos-14, windows-latest]
    node: [20, 22]
    exclude:
      - os: windows-latest
        node: 20

### Conditional expansion (PR vs main)
strategy:
  matrix:
    node: ${{ github.event_name == 'pull_request' && fromJSON('[20]') || fromJSON('[20, 22]') }}

## Reusable workflows

### Pattern: .github/workflows/_reusable-deploy.yml
on:
  workflow_call:
    inputs:
      environment: { required: true, type: string }
      ref: { required: false, type: string, default: ${{ github.sha }} }
    secrets:
      DEPLOY_TOKEN: { required: true }

permissions:
  contents: read
  deployments: write
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps: ...

### Caller pattern
jobs:
  deploy:
    uses: ./.github/workflows/_reusable-deploy.yml
    with:
      environment: production
    secrets:
      DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}

### Hard rule: Never use `secrets: inherit` in callers
# Passes ALL caller secrets to the reusable workflow. Always enumerate.

The fail-fast: false rule for matrix builds is a debugging quality-of-life rule. The default cancels the whole matrix when one leg fails, leaving you to guess whether the other legs would have failed too. With fail-fast: false, every leg runs to completion, so you can see whether the failure is platform-specific, version-specific, or universal. Claude defaults to fail-fast: true when this is not explicit.

The secrets: inherit rule is a least-privilege rule. The convenience of inherit is that callers do not have to enumerate the secrets the reusable workflow needs. The cost is that the reusable workflow gets every secret the caller can access, including ones unrelated to its purpose. Explicit passing is more verbose but makes the dependency graph auditable. The Claude Code monorepo guide covers reusable workflow patterns for repos that ship multiple apps.

Deploying via Actions

Deploys are where workflow correctness intersects with production reliability. A broken deploy workflow ships broken code or, worse, ships nothing while reporting success. The deploy patterns below are the ones that have survived contact with production.

Add to CLAUDE.md:

# .github/workflows/deploy.yml, Vercel (preview on PR, prod on main)
name: Deploy
on:
  push: { branches: [main] }
  pull_request: { branches: [main] }

permissions:
  contents: read
  deployments: write

concurrency:
  group: deploy-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: false  # do NOT cancel in-flight deploys

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    environment:
      name: ${{ github.event_name == 'push' && 'production' || 'preview' }}
      url: ${{ steps.deploy.outputs.deployment-url }}
    env:
      PROD: ${{ github.event_name == 'push' && '--prod' || '' }}
      VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - run: npm install -g vercel@latest
      - run: vercel pull --yes --environment=${{ github.event_name == 'push' && 'production' || 'preview' }} --token=$VERCEL_TOKEN
      - run: vercel build $PROD --token=$VERCEL_TOKEN
      - id: deploy
        run: |
          URL=$(vercel deploy --prebuilt $PROD --token=$VERCEL_TOKEN)
          echo "deployment-url=$URL" >> $GITHUB_OUTPUT
### Cloudflare Workers / Pages deploy
- name: Deploy to Cloudflare
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
    command: deploy --env=${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
### AWS deploy via OIDC (no long-lived keys)
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
    aws-region: us-east-1
- name: Deploy via CDK
  run: |
    npm ci
    npx cdk deploy --require-approval never --all
  env:
    NODE_ENV: production

The `concurrency: cancel-in-progress: false` for deploys is the inverse of the CI rule. CI runs are safe to cancel because they are read-only. Deploys mid-flight should not be cancelled because cancelling a half-deployed change leaves production in a corrupted state. The same group still serializes deploys, but each runs to completion. The [Claude Code Vercel guide](/blog/claude-code-vercel) and the [Claude Code Cloudflare Workers guide](/blog/claude-code-cloudflare-workers) cover provider-specific edge cases.

The Vercel deploy uses the CLI rather than `vercel/action` for two reasons. First, it gives direct control over the build step (split build and deploy into separate jobs to share the artifact). Second, it captures the deployment URL into a step output, which the `environment.url` field surfaces as a direct link from the GitHub UI. The [Claude Code deploy guide](/blog/claude-code-deploy) covers deployment patterns across providers.

The OIDC AWS pattern is worth implementing even if a team has been using long-lived keys for years. The IAM trust policy for the GitHub Actions role uses the OIDC token's claims (repo, branch, environment) to scope what the role can do. A misconfigured workflow on a feature branch cannot assume the production deploy role because the OIDC claims do not match.

## Calling Claude programmatically inside Actions

A separate use case from "writing workflows with Claude Code" is "calling Claude inside a workflow." This is where workflows automate review, documentation generation, or PR comment responses using the Claude API directly. Anthropic's `claude-code-action` and the Anthropic SDK make this straightforward.

```yaml
# .github/workflows/claude-pr-review.yml
name: Claude PR review

on:
  pull_request:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write
  id-token: write

concurrency:
  group: claude-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
        with:
          fetch-depth: 0  # full history for diff context
      - name: Run Claude review
        uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          prompt: "Review the diff in this PR. Comment on bugs, security issues, and unclear logic. Skip style nitpicks."
          allowed_tools: "Bash(git diff:*),Read,Grep"
          max_turns: 5

The allowed_tools field is the workflow equivalent of .claude/settings.local.json permissions. It restricts Claude to a known-safe set of tools for the duration of the workflow run, so a prompt-injected review request cannot escalate into running arbitrary shell commands. The pattern matches the same defence-in-depth approach as workflow permissions: blocks. The Claude Code best practices guide covers permission scoping principles that apply identically here.

For more bespoke needs, calling the Anthropic SDK directly from a workflow step gives you full control:

- name: Generate release notes
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
  run: |
    npm install @anthropic-ai/sdk
    node scripts/generate-release-notes.js > release-notes.md
- name: Upload release notes
  uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
  with:
    name: release-notes
    path: release-notes.md

The artifact pattern is how outputs flow between jobs in the same workflow. The release-notes job produces a markdown file as an artifact, which a downstream publish-release job can download and use as the release body. This decouples generation from publishing, so you can re-run only the failing step on a transient failure.

Hard rules and conclusion

The configuration above compounds. Pinned actions, permissions: defaults, concurrency: blocks, the env: secret pattern, and matrix conventions each handle a category of failure that recurs across teams. With them in CLAUDE.md, Claude generates workflows that pass actionlint, follow least-privilege defaults, cancel superseded runs without orphaning deploys, and handle secrets through scoped environments rather than echo statements.

Three rules deserve repeating because they account for the highest-severity incidents in real teams. SHA-pin every third-party action, no exceptions. Declare permissions: explicitly on every workflow, default to read-all, elevate per-job. Never use pull_request_target with checked-out fork code unless the reason is deliberate and reviewed. The first prevents supply-chain compromise. The second limits blast radius. The third closes the most-exploited RCE vector in the Actions ecosystem.

For broader workflow integration, the Claude Code git workflow guide covers branching and commit conventions. For build steps in Actions, the Claude Code TypeScript guide covers type-checking and bundling. For debugging when a workflow does the wrong thing, the Claude Code debugging guide covers isolating CI failures systematically.

A repo without a workflow CLAUDE.md produces floating action tags, missing permissions, secrets passed as command arguments, and matrix builds that cancel on first failure. A repo with the configuration above produces workflows that lint clean, deploy reliably, and survive the long tail of edge cases that only show up in production. Claudify includes a GitHub Actions CLAUDE.md template in the Claude Code workflow kit, with the SHA pin list, deploy templates for Vercel, Cloudflare, and AWS, and the actionlint pre-commit hook configured.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir