secrets-le 0.2.0

Find hardcoded credentials in a codebase, and never print one into the report
secrets-le-0.2.0 is not a library.

Useful? A star is how other developers find it — ★ GitHub · letools.dev/tools/secrets-le

The rule this is built around

A tool that finds secrets must not become the thing that leaks them.

A scanner's output goes into a CI log. That log is archived, is often world-readable on a public repository, is scraped, and outlives the credential. A scanner that prints what it found has disclosed every secret it detected to a wider audience than the commit would have.

So, without exception and without a flag:

  • No output ever contains a complete value. Not stdout, not stderr, not the MCP envelope, not an error message.
  • A preview is capped at eight characters and at half the value's length, and carries the length. Half-length is what makes the cap hold for short values — an eight-character cap on an eight-character password is the password.
  • The context line is masked too, because it is the raw source line and therefore contains the credential it is providing context for.
  • There is no --show-values. A flag that turns this off is a flag that ends up in someone's CI config.

A test plants credentials, runs the real binary, and asserts that not one of them appears anywhere in stdout or stderr. Another asserts the property exhaustively over value lengths 3 to 300. You can run both yourself: cargo test works from the published crate.

Sixty seconds

secrets-le .                          # scan a tree
secrets-le --sensitivity high .       # only high-confidence findings
secrets-le --no-ignore --hidden .     # reach .env and everything git ignores
cat config.env | secrets-le --stdin
./config/app.env:1:19  password database_password  hunter2h… (21 chars)  [high]
./config/app.env:2:19  aws-key  AKIAIOSF… (20 chars)  [high]
./src/client.js:1:13  token  ghp_1234… (40 chars)  [high]
3 findings in 2 files

The exit code is the answer: 0 nothing found · 1 findings · 2 the question was malformed. So secrets-le . is a CI step as it stands.

Install

Route Command Worth knowing
cargo cargo install secrets-le Any platform, needs Rust 1.88+.
From source git clone https://github.com/nolindnaidoo/secrets-lecd secrets-le/crate && cargo build --release The same build CI runs.

No runtime, no network, nothing written. It reads files and reports positions.

What it looks for

Thirty-four patterns across four families — API keys, passwords, tokens and private keys. Nineteen are named issuers: Anthropic, OpenAI, GitHub, GitLab, Slack, Stripe, Google, SendGrid, Mailgun, Sentry, npm, PyPI, Docker Hub, HashiCorp Vault, Terraform Cloud, Supabase, Shopify, Square and Azure SAS. The rest cover AWS access keys and secrets, Azure and GCP keys, bearer tokens, JWTs, OAuth/access/refresh tokens, session ids, cookies, database URLs with embedded credentials, and PEM private-key blocks (re-classified as SSH or PGP from their header).

The table lives in signatures/patterns.toml — reviewable without reading Rust, and mirrored from the extension's own table by a check that runs in both directions.

Every file is scanned, whatever its extension. A scanner that only looked at the extensions it recognised would report a clean tree while sitting next to a .bak full of passwords.

What it deliberately does not report

Ported from the extension as decisions, not oversights:

  • A bare x.y.z triple is not a JWT. A JWT header is base64 JSON and always begins eyJ. Version numbers, hostnames and module paths are the dominant false positive, and this kills them — at the cost of missing JWTs with non-JSON headers.
  • Template placeholders are never secrets: ${VAR}, {{var}}, <your-key>, and any run of one repeated character.
  • GCP project ids are identifiers, not credentials.

A false positive you will see

token: vscode.CancellationToken is reported as a token, because the generic key pattern matches an identifier ending in token followed by twenty-plus characters. It is the extension's behaviour, verified against it, and kept — parity is the contract, and a scanner whose two frontends disagree is worse than one with a known noisy pattern. --sensitivity high drops most of this class.

Options

--sensitivity <level>   low, medium (default) or high. Higher reports
                        fewer, more certain findings.
--no-api-keys           skip the API-key detectors
--no-passwords          skip the password detectors
--no-tokens             skip the token detectors
--no-private-keys       skip the private-key detectors
--strict                exit 2 if any file could not be read, rather
                        than reporting it and carrying on
--stdin                 read one document from stdin
--hidden                scan hidden files and directories too
--no-ignore             scan files that .gitignore excludes

The four --no-* toggles narrow what runs. They cannot widen it: there is no flag that turns masking off, and none that prints a value.

.gitignore is honoured, and that is a real risk

