No hardcoded credentials
On this page
Detect committed secrets - API keys, access tokens, OAuth tokens - by matching well-known issuer prefixes. The intent: once a credential is pushed to a remote, rotation is the only remedy. Pre-commit is the cheapest gate that catches the leak before it spreads.
What’s detected (V0.5)#
The frame matches credentials with distinctive issuer prefixes. These are high-confidence patterns with near-zero false-positive rates in real code. Generic high-entropy detection (random 40-char hex strings) is explicitly out of scope for V0.5 - too noisy on UUIDs, hashes, and compiled assets.
BLOCK - real leaks; fail the gate, rotate immediately#
| Pattern | Example prefix |
|---|---|
| AWS access key ID | AKIA… |
| AWS secret access key | AWS_SECRET_ACCESS_KEY=… (matched only in an assignment naming it) |
| GitHub personal access token (classic) | ghp_… |
| GitHub fine-grained PAT | github_pat_… |
| GitHub OAuth token | gho_… |
| GitHub user-to-server token | ghu_… |
| GitHub server-to-server token | ghs_… |
| GitHub refresh token | ghr_… |
| Stripe secret key (live) | sk_live_… |
| Stripe secret key (test) | sk_test_… |
| Stripe restricted key | rk_live_… / rk_test_… |
| Anthropic API key | sk-ant-… |
| OpenAI API key | sk-…, sk-proj-…, sk-svcacct-…, sk-admin-… |
| Hugging Face token | hf_… |
| Groq API key | gsk_… |
| xAI API key | xai-… |
| Slack token | xoxb-… / xoxp-… / xoxa-… / xoxr-… / xoxs-… |
| Google API key | AIza… |
INFO - publishable by design; catalogued, doesn’t block#
| Pattern | Example prefix |
|---|---|
| Stripe publishable key (live) | pk_live_… |
| Stripe publishable key (test) | pk_test_… |
INFO entries are reported in nimblegate check / lint output but
do not fail the gate. The intent is inventory: where in the codebase
does my public key appear? Did a test fixture accidentally use
pk_live_ when it should have used pk_test_? When migrating between
Stripe accounts, where do I have to update?
If a commit contains both a BLOCK-severity leak AND an INFO-severity publishable key, the overall outcome is BLOCK and the reason lists both findings.
Each pattern is matched with an anchored character-class regex of the documented length so partial prefixes alone don’t trigger.
Severity#
Mixed: BLOCK for real credentials, INFO for publishable-by-design
keys. The frame’s frontmatter declares the maximum severity it can
emit (BLOCK); per-pattern severity controls what actually fires for
each match.
Detection scope#
- Triggers:
pre-commit,cli. - Applies to: every staged/scanned file regardless of extension - secrets
hide in
.env,.yaml,.json,.toml,.md, source code, and CI scripts. We rely on the noise-dir exclusion (default + project-configured) to skip vendored code (node_modules/,dist/,build/). - Files larger than 1 MB are skipped (assumed binary / generated).
Failure message#
The reason names the file, line, and pattern - but never echoes the matched bytes. Audit log + terminal would otherwise re-leak the credential.
❌ security/no-hardcoded-credentials (security)
credentials detected (raw bytes redacted):
- src/aws-client.js:14 - AWS access key
- .env.example:3 - GitHub personal access token
fix: remove the credential, ROTATE IT NOW (assume compromised),
store via a secret manager / env var, and add a per-line
`appframes:disable-next-line security/no-hardcoded-credentials`
only if the value is a known-fake test fixture.
Override#
Per-file disable (suppresses every pattern in the file):
# appframes:disable security/no-hardcoded-credentials
Per-line disable (suppresses the line that follows the marker):
// appframes:disable-next-line security/no-hardcoded-credentials
const FAKE_TEST_KEY = "AKIAIOSFODNN7EXAMPLE";
Use per-line for genuine test fixtures. Use per-file ONLY for files that the project has decided are not subject to this scan (generated/vendored configs). When in doubt: don’t disable; rotate.
What’s NOT detected#
- Provider documentation sentinels (
AKIAIOSFODNN7EXAMPLE,AKIAI44QH8DHBEXAMPLE) - AWS’s own published example keys are fake by design and excluded entirely, not downgraded; docs quoting them never fire. A non-sentinel key on the same line still fires.
Documented so a future “why didn’t it catch X?” has an answer:
- Generic high-entropy strings - UUIDs, SHA hashes, base64 blobs in HTML/JS minified output produce too many false positives. Wait for V0.6+ context-aware detection.
- Stripe publishable keys (
pk_live_…,pk_test_…) - these are intentionally public; not a leak. - Private SSH keys / TLS certs - handled by a separate frame
(
security/no-private-keys-in-repo, Tier 1 candidate). - A token split across string concatenation (
"ghp_" + rest) or stored base64-encoded. Both are out of reach of any pattern matcher; the frame matches what is literally present in the file. - AI provider keys without a distinctive prefix - Mistral and Cohere issue bare fixed-length alphanumerics, unmatchable for the same reason a bare AWS secret is. Add a project-specific linter if you use them.
- AWS secret access keys assigned to an unrelated name. The value is
40 chars of
[A-Za-z0-9/+=], the same shape as every git SHA and base64 blob, so it is matched only where the assignment names it (AWS_SECRET_ACCESS_KEY=…,aws_secret_access_key: …). A bare 40-char token is left alone; catching it would fire on every lockfile. - Database connection strings with embedded passwords - pattern is
too varied (
postgres://,mysql://,mongodb+srv://…). Future expansion candidate.
BLOCK rejects the push. Turn frames on per repo on the dashboard's Policy page - see choosing what the gate checks.
Source on GitHub Live demo How it works Questions: contact@nimblegate.com