Skip to content

Secret scanning

Carbon ships a small, deterministic secret scanner. The same pattern list that scrubs the runtime access log (SECRET_VALUE_PATTERNS in @carbon/secret-scan) also runs against your working tree, at three distinct points, so a credential has to slip past all three to reach a public branch:

  1. Locally, on demandcarbon audit-secrets walks the tree and fails with exit code 1 on any finding.
  2. On every commit — the pre-commit hook installed at .githooks/ runs the scanner over the staged files. pnpm install’s prepare script points core.hooksPath at that directory automatically.
  3. In CI — the test job runs carbon audit-secrets after the unit suite. A finding fails the job, which blocks deploy.
Prefix or shapeKind label
ck_live_…carbon-key
sk_live_… / sk_test_…stripe-secret
sk-… (OpenAI / Anthropic-style)openai-anthropic
xoxb- / xoxp- / xoxa-…slack-token
ghp_ / gho_ / ghu_ / ghs_ / ghr_…github-token
AKIA… / ASIA… (AWS access key id)aws-access-key
-----BEGIN … PRIVATE KEY-----private-key-pem
postgres://user:pw@host and friendsdb-url-with-password

Patterns are deliberately anchored on unambiguous prefixes. A naked base64/hex blob will not match — it produces too many false positives against opaque request-body ids to be useful in a hard-fail linter.

Terminal window
# Scan the whole tree.
carbon audit-secrets
# Scan a subset.
carbon audit-secrets apps/api src/config
# Scripting: one JSON object per finding, plus a summary line.
carbon audit-secrets --json

Exit code:

  • 0 — no findings.
  • 1 — one or more findings. The offending kind, file, line, and a shape-preserving preview (never the full value) are printed.

Edit packages/secret-scan/src/patterns.ts. Both the CLI and the API runtime pick up the addition on the next build. Tests live alongside in scan.test.ts; fixtures are assembled at runtime so the test file itself never trips the scanner or GitHub push-protection.

Why we don’t also match secret access keys

Section titled “Why we don’t also match secret access keys”

AWS secret access keys are 40 characters of base64. Every S3 object key in a request body, every content hash, every opaque per-tenant id in a recording collides with that shape. A hard-fail scanner that cries wolf on those quickly gets disabled — a policy failure much worse than the one it was preventing. The access-key id (AKIA… / ASIA…) is diagnostic enough on its own: if it leaks, so did the paired secret.