Baseline
Applies when: Every code generation, file write, file edit, and diff review, and any shell command with inline credentials, environment-variable assignments, or curl -H headers.
Always:
- Scan every file write, edit, and diff for secret patterns before it lands, including shell commands with inline credentials or env assignments.
- Stop and do not write the file when a detection pattern matches; name the provider to the user and propose an environment-variable or secret-manager remediation.
- Treat a match for Stripe (
sk_live_/sk_test_/rk_live_; publishablepk_keys are not secrets), AWS (AKIA/ASIA/aws_secret_access_key), GitHub (ghp_/gho_/ghs_), GitLab (glpat-), Anthropic (sk-ant-), OpenAI (sk-), Slack (xox[abprs]-), Google (AIza/ya29.), a private-key block (-----BEGIN ... PRIVATE KEY-----), or a credentialed DB URI as a stop condition. - Use environment variables or a secret manager instead of a literal value.
- Generate
.env.examplewith placeholder values and confirm.envis in.gitignore. - Use clearly fake placeholders (
your-api-key-here,REPLACE_ME) in example code, never realistic-looking strings.
Never:
- Write a literal secret into source code, config files, comments, test fixtures, or documentation.
- Echo a secret in a shell command the agent intends to run (e.g.
curl -H "Authorization: Bearer sk-ant-..."). - Paste a secret into a commit message, PR description, or issue body.
- Write
.envfiles containing real secrets. - Include a secret in a log statement, even at DEBUG level.
Open the full component before acting: it has the examples, the remediation steps, and the detection patterns.
When to apply
Apply on every code generation, file write, file edit, and diff review.
Apply also on shell commands the agent is about to execute that include
inline credentials, environment variable assignments, or curl -H headers.
This skill is one of the always-on critical skills. It is cheap to evaluate (regex over the diff or the proposed write), and the cost of a false negative is a leaked credential that ends up in git history forever.
Why
A hardcoded secret in a public repository has, on average, been scraped within minutes of the push. Rotation is required even if the commit is deleted — git history is forever, and bots monitor force-pushes too. Detection at the agent layer, before the file write, is the cheapest place to catch this.
Rules
- [critical] Never write a literal secret into source code, config files, comments, test fixtures, or documentation.
- [critical] Never echo a secret in a shell command the agent intends to
run (e.g.
curl -H "Authorization: Bearer sk-ant-..."). Use environment variables. - [critical] Never paste a secret into a commit message, PR description, or issue body.
- [high] Never write
.envfiles containing real secrets. Generate.env.examplewith placeholder values instead, and ensure.envis in.gitignore. - [high] Never include a secret in a log statement, even at DEBUG level.
- [medium] Prefer secret managers (AWS Secrets Manager, Azure Key Vault,
Google Secret Manager, HashiCorp Vault, Doppler, 1Password Secrets
Automation) over
.envfor production deployments. - [medium] When generating example code, use clearly fake placeholders
(
your-api-key-here,REPLACE_ME) rather than realistic-looking strings.
[!agent] When you detect a match for any pattern in the "Detection patterns" table below, stop. Do not write the file. Surface the match to the user, name the provider, and propose the environment-variable or secret-manager remediation. Do not proceed without explicit confirmation that the value is a known-fake test fixture.
Detection patterns
| Pattern (regex, case-sensitive) | Provider / Type | Example |
|---|---|---|
\bsk_live_[A-Za-z0-9]{20,}\b |
Stripe live secret key | sk_live_51H... |
\bsk_test_[A-Za-z0-9]{20,}\b |
Stripe test secret key | sk_test_4eC39... |
\brk_live_[A-Za-z0-9]{20,}\b |
Stripe restricted key | rk_live_... |
\bAKIA[0-9A-Z]{16}\b |
AWS Access Key ID | AKIAIOSFODNN7EXAMPLE |
\bASIA[0-9A-Z]{16}\b |
AWS temp Access Key ID | ASIAIOSFODNN7EXAMPLE |
(?i)aws_secret_access_key\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']? |
AWS secret access key (= or :; AWS_SECRET_ACCESS_KEY: ... in YAML/compose/CI is the common shape) |
40-char base64 |
\bghp_[A-Za-z0-9]{36}\b |
GitHub personal token | ghp_xxxxxxxx... |
\bgho_[A-Za-z0-9]{36}\b |
GitHub OAuth token | gho_xxxxxxxx... |
\bghu_[A-Za-z0-9]{36}\b |
GitHub user-to-server | ghu_xxxxxxxx... |
\bghs_[A-Za-z0-9]{36}\b |
GitHub server-to-server | ghs_xxxxxxxx... |
\bghr_[A-Za-z0-9]{36}\b |
GitHub refresh token | ghr_xxxxxxxx... |
\bglpat-[A-Za-z0-9_\-]{20,}\b |
GitLab personal token | glpat-xxxx... |
\bsk-ant-(?:api|admin)\d+-[A-Za-z0-9_\-]{80,}\b |
Anthropic API key | sk-ant-api03-... |
\bsk-(?:proj-)?[A-Za-z0-9_\-]{40,}\b |
OpenAI API key | sk-proj-... (56+ chars) |
\bxox[abprs]-[A-Za-z0-9-]{10,}\b |
Slack token | xoxb-123-456-abc |
\bAIza[0-9A-Za-z_\-]{35}\b |
Google API key | AIzaSyD... |
\bya29\.[A-Za-z0-9_\-]+\b |
Google OAuth access token | ya29.a0AfH... |
\bsq0[a-z]{3}-[A-Za-z0-9_\-]{20,}\b |
Square token | sq0atp-..., sq0csp-... |
\bSG\.[A-Za-z0-9_\-]{22}\.[A-Za-z0-9_\-]{43}\b |
SendGrid API key | SG.xxx.yyy |
\bkey-[a-f0-9]{32}\b |
Mailgun API key | key-xxxx... |
\bSK[a-f0-9]{32}\b |
Twilio API SID | SKxxxx... |
\bAC[a-f0-9]{32}\b |
Twilio Account SID | ACxxxx... |
\bDOCKER_AUTH_CONFIG\s*=\s* |
Docker registry creds | env-style |
npm_[A-Za-z0-9]{36} |
npm access token | npm_xxxx... |
\beyJ[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\b |
JWT with payload | three-part eyJ... |
| `-----BEGIN (?:RSA | EC | DSA |
-----BEGIN CERTIFICATE----- |
TLS / x509 cert | Cert in source |
mongodb(?:\+srv)?://[^:\s]+:[^@\s]+@ |
MongoDB URI w/ password | mongodb+srv://u:p@host |
postgres(?:ql)?://[^:\s]+:[^@\s]+@ |
Postgres URI w/ password | postgres://u:p@host |
mysql://[^:\s]+:[^@\s]+@ |
MySQL URI w/ password | mysql://u:p@host |
redis://[^:\s]*:[^@\s]+@ |
Redis URI w/ password | redis://:pw@host |
amqp(?:s)?://[^:\s]+:[^@\s]+@ |
RabbitMQ URI w/ password | amqps://u:p@host |
(?i)\b(?:api[_-]?key|apikey)\s*[:=]\s*["'][A-Za-z0-9_\-]{16,}["'] |
Generic API key literal | api_key="..." |
(?i)\b(?:password|passwd|pwd)\s*[:=]\s*["'][^"']{6,}["'] |
Hardcoded password | password="..." |
(?i)\b(?:secret|client_secret)\s*[:=]\s*["'][A-Za-z0-9_\-]{16,}["'] |
OAuth client secret | client_secret="..." |
(?i)\b(?:token|auth_token|access_token)\s*[:=]\s*["'][A-Za-z0-9_\-\.]{16,}["'] |
Auth tokens | token="..." |
(?i)\bbearer\s+[A-Za-z0-9_\-\.=]{16,}\b |
Bearer in headers | Authorization: Bearer ... |
(?i)\baz(?:ure)?[_-]?(?:client[_-]?secret|tenant[_-]?id)\s*[:=]\s* |
Azure credentials | inline assignments |
(?i)\bDATABASE_URL\s*=\s*[^$\s][^"'\s]+ |
DB URL set to literal (not ${...}) |
DATABASE_URL=postgres://... |
Not secrets, and not stop conditions: Stripe publishable keys (pk_live_, pk_test_) are designed for client-side code, as Stripe's own documentation says; the shipped Bash hook does not block them either. Flag one only when it sits next to a secret key or in a file that also holds sk_/rk_ values.
Patterns are intentionally conservative on length/charset to minimize false positives in test fixtures. When in doubt, ask the user.
Negative examples
# ❌ Stripe live key in source — leaks the moment this is pushed
import stripe
stripe.api_key = "sk_live_<REDACTED_FAKE_EXAMPLE_DO_NOT_SCAN>"
// ❌ OpenAI key in a Next.js client component — also ships to the browser
const client = new OpenAI({
apiKey: "sk-proj-AbCdEf123456789..."
});
# ❌ AWS keys in a docker-compose.yml committed to the repo
services:
api:
environment:
AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# ❌ Connection string with embedded password
DATABASE_URL = "postgres://app:hunter2@db.internal:5432/prod"
# ❌ Bearer token inlined into a curl invocation in a script
curl -H "Authorization: Bearer ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
https://api.github.com/user
# ❌ Secret baked into an image layer — visible to anyone with `docker history`
ENV STRIPE_API_KEY=sk_live_<REDACTED_FAKE_EXAMPLE_DO_NOT_SCAN>
# ❌ Private key committed alongside test fixtures
PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA1...
-----END RSA PRIVATE KEY-----"""
Remediation
Pattern 1: read from environment
# ✅ Python
import os
import stripe
stripe.api_key = os.environ["STRIPE_API_KEY"]
// ✅ Node / TypeScript
const apiKey = process.env.STRIPE_API_KEY;
if (!apiKey) {
throw new Error("STRIPE_API_KEY is not set");
}
// ✅ Go
apiKey := os.Getenv("STRIPE_API_KEY")
if apiKey == "" {
log.Fatal("STRIPE_API_KEY is not set")
}
Pattern 2: .env for local dev, never committed
# .env (must be in .gitignore — always check)
STRIPE_API_KEY=sk_live_...
DATABASE_URL=postgres://app:...@localhost:5432/dev
# .gitignore
.env
.env.*
!.env.example
# .env.example (committed; placeholders only)
STRIPE_API_KEY=sk_live_REPLACE_ME
DATABASE_URL=postgres://app:REPLACE_ME@localhost:5432/dev
Pattern 3: secret manager in production
# ✅ AWS Secrets Manager
import boto3, json
sm = boto3.client("secretsmanager")
secret = json.loads(sm.get_secret_value(SecretId="prod/stripe")["SecretString"])
stripe.api_key = secret["api_key"]
// ✅ Azure Key Vault
import { DefaultAzureCredential } from "@azure/identity";
import { SecretClient } from "@azure/keyvault-secrets";
const credential = new DefaultAzureCredential();
const client = new SecretClient(process.env.KEY_VAULT_URL!, credential);
const secret = await client.getSecret("stripe-api-key");
stripe.apiKey = secret.value!;
Pattern 4: Docker build secrets (BuildKit)
# syntax=docker/dockerfile:1.4
FROM node:20@sha256:abc123...
# ✅ Mounted at build time, never persisted in any layer
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm install
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .
Pattern 5: CI/CD — never echo, always mask
# ✅ GitHub Actions
- name: Deploy
env:
STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
run: ./deploy.sh # script reads from env, does not echo
# ❌ Never:
# run: echo "Deploying with key ${{ secrets.STRIPE_API_KEY }}"
If a secret has already been committed
This is an incident. Run, in order:
- Rotate the credential immediately at the provider. Assume it is already compromised.
- Audit usage logs at the provider for unauthorized calls.
- Purge git history with
git filter-repoor BFG Repo-Cleaner. A new commit deleting the file does not remove the value from history. - Force-push the rewritten history (coordinate with the team — this rewrites everyone's clones).
- Invalidate cached copies: GitHub forks, mirrors, CI caches, container registries that pulled the affected commit.
The full incident-response playbook is in the incident-response skill.
References
- OWASP A02:2021 — Cryptographic Failures: https://owasp.org/www-project-top-ten/A02_2021-Cryptographic_Failures/
- OWASP A07:2021 — Identification and Authentication Failures: https://owasp.org/www-project-top-ten/A07_2021-Identification_and_Authentication_Failures/
- OWASP Secrets Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
- GitHub Secret Scanning patterns: https://docs.github.com/en/code-security/secret-scanning/secret-scanning-patterns
- AWS Secrets Manager: https://docs.aws.amazon.com/secretsmanager/
- Azure Key Vault: https://learn.microsoft.com/en-us/azure/key-vault/
- Google Secret Manager: https://cloud.google.com/secret-manager/docs
- HashiCorp Vault: https://developer.hashicorp.com/vault/docs