By default the walk skips what git ignores, which is where .env usually lives. A secret in an ignored file will not be committed — the threat this tool exists for — but it is still a secret on the disk.

Two things keep that from being invisible. Files whose names say they hold credentials (.env, .env.*, *.pem, *.key, .npmrc, id_rsa, …) are named individually when they are skipped. Everything else is a count at the end of the summary. --no-ignore --hidden reaches all of it.

The named list excludes vendored trees — node_modules, .vscode-test, vendor and friends — because six .npmrc files from inside a downloaded editor bundle is how a useful warning becomes noise. Measured on seven real repositories.

In CI

- name: No hardcoded credentials
  run: secrets-le --no-ignore --hidden .

Exit 1 fails the step on a finding. Exit 2 means the tool could not answer — an unreadable file or directory — and fails it too, because a scan that silently skipped something is worse than no scan.

As an MCP server

secrets-le mcp

Two tools, both returning { ok, data, diagnostics, meta }, both masked:

  • detect_secrets — content in, findings out. Touches no filesystem. The npm server ships the same tool with byte-identical output; one corpus runs against both.
  • secrets_le_scan — files or directories in, the same masked reports the CLI writes.

The masking matters most here: the caller is often a hosted model, so returning a value would post live credentials to a third party — a worse disclosure than the commit this tool prevents.

What it will not do

  • It does not rewrite files. Rewriting a file that holds a live credential is the most destructive thing this codebase could offer, and it needs a confirmation story that is owed before the code.
  • It does not validate credentials. Checking whether a key is live means transmitting it, to a third party.
  • It does not read git history. That is a different traversal, and conflating them would make "clean" ambiguous about what was examined.
  • It has no entropy detector. An untuned one produces a false-positive rate that trains people to ignore the output.

Full behaviour is in SPEC.md.

The other four ways to run it

Where What you get Install
VS Code Detection and in-place sanitising, in your editor Marketplace
Cursor, VSCodium, Windsurf The same extension Open VSX
Any MCP agent, via Node detect_secrets over stdio npx secrets-le-mcp · npm
Zed The MCP server as a context server add it by hand (no listing yet)

The extension is the one that can fix what it finds; this binary only reports. All sixteen LE tools are on letools.dev.

Documentation

What Where
What this tool is allowed to say — scope, output contract, refusals, non-goals SPEC.md
How the code is written and held together — architecture, invariants, the gates AGENTS.md
The VS Code extension this shares its extraction with README.md
What changed CHANGELOG.md
The tool's page, and the other fifteen letools.dev/tools/secrets-le

More from the LE family

Sixteen single-purpose tools for the work in front of every model. Each ships a Rust CLI and an MCP server. One page: letools.dev

Get it out

  • String-LE — Extract every string in a codebase, with its position, so a person can read them
  • Numbers-LE — Extract every hardcoded number in a codebase, so a person can check them
  • Units-LE — Extract every quantity with its unit, normalized, and refuse the ambiguous ones by name
  • Dates-LE — Extract every date and timestamp, and the exact instant each one resolves to
  • IDs-LE — Extract every UUID, ULID, NanoID, ObjectId and Snowflake, and decode the time inside
  • IPs-LE — Extract every IP address, CIDR block and MAC, normalized and classified by scope
  • URLs-LE — Extract every URL in a codebase, with its protocol and exact position
  • Paths-LE — Extract every file path in a codebase, and say whether it still points at anything
  • Colors-LE — Extract every color in a codebase, and say which ones are not in your palette

Check it

  • Regex-LE — Find every regex in a codebase, and report which can be driven into catastrophic backtracking
  • Versions-LE — Find where one dependency is constrained differently across a repository's manifests
  • i18n-LE — Identify the i18n library a project uses, then audit its catalogs by that library's rules
  • Scrape-LE — Check whether a page is scrapeable before the scraper is written, and say when it cannot tell

Guard it

  • Secrets-LE — Find hardcoded credentials in a codebase, and never print one into the report
  • EnvSync-LE — Compare the dotenv files in a tree, and say which keys are missing from which
  • Unicode-LE — Find the Unicode that hides meaning — bidi controls, invisibles, homoglyphs, mixed scripts

Each stands on its own: no shared crate, no published core. Where two of them agree, it is because the same answer was right twice.

Contactnolindnaidoo.com · GitHub · LinkedIn

Also by nolindnaidoo

Rust — pixelcoords and pixelactions are one loop: pixelcoords answers where, pixelactions acts there. Their own tools, their own voice — not part of the LE family.

License

MIT — see LICENSE.