Skip to content
Santekno.com | Level Up Your Engineering Skills
EN
📖 0%
09 Oct 2026 · 25 min read ·Article 63 / 208
Go

Setting Up Claude Code in GitHub Actions for Golang: Auth, CLAUDE.md, API

A complete guide to setting up Claude Code in GitHub Actions for a Go project. Configure the API key, access CLAUDE.md in CI, and make the first Claude API call from the pipeline.

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

This is the first implementation article in Topic 4, and its focus is setting up Claude Code in GitHub Actions for golang from scratch. After the blueprint in Article 02, now we go hands-on: getting Claude ready to use in GitHub Actions for Santekno Shop.

There are three different approaches to using Claude in CI. The summary below maps all three along with the situation each approach fits best.

text
 1Approach A: GitHub Copilot PR Review
 2  → Built-in action from GitHub
 3  → Easiest to set up
 4  → Best for: automated PR review
 5
 6Approach B: Claude API via HTTP (curl/script)
 7  → Flexible, allows custom prompts
 8  → Best for: PR description, failure analysis, release notes
 9
10Approach C: Claude Code CLI on the Runner
11  → Full Claude Code experience in CI
12  → Best for: complex multi-file analysis
13  → Requires Node.js on the runner

In Topic 4 we’ll use all three according to the use case. This article covers all three setups completely.


03.1 Approach A: GitHub Copilot PR Review

This is the easiest option if the team already subscribes to GitHub Copilot Business/Enterprise. The workflow below enables automated Copilot review on every PR that touches a Go file, complete with its review instructions.

yaml
 1# Setup required: NONE — the action is already available
 2# What needs configuring: permissions and copilot-instructions.md
 3
 4# .github/workflows/ai-review.yml
 5name: Copilot PR Review
 6
 7on:
 8  pull_request:
 9    types: [opened, synchronize, ready_for_review]
10    paths: ['**.go']
11
12jobs:
13  copilot-review:
14    runs-on: ubuntu-latest
15    if: "!github.event.pull_request.draft"
16    permissions:
17      pull-requests: write
18      contents: read
19    steps:
20      - uses: actions/checkout@v4
21        with:
22          fetch-depth: 0
23
24      - name: Copilot Code Review
25        uses: github/copilot-for-pull-requests@v1
26        with:
27          github-token: ${{ secrets.GITHUB_TOKEN }}
28          review-type: "code-review"
29          review-instructions: |
30            Review Go code for the Santekno Shop production service.
31            Read and follow all rules in .github/copilot-instructions.md.
32
33            CRITICAL — block the PR if found:
34            1. Error not wrapped: `return err` (must be `fmt.Errorf("pkg.Method: %w", err)`)
35            2. float64/float32 for monetary values (must be int64 cents)
36            3. Architecture violation: handler imports repository implementation
37            4. Repository returns an error for not-found (must return nil, nil)
38            5. Error ignored with _ in non-test code
39
40            SUGGESTION — informational:
41            6. Missing test for error paths
42            7. Missing godoc for exported symbols
43            8. Context not passed to downstream calls
44
45            End with: REVIEW_SCORE: X/100

The quality of Copilot’s review depends heavily on the context file that accompanies it. The copilot-instructions.md file below translates the CLAUDE.md rules into concise rules for Copilot’s review style.

markdown
 1# .github/copilot-instructions.md
 2# Santekno Shop — Copilot Review Rules
 3# Derived from CLAUDE.md
 4
 5## Architecture
 6Clean Architecture: handler → usecase → repository → domain
 7
 8## Error Handling (CRITICAL)
 9Repository not-found: MUST return (nil, nil)
10Error wrap: MUST use fmt.Errorf("package.Method: %w", err)
11HTTP error format: {"error": "UPPERCASE_CODE"}
12
13## Type Rules (CRITICAL)
14Monetary: ALWAYS int64 cents (money.IDR)
15IDs: ALWAYS uuid.UUID
16
17## Testing
18testify/suite + gomock pattern
19
20## Go Version: 1.22 — no 1.23+ features

With the combination of this workflow and the instructions file, Copilot stops giving generic feedback and starts enforcing Santekno Shop’s specific conventions — without a single line of API code you need to write.


03.2 Approach B: Claude API via HTTP

For use cases that need custom prompts — PR description, failure analysis, release notes — we call the Claude API directly via HTTP. First, register the API key in GitHub Secrets as shown below.

bash
1# Setup: add ANTHROPIC_API_KEY to GitHub Secrets
2gh secret set ANTHROPIC_API_KEY --body "sk-ant-api03-xxx"
3
4# Verify:
5gh secret list
6# Output: ANTHROPIC_API_KEY  Updated 2026-07-15

