# Secret Detection

> Keeping credentials out of source, and what to do once one is in: rotate before removing, because deleting the line does not un-leak it. Covers where a secret may live, safe .env templates, why a client-reachable config value is public, and running a scanner rather than eyeballing for patterns. Use when writing code that reads a credential, creating config or .env templates, before committing, or after discovering a secret in a repository.

- Skill: `shieldnet-360/secret-detection` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add shieldnet-360/secret-detection`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shieldnet-360/secret-detection/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: ShieldNet-360 (https://skillmd.com/u/shieldnet-360)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/shieldnet-360/secret-detection

---


# 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_secrets` and `check_secret_pattern`
  carry 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.example`** that is committed and contains every key with an empty or
  obviously-fake value, alongside a `.env` that is ignored and never committed. The
  example file is documentation, so it must never be produced by copying a working
  `.env` and 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 `.gitignore` entry is a guard against habit, not a control — it does
  nothing for a file already tracked, and `git add -f` overrides it.
- Treat every **client-reachable config value as public**: a shipped bundle, a
  `/config` endpoint, 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-security` owns the log sink; the others have no redactor at all.
- Treat a **pre-commit hook** as the control. `--no-verify` skips it, `git clone` does
  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, and
  `cicd-security` owns that gate.

### KNOWN FALSE POSITIVES
- The documented AWS example pair, `AKIAIOSFODNN7EXAMPLE` and
  `wJalrXUtnFEMI/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 web `apiKey` in a `firebaseConfig` block, 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 it
- `checklists/secret_detection.yaml` — the pattern set the scanner reads: regexes,
  prefixes, hotwords, entropy floors, and the `exclusions:` block
- `tests/corpus.json` — fixtures for validation
- [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
- [CWE-798](https://cwe.mitre.org/data/definitions/798.html) — Use of Hard-coded Credentials.
- [CWE-259](https://cwe.mitre.org/data/definitions/259.html) — Use of Hard-coded Password.

