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:

  1. In your GitHub repository: Settings → Secrets and variables → Actions
  2. Click New repository secret
  3. Name: ANTHROPIC_API_KEY (uppercase, exactly as you reference it in YAML)
  4. Value: your API key (sk-ant-...)
  5. 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:

  1. In the organization: Settings → Secrets and variables → Actions
  2. New organization secret
  3. Name: ANTHROPIC_API_KEY
  4. Value: the key
  5. 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 main can read secrets from the "production" environment, for example.

Steps:

  1. Settings → Environments → New environment
  2. Name: production (or staging, etc.)
  3. Configure:
    • Required reviewers: list of people who must approve deploys
    • Deployment branches: restrict to main or specific branches
  4. Add secret within the environment
  5. 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.

TypeUse caseVisible in logs?Editable later?
VariableModel name, base API URL, flags✅ Yes✅ Yes
SecretAPI 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:

ThingGoes in a secret?Where it goes
Anthropic API key✅ YesSecret
Database password✅ YesSecret
OAuth client secret✅ YesSecret
OAuth client ID⚠️ VariableVariable (it's not a secret)
Model name (e.g. claude-haiku-4-5)❌ NoVariable or hardcoded
A service's public URL❌ NoVariable or hardcoded
Personal information (PII)❌ NoNever pass it to CI
Real customer data❌ NoNever 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

  1. GitHub Encrypted Secrets — Official secrets docs
  2. GitHub Environments — Environment setup
  3. gitleaks — Secret detector for git
  4. detect-secrets — Yelp's alternative
  5. GitHub Secret Scanning — GitHub's native feature
  6. Anthropic API Key Best Practices — Anthropic's official guide
  7. BFG Repo-Cleaner — For cleaning the history when there's an incident