Once the key is stored, we can call it from any step. The reusable step template below shows the basic pattern for calling the Claude API with curl and parsing its response with jq.

yaml
 1# Reusable step template:
 2# Use this pattern in every workflow that needs Claude
 3
 4- name: Claude API Call
 5  env:
 6    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
 7  run: |
 8    # Build prompt (replace with a specific prompt)
 9    PROMPT="Your specific prompt here"
10
11    # Call Claude API
12    RESPONSE=$(curl -sf -X POST https://api.anthropic.com/v1/messages \
13      -H "x-api-key: ${ANTHROPIC_API_KEY}" \
14      -H "anthropic-version: 2023-06-01" \
15      -H "content-type: application/json" \
16      -d "$(jq -n \
17        --arg prompt "$PROMPT" \
18        '{
19          model: "claude-haiku-4-5-20251001",
20          max_tokens: 500,
21          messages: [{"role": "user", "content": $prompt}]
22        }'
23      )")
24
25    # Extract text from the response
26    RESULT=$(echo "$RESPONSE" | jq -r '.content[0].text')
27    echo "$RESULT"

This inline pattern works, but repeating the same curl block across many steps quickly gets messy — that’s the problem we solve in the next section with a wrapper script.


03.3 A Wrapper Script for the Claude API

To avoid repeating curl in every step, we wrap the API-calling logic into one reusable script. The claude-call.sh script below takes a prompt, max tokens, and model — complete with API error handling.

bash
 1#!/bin/bash
 2# scripts/claude-call.sh
 3# Usage: scripts/claude-call.sh "Your prompt" [max_tokens] [model]
 4
 5set -euo pipefail
 6
 7PROMPT="${1}"
 8MAX_TOKENS="${2:-500}"
 9MODEL="${3:-claude-haiku-4-5-20251001}"
10
11if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
12  echo "ERROR: ANTHROPIC_API_KEY not set" >&2
13  exit 1
14fi
15
16RESPONSE=$(curl -sf -X POST https://api.anthropic.com/v1/messages \
17  -H "x-api-key: ${ANTHROPIC_API_KEY}" \
18  -H "anthropic-version: 2023-06-01" \
19  -H "content-type: application/json" \
20  -d "$(jq -n \
21    --arg p "$PROMPT" \
22    --argjson t "$MAX_TOKENS" \
23    --arg m "$MODEL" \
24    '{
25      model: $m,
26      max_tokens: $t,
27      messages: [{"role": "user", "content": $p}]
28    }'
29  )" 2>&1) || {
30  echo "ERROR: Claude API call failed: $RESPONSE" >&2
31  exit 1
32}
33
34# Check for an error response from the API
35if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then
36  ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message')
37  echo "ERROR: Claude API error: $ERROR_MSG" >&2
38  exit 1
39fi
40
41# Return content text
42echo "$RESPONSE" | jq -r '.content[0].text'

Before using it in CI, it must be tested locally first so you don’t end up debugging in the Actions log. The commands below mark the script as executable and run a simple smoke test.

bash
1# Make executable and test:
2chmod +x scripts/claude-call.sh
3
4# Test locally:
5export ANTHROPIC_API_KEY="sk-ant-xxx"
6./scripts/claude-call.sh "Say 'CI ready' in one word" 50
7# Output: Ready

With this wrapper, every subsequent script simply calls claude-call.sh instead of duplicating the curl logic — one place for error handling, model selection, and JSON parsing.


03.4 Integrating CLAUDE.md into the CI Context

CLAUDE.md is the context file Claude needs to read to give a relevant review. In CI, we inject CLAUDE.md into every prompt via the claude-review.sh script below — which builds a different prompt for both general and security reviews.

bash
 1#!/bin/bash
 2# scripts/claude-review.sh
 3# Claude review with CLAUDE.md as context
 4
 5set -euo pipefail
 6
 7DIFF_FILE="${1}"
 8REVIEW_TYPE="${2:-general}"  # general, security, spec
 9
