A CLI tool and Chrome extension that detects exposed API keys, tokens, and other secrets in files, stdin, web pages, and network traffic. Rust core with 64 detection patterns. Fully local β no data leaves your machine or browser.
$ secrets-spotter src/ .env
[Critical] AWS Access Key ID
File: src/aws.js:42
Match: AKIA****EXAMPLE
[Medium] Google API Key
File: .env:8
Match: AIza****Z9aA
Three-tier detection pipeline tuned for speed and precision:
- Known-prefix patterns (50) β fixed prefixes baked into the credential format itself (
AKIA,ghp_,sk-ant-,eyJ...eyJ). Highest confidence, lowest false-positive rate. - Keyword patterns (12) β service or generic variable names paired with high-entropy values (
aws_secret_access_key=...,authorization: Bearer ...); includes 3 keyword-gated legacy formats. - Entropy fallback (2) β broad keyword match (
key,token,secret) with Shannon-entropy validation (β₯3.5 bits/char) to catch novel formats.
A RegexSet + memchr pre-filter on prefix substrings means non-matching input is rejected without running any regex. False-positive filtering rejects placeholders, code identifiers (camelCase / snake_case / kebab-case), URLs, file paths, and low-diversity character sets before reporting.
Pre-built CLI binaries for Linux x86_64, macOS (Intel + Apple Silicon), and Windows x86_64 are published with each tag:
# Replace TARGET with your platform (x86_64-unknown-linux-gnu, aarch64-apple-darwin, ...)
curl -L https://github.com/yipjunkai/secrets-spotter/releases/latest/download/secrets-spotter-v1.2.0-${TARGET}.tar.gz | tar xz # x-release-please-versiongit clone https://github.com/yipjunkai/secrets-spotter
cd secrets-spotter
just build
# CLI binary at target/release/secrets-spotterRequires Rust, just, and wasm-pack (extension only).
just build(builds the WASM intoextension/wasm/)- Open
chrome://extensions/β enable Developer mode β Load unpacked β select theextension/folder
# Scan files and directories
secrets-spotter src/ .env config/
# Scan from stdin
cat credentials.json | secrets-spotter
# JSON output for scripting / CI
secrets-spotter --format json .
# SARIF output for GitHub Code Scanning
secrets-spotter --format sarif . > results.sarif
# Filter by severity
secrets-spotter --severity high .
# Glob filter
secrets-spotter --glob "*.js,*.env,*.yaml" .
# Reveal full unredacted values (use with care)
secrets-spotter --reveal .
# Quiet mode β exit code only
secrets-spotter --quiet .Once the Chrome extension is loaded, browse any website. The icon badge shows the count of Critical/High findings. Click the icon for a grouped breakdown with redacted previews, copy-to-clipboard, and an expandable JWT decoder.
- CLI tool for scanning files, directories, and stdin β CI/CD ready with JSON and SARIF output
- Chrome extension for real-time scanning of DOM content, network traffic (fetch, XHR, WebSocket, Server-Sent Events), cookies,
localStorage/sessionStorage, the page URL, and external<script>bundles - 64 detection patterns β AWS keys, GitHub tokens, Stripe keys, JWTs, private keys, database connection strings, and 58 more (including keyword-gated legacy formats)
- False-positive filtering β Shannon entropy, placeholder detection, code-identifier rejection, URL/path exclusion
- JWT decoder in popup β expandable header / payload JSON view for detected JWTs
- SPA-aware β extension re-scans on
pushState,replaceState,popstate, andhashchangenavigations - Fully local β no data leaves your machine or browser; no telemetry, no cloud calls
- Chrome Web Store listing
- Firefox support (Manifest V3 port)
- User-defined rules via options page
- Per-site disable toggle and severity filtering in popup
- User-defined allowlisting for known-safe values (beyond the built-in example-key filter)
- Dark mode in popup
Existing secret scanners optimize for the server-side audit workflow: scan a git history, scan a CI build, scan an S3 bucket. They're heavy, slow, and operate on already-committed code.
Secrets Spotter optimizes for two complementary workflows the existing tools miss:
- Local pre-flight β a CLI fast enough to run on every save (
secrets-spotter src/in tens of milliseconds), not just on commit. Same Rust regex engine as the extension, no Python / Node startup tax. - Live browser scanning β the Chrome extension surfaces secrets as you visit sites. Find your own keys leaking from someone else's frontend, find production tokens accidentally shipped in client JS, audit a vendor's web app in real time.
Both surfaces are fully local β nothing about your code, network traffic, or browsing leaves the machine.
secrets-spotter/
βββ Cargo.toml # Workspace root
βββ crates/
β βββ core/ # Shared detection library (no WASM deps)
β β βββ benches/ # Criterion scan bench + CI regression corpus
β β βββ src/
β β βββ lib.rs # Public API (scan_text, merge_findings)
β β βββ detector.rs # Detection engine + false-positive filtering
β β βββ patterns.rs # 64 secret regex patterns
β β βββ types.rs # SecretKind, Severity, SecretFinding
β β βββ filter.rs # URL/content filtering (skip CDNs, media)
β β βββ cookies.rs # Cookie parsing utility
β β βββ attributes.rs # HTML attribute formatting
β β βββ test_fixtures.rs # Secret-free fixture builders (tests + fuzz seeds)
β β βββ pattern_tests.rs # Per-pattern positive/negative cases
β βββ cli/ # CLI binary
β β βββ src/
β β βββ main.rs # Entry point + arg parsing
β β βββ scan.rs # File/dir/stdin scanning
β β βββ output.rs # Text, JSON, SARIF formatters
β βββ wasm/ # WASM bindings for extension
β βββ src/
β βββ lib.rs # Thin #[wasm_bindgen] wrappers
βββ extension/ # Chrome extension (Manifest V3)
β βββ manifest.json
β βββ background/service-worker.js
β βββ content/
β β βββ interceptor.js # Network traffic capture (MAIN world)
β β βββ content.js # DOM scanning (ISOLATED world)
β βββ popup/
β βββ icons/ # Extension icons (16/32/48/128 px)
β βββ test/ # vitest suite (happy-dom)
β βββ wasm/ # Compiled WASM output (build artifacts)
βββ fuzz/ # cargo-fuzz workspace (5 targets) β see fuzz/README.md
βββ .github/workflows/ # verify (CI), release, release-please, security, audit, scorecard, fuzz, stale
βββ justfile # Build, test, lint, clean recipes
secrets-spotter [OPTIONS] [PATH...]
| Option | Description |
|---|---|
[PATH...] |
Files or directories to scan. Reads stdin if omitted. |
-f, --format <FMT> |
text (default), json, sarif |
-s, --severity <L> |
Minimum severity: critical, high, medium, low (default) |
-g, --glob <P> |
Only scan files matching glob (comma-separated, e.g. "*.js,*.env") |
--no-ignore |
Also scan files a directory walk would skip via .gitignore / .ignore (skipped by default) |
--max-size <N> |
Max file size in bytes (default 2,097,152 = 2 MiB) |
--reveal |
Print full unredacted match values (off by default β secrets are masked) |
--no-color |
Disable colored output |
-q, --quiet |
Suppress output, exit code only |
-h, --help |
Print help |
-V, --version |
Print version |
Directory scans honor
.gitignore. Walking a directory skips files matched by.gitignore/.ignore/ git exclude rules β which often includes.env. Pass--no-ignoreto include them, or name the file directly as a path argument (explicitly-listed files are always scanned).
Exit codes:
| Code | Meaning |
|---|---|
0 |
No secrets found |
1 |
Secrets found |
2 |
Error (bad arguments, I/O failure) |
Page loaded β interceptor.js patches fetch, XHR, WebSocket, SSE, and cookies
β content.js extracts DOM + structured attrs, localStorage/
sessionStorage, and the URL; collects external <script src> URLs
β Text truncated to 2 MB and deduplicated by SHA-256 hash
β Background service worker runs the WASM scanner β and fetches the
external <script> bundles itself (free of the page's CSP)
β Rust matches against RegexSet (+ memchr prefix prefilter)
β False positives filtered (entropy, placeholders, code identifiers, English words)
β Findings deduplicated in single O(n) pass, merged across scan batches
β Findings shown in popup (JWTs include a decoder view)
β SPA navigations trigger re-scan automatically
External <script src> bundles are a primary secret-leak surface β hardcoded keys in config objects, keys used only inside a Worker, etc. β that the network interceptor misses unless the page later uses the key in an intercepted call. content.js collects their URLs and the service worker fetches and scans each one.
Doing the fetch in the worker rather than the page's MAIN world means the page's Content Security Policy (connect-src) doesn't apply and there are no console errors. Each unique URL is fetched once (cached), the read is size-capped, and CDN/library hosts are skipped via the shared should_scan filter β so only first-party bundles are fetched.
Default: on. Set SCAN_EXTERNAL_RESOURCES = false in extension/content/content.js to disable.
64 patterns across three tiers, plus keyword-gated legacy formats.
Match by a fixed prefix or structure baked into the credential itself β highest confidence.
| Service | Prefix/Structure | Severity |
|---|---|---|
| Anthropic | sk-ant-(api03|admin01|oat01)- |
Critical |
| AWS Access Key ID | AKIA... |
Critical |
| AWS Temp Key (STS) | ASIA... |
Critical |
| Cloudflare API | cfat_ / cfut_ / cfk_ |
Critical |
| Cloudflare Origin | v1.0-<24hex>-<146hex> |
Critical |
| Databricks | dapi |
Critical |
| DigitalOcean | dop_v1_ |
Critical |
| Discord Bot | [MNO]...(dot-separated base64) |
Critical |
| Doppler | dp.(st|sa|ct|pt|scim|audit). |
Critical |
| GCP OAuth | ya29. |
Critical |
| GitHub App | ghu_ / ghr_ (36 chars) |
Critical |
| GitHub App Install | ghs_ (legacy 36 chars + new stateless ~520 chars) |
Critical |
| GitHub OAuth | gho_ |
Critical |
| GitHub PAT | ghp_ / github_pat_ |
Critical |
| GitLab PAT | glpat- |
Critical |
| Google API Key | AIza |
Medium |
| Grafana | glsa_ |
Critical |
| Hashicorp Vault | hvs. |
Critical |
| Hugging Face | hf_ |
Critical |
| JWT | eyJ...eyJ... |
Medium |
| Linear | lin_api_ |
Critical |
| npm | npm_ |
Critical |
| OpenAI (legacy) | sk-...T3BlbkFJ... |
Critical |
| OpenAI (new) | sk-(proj|svcacct|admin)-...T3BlbkFJ... |
Critical |
| Password in URL | protocol://user:pass@host (incl. redis, mongodb, amqp, smtp) |
Critical |
| PostHog | ph[cxsar]_ |
Low / Critical |
| Private Key (PEM) | -----BEGIN...PRIVATE KEY----- |
Critical |
| Private Key (SSH2) | ---- BEGIN SSH2... / PuTTY-User-Key-File- |
Critical |
| Pulumi | pul- |
Critical |
| PyPI | pypi- |
Critical |
| SendGrid | SG. |
Critical |
| Shopify | shp(at|ss|ca|pa)_ |
Critical |
| Slack | xox[bpors]- |
Critical |
| Slack App-Level | xapp- |
Critical |
| Square | sq0atp- / sq0csp- / EAAA |
Critical |
| Stripe Publishable | pk_(live|test)_ |
Low |
| Stripe Restricted | rk_(live|test)_ |
High |
| Stripe Secret | sk_(live|test)_ |
Critical |
| Stripe Webhook | whsec_ |
Critical |
| Supabase Access | sbp_ |
Critical |
| Supabase Secret | sb_secret_ |
Critical |
| Twilio Account SID | AC + 32 hex chars |
Low |
| Twilio API Key SID | SK + 32 hex chars |
High |
| Vercel | vc[pirak]_ (vcp_/vci_/vca_/vcr_/vck_) |
Critical |
No fixed prefix on the credential itself β matched by a service or generic variable name sitting next to a high-entropy value (lower confidence than known-prefix, so entropy/format-gated).
| Service / Type | Trigger keyword(s) | Value shape | Severity |
|---|---|---|---|
| AWS Secret Access Key | aws_secret_access_key / secret_key |
40 base64 chars | Critical |
| Azure Subscription Key | subscription_key / ocp-apim-β¦ |
32 hex | High |
| Bearer Token | authorization: / auth: + Bearer |
20β512 chars | High |
| Cloudflare API Token | cloudflare⦠|
37β40 chars | Critical |
| Datadog API Key | dd_api_key / datadog_api_key |
32 hex | Critical |
| Generic API Key | api_key / apikey / api_secret |
20β64 chars | Medium |
| Generic API Token | api_token / access_token / client_secret |
20β512 (quoted) | High |
| Heroku API Key | heroku_api_key |
UUID | Critical |
| Mailgun API Key | mailgun⦠|
key- + 32 |
High |
Broad keyword match (key, token, secret, password) with Shannon entropy validation (β₯3.5 bits/char) to catch credentials that don't match any known prefix or service keyword.
Older token formats that are no longer issued but whose existing tokens may still be valid. Keyword-gated to avoid false positives on the (often generic) old shapes.
- GitHub β pre-2021 40-char hex PAT / OAuth token (gated on a
github/ghtoken keyword; a bare 40-hex is a SHA-1) - HashiCorp Vault β pre-1.10
s.service token (gated on avaultkeyword) - Vercel β pre-2026 24-char token (gated on a
vercelkeyword)
OpenAI's legacy sk-β¦T3BlbkFJβ¦ and Square's sq0atp- are also labeled (legacy) in findings.
- Placeholder detection β skips
YOUR_KEY,example,test,TODO, and similar - Example-key allowlist β skips published non-functional sample credentials (AWS's
AKIA...EXAMPLE, Stripe's documentation test keys) across all pattern tiers, including known-prefix - Template / interpolation rejection β skips
{{...}},${...},<...>,%...%, andname()call wrappers - Shannon entropy β rejects low-entropy values for entropy-gated patterns (UTF-8-aware, counts chars not bytes)
- Character class diversity β high-entropy values must mix at least 2 of: uppercase, lowercase, digits (symbols / non-ASCII don't count toward the threshold)
- English word filtering β ignores lowercase hyphenated phrases like
my-setting - URL / path exclusion β ignores values that look like URLs or file paths
- Code identifier rejection β skips camelCase, PascalCase, snake_case, SCREAMING_SNAKE, kebab-case, and dot-notation values
See CONTRIBUTING.md. The recipe for adding a new detection pattern is documented there with a worked example.
Security issues: please use the private advisory channel β see SECURITY.md.
Dual-licensed under MIT or Apache 2.0, at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Secrets Spotter by you, as defined in the Apache-2.0 license, shall be dually licensed as above, without any additional terms or conditions.