Module 1: Claude Code in GitHub Actions
Secrets Management with GitHub Secrets
Secrets Management with GitHub Secrets
Overview
This capsule is the most important for production in the whole guide. Nothing you build in CI/CD is worth it if your API key ends up exposed. Capsule 02 gave you a working workflow using ${{ secrets.ANTHROPIC_API_KEY }}; here you learn what's behind that reference, where and how it's configured, the three scope levels (repo, organization, environment), and what to do when something goes wrong.
By the end, you'll be able to configure GitHub Secrets at the right level for the use case, you'll know what should never go in a secret, you'll have a clear protocol for what to do if a key leaks, and you'll understand why hardcoding "just to test" is the #1 cause of security incidents in CI/CD.
The Mental Model: Three Scope Levels
GitHub Secrets are configured at one of three scopes. Each has a distinct use case.
┌──────────────────────────────────────────────────┐
│ ORGANIZATION (highest level) │
│ Settings → Secrets → Actions │
│ → Available in ALL repos of the org │
│ → Useful for: shared keys (Anthropic prod, │
│ Sentry DSN, etc.) │
│ → Optional restriction: only to specific repos │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ REPOSITORY │ │
│ │ Settings → Secrets → Actions │ │
│ │ → Available in ALL workflows │ │
│ │ of THIS repo │ │
│ │ → Useful for: repo-specific keys │ │
│ │ (a project's DB credentials, etc.) │ │
│ │ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ ENVIRONMENT │ │ │
│ │ │ Settings → Environments → New │ │ │
│ │ │ → Available only in jobs that │ │ │
│ │ │ reference that environment │ │ │
│ │ │ → Useful for: distinguishing │ │ │
│ │ │ staging vs production, gates │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
The golden rule: use the most restrictive scope that covers your case. If only one repo needs it, put it at the repo level. If only production deploys need it, put it in the "production" environment.
Configuring a Secret at the Repository Level
It's the most common case and the one you'll use in the module's workflow.
Steps:
- In your GitHub repository: Settings → Secrets and variables → Actions
- Click New repository secret
- Name:
ANTHROPIC_API_KEY(uppercase, exactly as you reference it in YAML) - Value: your API key (
sk-ant-...) - Click Add secret
How it's referenced in YAML:
- name: Run Claude Code analysis
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: python .github/scripts/analyze_pr.py
The syntax ${{ secrets.NAME }} is the exact form. GitHub replaces that placeholder at runtime with the real value, masking it in the logs (you'll see *** instead of the value).
Verify that masking works
Temporarily add this step to confirm:
- name: Verify masking (TEMPORARY)
run: echo "Key available (masked in logs):" "$ANTHROPIC_API_KEY"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
In the logs you'll see: Key available (masked in logs): ***. If you see the real key instead of ***, something is misconfigured — remove the step and check the secret.
After verifying, delete that step. Don't leave secret prints in production, even masked ones — a future change to the runner could expose them.
When to Use Organization Secrets
If your organization has several repositories that use Claude Code (typical in companies), repeating the secret in each repo is duplication. Worse: if you rotate the key, you have to change it in N places.
Solution: an organization-level secret.
Steps:
- In the organization: Settings → Secrets and variables → Actions
- New organization secret
- Name:
ANTHROPIC_API_KEY - Value: the key
- Repository access: choose between:
- Public repositories — available in all public repos
- Private repositories — available in all private ones (typically what you want)
- Selected repositories — only the ones you explicitly choose (more secure)
Recommendation: "Selected repositories" whenever feasible. If a key should reach only 5 repos, don't expose it to 50.
Override in a specific repo
If you configured the secret at the org level but a repo needs a different key (e.g. a testing account), you can define the secret with the same name in that repo. The repo secret wins over the organization one in that specific workflow.
When to Use Environment Secrets
Environments are the finest scope. Useful when:
- You distinguish between staging and production. The workflow can use the staging API key for test deploys and the production one for real deploys — with no risk of a crossover.
- You want to require human approval before a deploy. Environments can have "required reviewers" configured.
- You want to restrict secrets to specific branches. Only
maincan read secrets from the "production" environment, for example.
Steps:
- Settings → Environments → New environment
- Name:
production(orstaging, etc.) - Configure:
- Required reviewers: list of people who must approve deploys
- Deployment branches: restrict to
mainor specific branches
- Add secret within the environment
- Name:
ANTHROPIC_API_KEY, Value: the key
How it's referenced in YAML:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # ← key to accessing the env's secrets
steps:
- name: Deploy
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: ./scripts/deploy.sh
Without the environment: production line, the job has no access to that environment's secrets.
Variables vs Secrets
GitHub Actions distinguishes between variables (non-sensitive) and secrets (sensitive). Both are configured in the same UI but on different tabs.
| Type | Use case | Visible in logs? | Editable later? |
|---|---|---|---|
| Variable | Model name, base API URL, flags | ✅ Yes | ✅ Yes |
| Secret | API keys, passwords, tokens | ❌ No (***) | Only the value (not the name) |
Simple rule:
- If someone sees it, is it a problem? → Secret
- If someone sees it, nothing happens? → Variable
Example:
env:
CLAUDE_MODEL: ${{ vars.CLAUDE_MODEL }} # variable
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # secret
vars.CLAUDE_MODEL (not secrets) is the syntax for variables.
What Should NEVER Go in a Secret
Secrets are for credentials. There are things that look sensitive but don't go in secrets:
| Thing | Goes in a secret? | Where it goes |
|---|---|---|
| Anthropic API key | ✅ Yes | Secret |
| Database password | ✅ Yes | Secret |
| OAuth client secret | ✅ Yes | Secret |
| OAuth client ID | ⚠️ Variable | Variable (it's not a secret) |
Model name (e.g. claude-haiku-4-5) | ❌ No | Variable or hardcoded |
| A service's public URL | ❌ No | Variable or hardcoded |
| Personal information (PII) | ❌ No | Never pass it to CI |
| Real customer data | ❌ No | Never pass it to CI |
Special case: PII and customer data should not touch CI. If your test needs real data, it's poorly designed — use synthetic fixtures. Passing real data through logs (even masked) is a regulatory risk (GDPR, HIPAA, LGPD).
Incident Protocol: Leaked API Key
You're human. Sooner or later, someone on the team will commit a key by accident. The protocol matters more than avoiding it perfectly.
If you discover a key in a commit (yours or someone else's):
STEP 1 — REVOKE IMMEDIATELY (5 minutes)
→ Anthropic console → API Keys → your key
→ Click "Revoke"
→ The key stops working in any future call
→ THIS COMES FIRST. Don't wait to "clean up git".
STEP 2 — GENERATE A NEW KEY (5 minutes)
→ Anthropic console → "Create Key"
→ Configure the minimum permissions needed
STEP 3 — UPDATE THE SECRET (5 minutes)
→ GitHub Secrets → ANTHROPIC_API_KEY → "Update"
→ Paste the new key
→ Any workflow that runs afterward uses the new key
STEP 4 — CLEAN THE HISTORY (optional, complex)
→ If the key was in git even for 1 minute, it's
archived forever in the history
→ git filter-repo or BFG Repo-Cleaner can rewrite
the history, but they require coordination with the team
(force push, all devs have to re-clone)
→ For public repos, consider the commit "lost"
(someone could have cloned it between the commit and the
cleanup)
→ For private repos, it's worth the effort if the key
was critical
STEP 5 — POST-MORTEM (1 hour)
→ How did it happen? (e.g. "I had the key in my .env and added
.env by accident")
→ What controls do we add? (pre-commit hooks that
detect secrets, a reinforced .gitignore, CI that
detects secrets on every push)
The important thing: don't delete the commit and pray. Revoke first, clean later. If you've already revoked, the key in the commit is useless even if someone finds it.
Automatic Secret Detection in CI
GitHub has secret scanning enabled by default on public repos: it detects known patterns (sk-ant-..., AKIA..., glpat-...) and notifies the owner. On private repos it's a paid feature (Advanced Security).
Free alternative: pre-commit hooks with gitleaks or detect-secrets.
Setup with gitleaks (recommended)
# Install gitleaks
brew install gitleaks # macOS
# or: docker pull zricethezav/gitleaks
# Create pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
gitleaks protect --staged --verbose
EOF
chmod +x .git/hooks/pre-commit
Before each commit, gitleaks scans the staged files looking for secret patterns. If it detects one, it blocks the commit and shows you what it found.
For CI, add it as a step:
- name: Detect secrets
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Common Pitfalls in Secrets Management
Error 1: "I configure it as a secret but I also print it for debug"
Symptom: Even though it's configured as a secret, the team sees it in logs because some script does print(api_key).
Why it happens: For "quick" debug, someone adds a print. GitHub masks ${{ secrets.X }} automatically, but if the script prints the value at runtime, the masking sometimes doesn't work (the matching is by exact strings).
How to fix it: Never print secrets, not even "for debug". If you need to verify that a secret is available, print only "ANTHROPIC_API_KEY is configured: True/False" (not the value).
Error 2: Reusing the same key in development and production
Symptom: The team uses a single key for everything. If a junior dev leaks it, the incident affects production.
Why it happens: "We have one account, one key, why have two?". But the blast radius of an incident is drastically reduced if they're separated.
How to fix it: Create separate keys in the Anthropic console: one for dev/CI tests, another for production. Configure them in different environments. If dev leaks, prod isn't affected.
Error 3: Configuring the secret at the organization level "for convenience"
Symptom: All the org's repos have access to the production key, even though only 3 need it.
Why it happens: Setting the secret at the org level once is more comfortable. But it exposes the key to all the workflows of all the repos.
How to fix it: Use "Selected repositories" in the organization secret, or put it at the repo level only in the few that need it.
Error 4: Forks and external pull requests
Symptom: An external contributor opens a PR. The workflow runs with their modifications and... has no access to the secret. The job fails.
Why it happens: GitHub deliberately doesn't expose secrets to workflows that run from forks (it would be trivial to steal them). It's the correct behavior.
How to fix it: Design the workflow assuming external PRs don't have access to secrets. For open-source repos, consider running the analysis after the merge (on push to main) or requiring a maintainer to approve running the workflow for external PRs.
Error 5: Rotating the secret but forgetting to update all the workflows
Symptom: You rotated the key, configured the new one in ANTHROPIC_API_KEY. But the deploy.yml workflow referenced ANTHROPIC_KEY (no underscore) — now it fails.
Why it happens: Inconsistency in secret names between workflows. Each one uses a variation.
How to fix it: Audit all the workflows after rotating. Keep a consistent name across the whole repo (ANTHROPIC_API_KEY always, not sometimes ANTHROPIC_KEY).
Diagnosis: Verify Your Setup
Question 1: Is your API key in any file in the repository?
Search: git grep -i "sk-ant" and git log --all --full-history -- "*.py" "*.yml" "*.yaml" | grep -i "sk-ant". If anything shows up, revoke it right now and rotate.
Question 2: Does your workflow work but is the secret configured correctly?
If the script fails with "API key missing" but ${{ secrets.ANTHROPIC_API_KEY }} is in the YAML, check: (1) the name matches exactly between YAML and Settings, (2) the secret is at the correct repo level (if it's a fork, it's not available).
Question 3: Do you have separate keys for dev/test and production?
If not, make the change before continuing. It takes 10 minutes and dramatically reduces blast radius.
Question 4: Do you have a clear protocol for "what to do if a key leaks"?
If not, write it now. The pressure of a real incident is no time to invent the process. Anchor it in .github/SECURITY.md or a runbook.
Question 5: Does your CI detect secrets in commits before accepting them?
If not, set up gitleaks or detect-secrets. It's 30 minutes of setup and prevents the "we committed by accident and don't notice until it's too late" case.
Exercises
Exercise 1: Configure a secret and verify masking (Easy)
Configure ANTHROPIC_API_KEY in your repo. Add a temporary step that prints the value. Verify that *** appears in the logs. Remove the step.
Exercise 2: Configure an environment with an approval gate (Medium)
Create a production environment with required reviewers. Modify the capsule 02 workflow so it uses that environment. Open a PR and merge — the workflow should wait for approval before continuing.
See solution
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
# ... rest of the workflow
In Settings → Environments → New environment "production" → Required reviewers: add your user. When the job reaches this step, GitHub pauses and waits for approval.
Exercise 3: Implement secret detection with gitleaks (Medium)
Configure gitleaks as a pre-commit hook. Try to commit a file with a fake key (sk-ant-test123...) and verify that the commit is blocked.
Summary
- Three scope levels: organization, repository, environment — use the most restrictive applicable one
- Variables vs secrets: everything sensitive in a secret; everything else in a variable
- Never print secrets, not even "for debug"
- Separate keys between dev/test and production reduce blast radius
- Incident protocol: revoke first, clean later
- Automatic detection: gitleaks or secret scanning to avoid the "we committed it without noticing"
- Forks have no access to secrets — design the workflow accounting for it
Next capsule: 04 — Parsing output and generating artifacts. Your workflow already runs securely, but the output is still in logs nobody reads. Capsule 04 teaches you to bring it to the PR itself: comments, annotations, downloadable artifacts.
Additional Resources
- GitHub Encrypted Secrets — Official secrets docs
- GitHub Environments — Environment setup
- gitleaks — Secret detector for git
- detect-secrets — Yelp's alternative
- GitHub Secret Scanning — GitHub's native feature
- Anthropic API Key Best Practices — Anthropic's official guide
- BFG Repo-Cleaner — For cleaning the history when there's an incident