10# Load CLAUDE.md as context
11CLAUDE_CONTEXT=""
12if [ -f "CLAUDE.md" ]; then
13  CLAUDE_CONTEXT=$(cat CLAUDE.md | head -200)  # first 200 lines
14fi
15
16# Load service-specific context if it exists
17SERVICE_CONTEXT=""
18if [ -f "services/order-service/CLAUDE.md" ]; then
19  SERVICE_CONTEXT=$(cat "services/order-service/CLAUDE.md")
20fi
21
22# Build review diff
23DIFF=$(cat "$DIFF_FILE" | head -300)  # limit for cost control
24
25# Build prompt based on review type
26case "$REVIEW_TYPE" in
27  "general")
28    PROMPT="Project context (CLAUDE.md):
29${CLAUDE_CONTEXT}
30
31Review the following Go code diff. Check for CRITICAL issues:
321. Error not wrapped (return err without fmt.Errorf)
332. float64 for monetary values
343. Architecture layer violations
354. Nil not checked after a repo call
365. Errors ignored with _
37
38Diff:
39${DIFF}
40
41Output format:
42CRITICAL: [issue] at [file:line] → [fix]
43SUGGESTION: [improvement]
44REVIEW_SCORE: [0-100]"
45    ;;
46
47  "security")
48    PROMPT="Review the Go code diff for security issues:
491. SQL injection via string concatenation
502. Hardcoded credentials
513. Missing input validation
524. Path traversal
535. Integer overflow for financial calculations
54
55Diff:
56${DIFF}
57
58Mark: BLOCKING (immediate fix) / WARNING / OK"
59    ;;
60esac
61
62# Call Claude
63RESULT=$(./scripts/claude-call.sh "$PROMPT" 800)
64echo "$RESULT"
65
66# Check if there's a CRITICAL or BLOCKING
67if echo "$RESULT" | grep -qE "^(CRITICAL|BLOCKING):"; then
68  echo ""
69  echo "::warning::AI review found critical issues"
70  exit 1  # fail the step
71fi
72
73exit 0

Note the first twenty lines: without injecting CLAUDE.md, the AI only uses generic Go conventions; with that context, it knows project-specific rules like the ban on float64 for money and the nil/nil pattern for not-found.


03.5 Workflow: AI Review with CLAUDE.md Context

Once we have the script, we assemble it into a complete workflow that fetches the diff, runs the review, and posts the result as a PR comment. The workflow below unites all those steps — from diff extraction to the score gate.

yaml
 1# .github/workflows/ai-code-review.yml
 2name: AI Code Review
 3
 4on:
 5  pull_request:
 6    types: [opened, synchronize, ready_for_review]
 7    paths: ['**.go']
 8
 9env:
10  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
11
12jobs:
13  claude-review:
14    name: Claude Code Review
15    runs-on: ubuntu-latest
16    if: "!github.event.pull_request.draft"
17    permissions:
18      pull-requests: write
19      contents: read
20    steps:
21      - uses: actions/checkout@v4
22        with:
23          fetch-depth: 0
24
25      - name: Get Go diff
26        id: diff
27        run: |
28          # Get diff from the PR
29          git diff origin/${{ github.base_ref }}...HEAD -- '*.go' \
30            | head -500 > /tmp/pr-diff.txt
31
32          # Check whether there are Go changes
33          if [ ! -s /tmp/pr-diff.txt ]; then
34            echo "no_changes=true" >> $GITHUB_OUTPUT
35          else
36            echo "no_changes=false" >> $GITHUB_OUTPUT
37            echo "diff_size=$(wc -l < /tmp/pr-diff.txt)" >> $GITHUB_OUTPUT
38          fi
39
40      - name: AI Review
41        id: review
42        if: steps.diff.outputs.no_changes != 'true'
43        run: |
44          chmod +x scripts/claude-review.sh
45          REVIEW=$(./scripts/claude-review.sh /tmp/pr-diff.txt general) || REVIEW_FAILED=true
46
47          echo "review_output<<EOF" >> $GITHUB_OUTPUT
48          echo "$REVIEW" >> $GITHUB_OUTPUT
49          echo "EOF" >> $GITHUB_OUTPUT
50
51          # Extract score
52          SCORE=$(echo "$REVIEW" | grep "REVIEW_SCORE:" | grep -oP '\d+' || echo "0")
53          echo "score=$SCORE" >> $GITHUB_OUTPUT
54          echo "failed=${REVIEW_FAILED:-false}" >> $GITHUB_OUTPUT
55
56      - name: Post Review Comment
57        if: steps.diff.outputs.no_changes != 'true'
58        uses: actions/github-script@v7
59        with:
60          script: |
61            const review = `${{ steps.review.outputs.review_output }}`;
62            const score = parseInt('${{ steps.review.outputs.score }}') || 0;
63
64            const scoreEmoji = score >= 90 ? '🟢' : score >= 70 ? '🟡' : '🔴';
65
66            const body = [
67              `## 🤖 AI Code Review`,
68              ``,
69              `**Score:** ${scoreEmoji} ${score}/100`,
70              ``,
71              review,
72              ``,
73              `---`,
74              `*Reviewed by Claude claude-haiku-4-5-20251001 | [Ignore this review](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests)*`
75            ].join('\n');
76
77            await github.rest.issues.createComment({
78              owner: context.repo.owner,
79              repo: context.repo.repo,
80              issue_number: context.issue.number,
81              body
82            });
83
84      - name: Check Score Gate
85        if: |
86          steps.diff.outputs.no_changes != 'true' &&
87          steps.review.outputs.failed == 'true'
88        run: |
89          SCORE=${{ steps.review.outputs.score }}
90          echo "AI Review Score: $SCORE/100"
91
92          # Phase 1: warning only
93          # Phase 2 (uncomment): block if there's a CRITICAL
94          # if echo "$REVIEW" | grep -q "^CRITICAL:"; then
95          #   echo "::error::PR has CRITICAL issues that must be fixed"
96          #   exit 1
97          # fi
98
99          echo "::warning::AI review issues found — please review the comment above"

