Skip to content

Never Commit Secrets

Introduced in: Module 00 · Environment Setup

This is the most common — and most permanent — security mistake in software development. A committed secret is effectively public, even after you delete it. This page explains why, and walks through the full defence stack.


Why Git History Is Forever

When you commit a secret and then delete it in a follow-up commit, most people assume the secret is gone. It isn’t.

Git stores every version of every file as a snapshot. Both commits exist in the history:

a1b2c3d Add API key to config ← secret is here
e4f5a6b Remove API key from config ← secret is still in a1b2c3d

Anyone with access to your repository can run:

Terminal window
git show a1b2c3d:config.py

And see the secret in full. This is true even if:

  • You pushed a deletion commit immediately after
  • You squashed the commits before merging
  • The branch was deleted
  • The repository was later made private (if it was ever public, crawlers may have already captured it)

The only real remediation is to rotate the credential — assume it’s compromised and generate a new one.


What Counts as a Secret

Not just passwords. Any value that grants access or authenticates identity:

Secret typeExamples
API keysOpenAI key, Anthropic key, Stripe key
Cloud credentialsAWS access key + secret, GCP service account JSON
Database connection stringspostgresql://user:password@host/db
Private keysSSH private keys, TLS certificates, JWT signing keys
OAuth tokensPersonal access tokens, app installation tokens
Webhook secretsUsed to verify payload authenticity
Internal service credentialsAgent-to-agent authentication tokens

For an A2A system specifically, agent authentication tokens are high-value targets. If an attacker obtains the token an orchestrator uses to call a specialist agent, they can impersonate the orchestrator and make arbitrary requests to the agent — potentially exfiltrating data or manipulating its outputs.


The Defence Stack

Defence is layered. Each layer catches what the previous one missed.

Layer 1: .gitignore

The first line of defence. Before a file containing secrets can be committed, it needs to not be tracked by git.

The A2A project’s .gitignore excludes:

# Environment variables — never commit these
.env
.env.local
.env.*.local
*.env
# Python secrets
*.pem
*.key
secrets/
# Cloud credential files
.aws/credentials
gcloud/
service-account*.json

How to verify a file is ignored:

Terminal window
git check-ignore -v .env

If the file is properly ignored, git outputs the rule that matched. If it outputs nothing, the file is not ignored.

Layer 2: .env Pattern

Never hardcode secrets in source files. Use environment variables loaded from a .env file at runtime:

# Install: pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv() # Loads .env into environment
api_key = os.environ["ANTHROPIC_API_KEY"]
# Raises KeyError if not set — fails loudly rather than silently

Commit a .env.example with placeholder values instead:

Terminal window
# .env.example — safe to commit, shows what variables are needed
ANTHROPIC_API_KEY=your-key-here
ORCHESTRATOR_SECRET=your-secret-here
DATABASE_URL=postgresql://user:password@localhost/db

Layer 3: GitHub Secret Scanning

GitHub scans every push for patterns matching known secret formats — API key prefixes, private key headers, connection string patterns. If a match is found, GitHub:

  1. Notifies repository administrators
  2. (For supported providers) automatically notifies the provider to revoke the key
  3. Blocks the push if Push Protection is enabled

Enabling Push Protection (the most important step):

  1. Go to your repository → Settings → Code security
  2. Find Secret scanning → enable it
  3. Find Push protection → enable it
  4. Push protection will now block commits containing detected secrets before they reach the remote

Push protection is the only layer that prevents the secret from ever entering the git history. All other layers are detection and response, not prevention.

For local development, a pre-commit hook runs before every commit and can scan staged files for secrets:

Terminal window
# Install detect-secrets
pip install detect-secrets
# Create a baseline of known false positives
detect-secrets scan > .secrets.baseline
# Add a pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
detect-secrets-hook --baseline .secrets.baseline $(git diff --cached --name-only)
EOF
chmod +x .git/hooks/pre-commit

This catches secrets before they’re committed, at the developer’s machine, before any of the other layers even apply.


What to Do If You Accidentally Commit a Secret

  1. Rotate the credential immediately. Don’t wait. Assume it has already been seen. Generate a new key in whatever service issued it and revoke the old one.

  2. Remove it from the current codebase. Delete the file or value, add it to .gitignore, and commit the removal.

  3. Assess whether history cleanup is needed. If the repository is private and you’re confident no external access occurred, document the incident and move on. If the repository is or was ever public, consider using git filter-repo to scrub the history — but note this rewrites all commit SHAs and is disruptive for collaborators.

  4. Audit access logs. Check the service’s access logs for any requests made with the exposed credential. If you see suspicious activity, escalate to an incident response process.


Applying This to the A2A Project

The A2A starter project is pre-configured with the right patterns:

  • .env.example documents all required environment variables
  • .gitignore excludes .env and credential files
  • Each agent reads credentials from environment variables, never from source files
  • The Codespace environment injects secrets via GitHub Codespaces secrets (not .env)

When you add a new agent that needs API access, follow the same pattern: add the variable name to .env.example, load it with os.environ["VAR_NAME"] or process.env.VAR_NAME, and never put the value in source code.