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.
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.
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 runnerIn 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.
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/100The 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.
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+ featuresWith 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.
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-15Once 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.
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.
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.
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: ReadyWith 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.
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 0Note 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.
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.
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.
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 1The 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.
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.
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.
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.
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.
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 timeoutThe 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.
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 fiSeparating 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.
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.
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 hashThe 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.
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-branchIf 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.
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 logicThese 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.
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: trueBecause 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.
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 → sonnetThe 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.