Note the “Check Score Gate” step: in the early phase it only emits a warning, and the merge block (the commented-out lines) is only enabled once the team gets used to it — exactly the spirit of graduated enforcement from Article 02.


03.6 Approach C: Claude Code CLI on the Runner

For use cases that need the full Claude Code experience — multi-file analysis, spec-aware — we install the Claude Code CLI directly on the runner. The workflow below installs the CLI, configures auth, runs the analysis, and then posts the result to the PR.

yaml
 1# .github/workflows/claude-code-ci.yml
 2name: Claude Code Analysis
 3
 4on:
 5  pull_request:
 6    paths: ['**.go', 'CLAUDE.md', '.specify/**']
 7
 8jobs:
 9  claude-code:
10    runs-on: ubuntu-latest
11    permissions:
12      pull-requests: write
13      contents: read
14    steps:
15      - uses: actions/checkout@v4
16        with:
17          fetch-depth: 0
18
19      - uses: actions/setup-go@v5
20        with:
21          go-version: '1.22'
22          cache: true
23
24      # Install Node.js for the Claude Code CLI
25      - uses: actions/setup-node@v4
26        with:
27          node-version: '20'
28
29      # Install Claude Code
30      - name: Install Claude Code
31        run: npm install -g @anthropic-ai/claude-code
32
33      # Setup API key
34      - name: Configure Claude Code
35        env:
36          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
37        run: |
38          # Claude Code automatically reads ANTHROPIC_API_KEY from the environment
39          claude --version
40
41      # Run Claude Code analysis
42      - name: Claude Code Architecture Review
43        env:
44          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
45        run: |
46          # Non-interactive mode for CI
47          claude --print "
48            Read CLAUDE.md and analyze the changes in this PR.
49
50            Run checks:
51            1. go build ./... (verify the build doesn't break)
52            2. Review the diff for architecture violations
53            3. Check spec compliance (if .specify/ exists)
54
55            Output format:
56            BUILD: [PASS/FAIL]
57            ARCHITECTURE: [PASS/FAIL + issues if any]
58            SPEC: [COMPLIANT/NON-COMPLIANT + details]
59            OVERALL: [PASS/FAIL]
60          " --output-format text > /tmp/claude-analysis.txt 2>&1
61
62          cat /tmp/claude-analysis.txt
63
64      # Post result to the PR
65      - name: Post Claude Code Analysis
66        uses: actions/github-script@v7
67        with:
68          script: |
69            const fs = require('fs');
70            const analysis = fs.readFileSync('/tmp/claude-analysis.txt', 'utf8');
71
72            await github.rest.issues.createComment({
73              owner: context.repo.owner,
74              repo: context.repo.repo,
75              issue_number: context.issue.number,
76              body: `## 🤖 Claude Code Analysis\n\n\`\`\`\n${analysis}\n\`\`\``
77            });

The advantage of this approach: claude --print runs non-interactively and reads CLAUDE.md automatically from the repo root, so it’s well suited to multi-file analysis that needs full context — at the added cost of installing Node.js on the runner.


03.7 Rate Limiting and Error Handling

In a busy CI, API calls can hit a rate limit or a server overload. The script below wraps curl with retry and exponential backoff so a temporary failure doesn’t immediately fail the pipeline.

bash
 1#!/bin/bash
 2# scripts/claude-call-with-retry.sh
 3# Claude API call with retry for rate limit handling
 4
 5set -euo pipefail
 6
 7PROMPT="${1}"
 8MAX_TOKENS="${2:-500}"
 9MAX_RETRIES=3
10RETRY_DELAY=5  # seconds
11
12for attempt in $(seq 1 $MAX_RETRIES); do
13  RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
14    https://api.anthropic.com/v1/messages \
15    -H "x-api-key: ${ANTHROPIC_API_KEY}" \
16    -H "anthropic-version: 2023-06-01" \
17    -H "content-type: application/json" \
18    -d "$(jq -n \
19      --arg p "$PROMPT" \
20      --argjson t "$MAX_TOKENS" \
21      '{
22        model: "claude-haiku-4-5-20251001",
23        max_tokens: $t,
24        messages: [{"role": "user", "content": $p}]
25      }'
26    )")
27
28  HTTP_CODE=$(echo "$RESPONSE" | tail -1)
29  BODY=$(echo "$RESPONSE" | head -n -1)
30
31  case "$HTTP_CODE" in
32    200)
33      echo "$BODY" | jq -r '.content[0].text'
34      exit 0
35      ;;
36    429)
37      # Rate limit — wait and retry
38      echo "Rate limited (attempt $attempt/$MAX_RETRIES), waiting ${RETRY_DELAY}s..." >&2
39      sleep $RETRY_DELAY
40      RETRY_DELAY=$((RETRY_DELAY * 2))  # exponential backoff
41      ;;
42    529)
43      # Overloaded — retry with backoff
44      echo "API overloaded (attempt $attempt/$MAX_RETRIES), waiting ${RETRY_DELAY}s..." >&2
45      sleep $RETRY_DELAY
46      ;;
47    *)
48      echo "ERROR: HTTP $HTTP_CODE: $BODY" >&2
49      exit 1
50      ;;
51  esac
52done
53
54echo "ERROR: All $MAX_RETRIES attempts failed" >&2
55exit 1

