Secret Detection
Rules (for AI agents)
ALWAYS
- On finding a committed credential, rotate or revoke it at the provider first.
Deleting the line does not remediate it, and neither does
git rm, an amended commit, or a history rewrite: the value reached a remote, a clone, a fork, a CI log and a backup, and it is compromised from the moment it was pushed. Rotate, then clean the history, then add the guard that stops it recurring — in that order, because only the first one is time-critical. - Run a scanner; do not scan by eye.
scan_secretsandcheck_secret_patterncarry the full pattern set with per-pattern entropy floors and hotword proximity, and a person reading a diff matches none of that reliably. A working-tree scan and a history scan answer different questions — the first says what you are about to commit, the second says what is already public. - Read the credential from the environment or a secret manager at the point of use —
os.environ,process.env,os.Getenv, a Vault / AWS Secrets Manager / GCP Secret Manager / 1Password client. Nothing about the value should be reconstructible from the repository. - Ship a
.env.examplethat is committed and contains every key with an empty or obviously-fake value, alongside a.envthat is ignored and never committed. The example file is documentation, so it must never be produced by copying a working.envand editing it — that is how a real value survives into the template. - Ignore the file classes that carry key material, in one list:
.env,.env.*(but not.env.example),*.pem,*.key,*.p12,*.pfx,*.ppk,id_rsa*,id_dsa*,id_ecdsa*,id_ed25519*,*credentials*,*secret*. A.gitignoreentry is a guard against habit, not a control — it does nothing for a file already tracked, andgit add -foverrides it. - Treat every client-reachable config value as public: a shipped bundle, a
/configendpoint, Firebase Remote Config, a feature-flag payload, a mobile app's build-time constants. A client may hold public identifiers and nothing else. - Prefer credentials that expire and are scoped. A short-lived, narrowly-scoped token bounds what a leak costs, and it is the only control that still helps after every other one has failed — which, given the rule above about rotation, is the case worth designing for.
NEVER
- Hardcode an API key, token, password, connection string, or private key in source,
and do not hide one in a default parameter, a test constant, a Dockerfile
ARG, or a committed CI variable. - Put a real secret in a test fixture. Use the documented placeholders —
AKIAIOSFODNN7EXAMPLE,wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY,xoxb-EXAMPLE-EXAMPLE— which every scanner already excludes by name. - Distribute a bearer secret to clients at runtime through a config or feature-flag service. Anyone holding the app's public configuration can fetch it, so it is exactly as exposed as a hardcoded one. Proxy the privileged call through an authenticated backend instead.
- Write a credential into a place designed to be read later: a log line, an error
payload, a commit message, a PR or issue body, a CI job name, a screenshot in a
ticket.
logging-securityowns the log sink; the others have no redactor at all. - Treat a pre-commit hook as the control.
--no-verifyskips it,git clonedoes not carry it, and it never runs on the server or on a force-push. It is a convenience that catches accidents; the blocking CI check is the control, andcicd-securityowns that gate.
KNOWN FALSE POSITIVES
- The documented AWS example pair,
AKIAIOSFODNN7EXAMPLEandwJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY. - A value whose text is itself a placeholder —
your-key-here,REPLACE_ME,CHANGEME,xxx,dummy— or a credential-shaped string with a documentation marker in the surrounding prose. - High-entropy strings that are not credentials: git commit SHAs in a changelog, CSS
colour literals, base64-encoded test fixtures, a UUID used as a correlation id, an
integrity
sha384-hash in a<script>tag. - Publishable keys that are designed to be in a client: a Stripe
pk_live_, a Firebase webapiKeyin afirebaseConfigblock, a public Sentry DSN. These are identifiers, not credentials — the question is whether the backend authorizes anything on them. - A test-mode key is not in this list.
sk_test_authenticates against a real test API, reads real test data, and is a credential whose name merely contains the word "test".
Context (for humans)
The rule ordering is the whole skill. Everything else here is standard advice; the one thing worth being emphatic about is that removal is not remediation, because it is the step people reach for first and the one that does nothing. A secret that reached a remote is public, and every minute spent rewriting history before rotating is a minute the credential still works.
The second thing is the split between detection and prevention. Prevention is deterministic and happens while the code is written: the value is read from the environment, so there is nothing to detect. Detection is probabilistic — regex, entropy, proximity — and it has both false positives and false negatives, which means a clean scan is evidence and not proof. Skills that treat a green scanner as the goal end up tuning the scanner.
Which literal strings count as credentials is data, not prose: it lives in
checklists/secret_detection.yaml with per-pattern entropy floors, hotwords and
exclusions, and it is what scan_secrets reads. That is why the rules above tell the
agent to run the scanner rather than reproducing a prefix list here — an inline list
is a copy that goes stale the first time the file gains a pattern.
References
references/verifying-findings.md— confirm or refute a finding, then rotate, clean and lock itchecklists/secret_detection.yaml— the pattern set the scanner reads: regexes, prefixes, hotwords, entropy floors, and theexclusions:blocktests/corpus.json— fixtures for validation- OWASP Secrets Management Cheat Sheet
- CWE-798 — Use of Hard-coded Credentials.
- CWE-259 — Use of Hard-coded Password.