The design key here: only HTTP 429 and 529 are retried with backoff, while other errors fail immediately — so the pipeline doesn’t waste time retrying mistakes that won’t be fixed by waiting.


03.8 Caching AI Review Results

When several commits are pushed to the same PR without changing Go code, re-running the review is a waste. The cache configuration below stores the review result per combination of SHA and Go file hash, so the same review isn’t paid for twice.

yaml
 1# Cache the AI review for the same commit (avoid double billing)
 2- name: Check review cache
 3  id: cache
 4  uses: actions/cache@v4
 5  with:
 6    path: /tmp/ai-review-cache
 7    key: ai-review-${{ github.sha }}-${{ hashFiles('**/*.go') }}
 8
 9- name: Run AI Review
10  if: steps.cache.outputs.cache-hit != 'true'
11  env:
12    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
13  run: |
14    mkdir -p /tmp/ai-review-cache
15
16    # Run review
17    REVIEW=$(./scripts/claude-review.sh /tmp/pr-diff.txt general)
18
19    # Save to cache
20    echo "$REVIEW" > /tmp/ai-review-cache/result.txt
21
22- name: Read Review Result
23  run: |
24    # Read from cache (or from the previous run)
25    REVIEW=$(cat /tmp/ai-review-cache/result.txt)
26    echo "$REVIEW"

With a cache key that locks onto the Go file hash, the review only re-runs when the code actually changes — every cache hit saves both API cost and pipeline time.


03.9 Testing the Setup Locally Before CI

Before pushing changes to CI, all scripts should be validated locally so you don’t get stuck debugging in the Actions log. The sequence of commands below tests API connectivity, the review script, the architecture check, and finally ensures no key leaks into the YAML.

bash
 1# Validate that all scripts run correctly before pushing to CI
 2
 3# 1. Test claude-call.sh
 4export ANTHROPIC_API_KEY="sk-ant-xxx"
 5./scripts/claude-call.sh "Say 'test passed' in 3 words" 20
 6# Expected output: "The test passed" or similar
 7
 8# 2. Test claude-review.sh with a sample diff
 9git diff HEAD~1 -- '*.go' > /tmp/test-diff.txt
10./scripts/claude-review.sh /tmp/test-diff.txt general
11# Expected: output with CRITICAL/SUGGESTION/SCORE
12
13# 3. Test the architecture check
14go run scripts/check-architecture.go ./...
15# Expected: "Architecture check: PASSED" or a violation list
16
17# 4. Simulate the CI environment
18act -j claude-review  # use the 'act' tool to run Actions locally
19# Install act: brew install act
20
21# 5. Verify the secret isn't exposed to logs
22grep -r "sk-ant" .github/ 2>/dev/null && echo "WARNING: API key in YAML!" || echo "✅ No hardcoded keys"

Step 4 with act is very valuable: it runs the workflow locally so you can validate the full CI behavior before a single commit touches GitHub.


03.10 Monitoring and Alerting for CI Claude Usage

Once AI is active in the pipeline, you need to monitor its usage to keep costs under control. The configuration below records usage to the GitHub Step Summary on every run and sets up a hook for a weekly cost report.

yaml
 1# Track token usage on every run
 2- name: Track AI usage
 3  if: always()
 4  run: |
 5    # Log usage to the GitHub Step Summary
 6    cat >> $GITHUB_STEP_SUMMARY << EOF
 7    ## AI Usage
 8    - Model: claude-haiku-4-5-20251001
 9    - PR: #${{ github.event.number }}
10    - Author: ${{ github.actor }}
11    - Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)
12    EOF
13
14    # Optional: send to a monitoring system
15    # curl -X POST https://monitoring.santekno.com/api/ai-usage ...
16
17# Weekly cost report via a scheduled job
18- name: Weekly AI Cost Report
19  if: github.event_name == 'schedule'
20  run: |
21    echo "Monthly AI tool costs:"
22    echo "Review the Anthropic dashboard: https://console.anthropic.com/settings/usage"
23    echo "Review GitHub Copilot: https://github.com/organizations/santekno/settings/billing"

By recording the model and timestamp on every run, you have an audit trail to compare estimates against actual usage in the Anthropic console each month.


03.11 Troubleshooting Common Issues

Setting up AI in CI almost always surfaces errors on the first try. The set of solutions below maps the most common problems — from permission denied to false positives — along with how to fix them.

bash
 1# Issue 1: "Permission denied" when running a script
 2chmod +x scripts/claude-call.sh scripts/claude-review.sh
 3git add scripts/*.sh
 4git commit -m "chore: make scripts executable"
 5
 6# Issue 2: "ANTHROPIC_API_KEY not set" in CI
 7# Check: is the secret set?
 8gh secret list | grep ANTHROPIC
 9# If missing: gh secret set ANTHROPIC_API_KEY --body "sk-ant-xxx"
10
11# Issue 3: jq not available on the runner
12# ubuntu-latest already includes jq, but if not:
13sudo apt-get install -y jq
14
15# Issue 4: "curl: (6) Could not resolve host"
16# Check: does the runner have internet access?
17# A self-hosted runner might not — check the network policy
18
19# Issue 5: "content[0].text not found"
20# Debug:
21RESPONSE=$(curl ... api.anthropic.com ...)
22echo "$RESPONSE" | jq .  # see the full response structure
23
24# Issue 6: Pipeline too slow
25# Identify the bottleneck:
26# GitHub Actions → workflow run → look at each job's timeline
27# The bottleneck is usually: go test (needs cache), golangci-lint (needs cache)
28
29# Issue 7: False positives from AI review
30# Solution: update copilot-instructions.md or the review prompt
31# Add: "Exception: X is intentional because Y"

The two most common issues: forgetting chmod +x (Issue 1) and a secret that hasn’t been set (Issue 2) — both fail the first run, and both have a one-line fix.


03.12 Security Hardening for CI

Because the diff and secrets pass through the pipeline, security hardening is not optional. The configuration below applies five core practices: pin actions, minimal permissions, don’t echo secrets, validate input, and set timeouts.

yaml
 1# Security best practices in GitHub Actions:
 2
 3# 1. Pin action versions to a commit hash (not a tag)
 4- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
 5
 6# 2. Minimal permissions per job
 7permissions:
 8  contents: read          # the minimum needed
 9  pull-requests: write    # only for the job that needs it
10
11# 3. Don't expose secrets to output
12- name: Claude API Call
13  env:
14    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
15  run: |
16    # DON'T: echo $ANTHROPIC_API_KEY
17    # DO: use it directly in the command, don't echo it
18
19# 4. Validate input before sending to AI
20- name: Validate diff before AI review
21  run: |
22    # Check the diff size (too big → skip or chunk)
23    DIFF_SIZE=$(wc -c < /tmp/pr-diff.txt)
24    if [ "$DIFF_SIZE" -gt 50000 ]; then
25      echo "::warning::Diff too big ($DIFF_SIZE bytes), skip AI review"
26      exit 0  # graceful skip, don't fail
27    fi
28
29    # Redact secrets from the diff before sending to AI
30    sed -i 's/sk-ant-[a-zA-Z0-9_-]*/sk-ant-REDACTED/g' /tmp/pr-diff.txt
31    sed -i 's/ghp_[a-zA-Z0-9]*/ghp_REDACTED/g' /tmp/pr-diff.txt
32
33# 5. Timeout to prevent hanging
34jobs:
35  claude-review:
36    timeout-minutes: 10    # maximum 10 minutes
37    steps:
38      - name: Claude API Call
39        timeout-minutes: 3   # per-step timeout

The step that redacts secrets before the diff is sent to AI is defense in depth: even if an API key or token accidentally lands in the diff, it’s already masked before it leaves the runner.


03.13 Multi-Environment Setup

Some teams want to separate AI costs between CI for production and development. The configuration below selects a different API key based on the branch, so usage per environment can be tracked separately.

yaml
 1# Different API key for different environments (optional)
 2# Useful if you want to track cost per environment
 3
 4# Production CI (main branch):
 5secrets.ANTHROPIC_API_KEY_PROD
 6
 7# Development CI (feature branches):
 8secrets.ANTHROPIC_API_KEY_DEV
 9
10# Implementation:
11- name: Set API key based on branch
12  run: |
13    if [ "${{ github.ref }}" == "refs/heads/main" ]; then
14      echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY_PROD }}" >> $GITHUB_ENV
15    else
16      echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY_DEV }}" >> $GITHUB_ENV
17    fi

Separating the keys is optional, but it’s useful when you need to answer a question like “how much AI cost is specifically for PRs to main?” without guessing from a single combined bill.


03.14 Reusable Workflow for Multi-Repo

In a monorepo or multi-repo setup, copying the same workflow into every service quickly becomes a maintenance nightmare. The reusable workflow below centralizes the review logic so other repos only need a few lines to call it.

yaml
 1# .github/workflows/reusable-ai-review.yml
 2# A reusable workflow that can be called from other repos
 3
 4name: Reusable AI Review
 5
 6on:
 7  workflow_call:
 8    inputs:
 9      go_version:
10        default: '1.22'
11        type: string
12      review_model:
13        default: 'claude-haiku-4-5-20251001'
14        type: string
15      fail_on_critical:
16        default: false
17        type: boolean
18    secrets:
19      anthropic_api_key:
20        required: true
21
22jobs:
23  ai-review:
24    runs-on: ubuntu-latest
25    permissions:
26      pull-requests: write
27      contents: read
28    steps:
29      - uses: actions/checkout@v4
30        with: { fetch-depth: 0 }
31
32      - name: Run AI Review
33        env:
34          ANTHROPIC_API_KEY: ${{ secrets.anthropic_api_key }}
35          REVIEW_MODEL: ${{ inputs.review_model }}
36        run: |
37          # ... review logic
38
39# Usage from another repo:
40# .github/workflows/ci.yml (in repo santekno-payment-service)
41jobs:
42  ai-review:
43    uses: santekno/.github/.github/workflows/reusable-ai-review.yml@main
44    with:
45      go_version: '1.22'
46      fail_on_critical: true
47    secrets:
48      anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

With this pattern, one change to the reusable workflow automatically applies to all services that call it — eliminating the duplication that has been the source of drift between repos.


03.15 Complete Setup Checklist

Before considering the setup done, it helps to go back through all the steps via a checklist. The list below groups the verification per approach (A/B/C) plus the security section.

text
 1Setup checklist for AI review in GitHub Actions:
 2
 3Approach A (Copilot):
 4□ GitHub Copilot Business/Enterprise subscription active
 5□ .github/copilot-instructions.md created from CLAUDE.md
 6□ Workflow file .github/workflows/ai-review.yml created
 7□ Test: create a small PR, verify the Copilot comment appears
 8
 9Approach B (Claude API):
10□ ANTHROPIC_API_KEY set in GitHub Secrets
11□ scripts/claude-call.sh created and executable
12□ scripts/claude-review.sh created with CLAUDE.md injection
13□ Workflow file with proper permissions
14□ Test: push a PR, verify the AI comment appears
15
16Approach C (Claude Code CLI):
17□ Node.js 20 available on the runner
18□ npm install -g @anthropic-ai/claude-code succeeds
19□ CLAUDE.md at the repository root
20□ Test: run claude --print "test" on the runner
21
22Security:
23□ No API key hardcoded in the YAML
24□ Minimal permissions per job (pull-requests: write only where needed)
25□ Diff redacted of secrets before sending to AI
26□ Timeout set for all AI steps
27□ Action versions pinned to a commit hash

The Security section of this checklist is not filler: the last four items are the most often missed and the most risky if ignored.


03.16 End-to-End Verification

A new setup is only truly proven to work when tested end-to-end with a deliberate violation. The scenario below creates a branch with intentionally wrong code, opens a PR, and then verifies that the AI flags the violation.

bash
 1# Test the setup end-to-end:
 2
 3# 1. Create a branch and make an intentionally wrong change
 4git checkout -b test/ai-review-setup
 5
 6# Create a file with an intentional violation
 7cat > /tmp/test_order.go << 'GOFILE'
 8package usecase
 9
10import "errors"
11
12func BadFunction() error {
13    err := doSomething()
14    return err  // WRONG: not wrapped
15}
16
17type Order struct {
18    Price float64  // WRONG: should be int64
19}
20GOFILE
21
22cp /tmp/test_order.go internal/usecase/order/test_order.go
23git add internal/usecase/order/test_order.go
24git commit -m "test: intentional violations for AI review testing"
25git push origin test/ai-review-setup
26
27# 2. Create a PR on GitHub
28gh pr create --title "Test: AI Review Setup" \
29  --body "Testing AI review pipeline setup" \
30  --base develop
31
32# 3. Verify the AI review appears as a PR comment
33# Expected: AI flags CRITICAL for float64 and the unwrapped error
34gh pr view --json comments
35
36# 4. Cleanup
37git checkout develop
38git branch -d test/ai-review-setup
39gh pr close --delete-branch

If the AI flags both violations — float64 for price and the unwrapped error — as CRITICAL, then the whole chain from diff to PR comment is working end to end.


03.17 Cost Control per Repository

AI cost balloons most easily when the review runs on irrelevant files or a giant PR. The configuration below limits the review to Go files only and skips PRs that are too large for efficiency.

yaml
 1# Limit AI review to relevant files only
 2on:
 3  pull_request:
 4    paths:
 5      # Only review Go files, not all files
 6      - '**.go'
 7      # Excludes:
 8      # - '**.md'     → no need to review documentation
 9      # - '**.yaml'   → no need to review config
10      # - '**.json'   → no need to review JSON
11
12# Limit based on PR size
13- name: Check PR size
14  run: |
15    CHANGED_FILES=$(git diff origin/${{ github.base_ref }}...HEAD --name-only | wc -l)
16
17    if [ "$CHANGED_FILES" -gt 50 ]; then
18      echo "::notice::PR too big ($CHANGED_FILES files). AI review skipped for efficiency."
19      echo "SKIP_AI=true" >> $GITHUB_ENV
20    fi
21
22- name: AI Review
23  if: env.SKIP_AI != 'true'
24  # ... review logic

These two limiters — path filter and file-count threshold — ensure AI tokens are only spent on Go changes that genuinely need review, not on documentation or config changes.


03.18 Debug Mode for Development

When the AI review behaves oddly, you need a way to see what’s actually being sent to the model. The debug configuration below prints a snippet of CLAUDE.md and the diff size, and can be enabled via a repository variable without changing code.

yaml
 1# Debug mode that can be enabled via a repository variable
 2
 3- name: AI Review (with debug)
 4  env:
 5    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
 6    DEBUG_AI: ${{ vars.DEBUG_AI_REVIEW }}  # repository variable
 7  run: |
 8    if [ "${DEBUG_AI}" == "true" ]; then
 9      set -x  # print every command
10      echo "=== CLAUDE.md content (first 50 lines) ==="
11      head -50 CLAUDE.md
12      echo "=== Diff size ==="
13      wc -l /tmp/pr-diff.txt
14    fi
15
16    # Run review
17    ./scripts/claude-review.sh /tmp/pr-diff.txt general
18
19# Enable debug via GitHub:
20# Repository → Settings → Variables → New variable
21# Name: DEBUG_AI_REVIEW, Value: true

Because debug is controlled by a repository variable, you can turn it on while investigating a problem and turn it back off afterward — without needing to commit or revert anything.


03.19 Using the Right Model per Use Case

Choosing the right model is the most important cost-vs-quality lever in CI. The guide below compares haiku and sonnet — when to use each, along with a simple rule of thumb.

text
 1Model selection guide for CI/CD:
 2
 3claude-haiku-4-5-20251001 (default for automation):
 4  Cost: $0.80/M input + $4.00/M output
 5  Speed: < 3 seconds
 6  Quality: good for mechanical checks
 7
 8  Use for:
 9  ✅ Pre-commit quick review
10  ✅ PR description generation
11  ✅ Test failure analysis
12  ✅ Release notes generation
13  ✅ Security flags (no deep reasoning needed)
14
15claude-sonnet-4-6 (for complex analysis):
16  Cost: $3.00/M input + $15.00/M output
17  Speed: 5-10 seconds
18  Quality: better for nuanced analysis
19
20  Use for:
21  ✅ Complex architecture review
22  ✅ Spec compliance that needs reasoning
23  ✅ Security review that needs context understanding
24
25  NOTE: Use sparingly in CI —
26  cost is 3-4x more expensive than haiku
27
28Rule of thumb:
29  Automation that runs on every PR → haiku
30  Analysis that runs once per sprint → sonnet

The rule of thumb is easy to remember: use haiku for anything that runs on every PR, and reserve sonnet only for high-value analysis that runs rarely.


03.20 Summary

We’ve set up three approaches to using Claude in GitHub Actions:

Approach A (Copilot): Easiest, best for automated PR review. Requires a Copilot subscription.

Approach B (Claude API via HTTP): Flexible, cheap, ideal for automation: PR description, failure analysis, release notes.

Approach C (Claude Code CLI): Full experience, best for complex multi-file analysis.

Key security rules:

  • API key always via ${{ secrets.ANTHROPIC_API_KEY }}
  • Diff redacted of secrets before sending
  • Timeout on all AI steps
  • Minimal permissions per job

In the next article, we use this setup to implement the first quality gate: spec validation in CI.

Related Articles

💬 Comments