secretscreen
Detect and redact secrets in key-value pairs, dicts, and environment variables.
Best-effort defense-in-depth. Not a security boundary.
Install
As a library:
pip install secretscreen
As a command-line tool — pipx or uv keep it in its own environment and put secretscreen on your PATH everywhere, rather than only in whichever venv is active:
pipx install secretscreen
uv tool install secretscreen
Zero dependencies, Python 3.11+.
Quick start
from secretscreen import redact_pair, redact_dict, audit_dict, Mode
# Single pair
redact_pair("DB_PASSWORD", "hunter2") # → "[REDACTED]"
redact_pair("APP_NAME", "myapp") # → "myapp"
# Dict with recursion
redact_dict({"db": {"password": "x", "host": "localhost"}})
# → {"db": {"password": "[REDACTED]", "host": "localhost"}}
# Aggressive mode (adds entropy detection)
redact_dict(env, mode=Mode.AGGRESSIVE)
# Audit mode (structured findings, no mutation)
findings = audit_dict(env)
# → [Finding(key="DB_PASSWORD", reason="key_pattern:password", ...)]
# Custom safe suffixes (keys ending with these are never redacted)
redact_dict(env, safe_suffixes=("_config", "_enabled"))
Command line
The library only helps Python callers. The CLI covers the shell side — docker exec, Makefiles, CI logs, anything you are about to paste somewhere.
secretscreen tandoor.env # redact and print, cat-like
docker exec app env | secretscreen # scrub a stream before it hits your terminal
secretscreen --audit config.json # findings only, no values, exit 1 if any
secretscreen [FILE...] reads stdin when FILE is omitted or '-'
--audit report findings without values; exit 1 if any
--format env|json|ini|yaml|dsn|auto default: auto-detect from extension, then content
--aggressive add entropy detection; more false positives
--explain account on stderr for every value, including the untouched ones
--replacement TEXT default: [REDACTED]
Redaction is structural, not line-based: it parses the format, so it catches DB_PASSWORD=hunter2 on the key name and rewrites postgres://admin:s3cr3t@host/db to postgres://admin:REDACTED@host/db without destroying the rest of the line. Comments, blank lines, quoting, export prefixes, INI sections and : separators all survive the round trip.
Inside a URL the replacement drops its brackets, because [ in that position makes the result unparseable — screening already-screened output would otherwise destroy it.
Compose files and grep output
$ secretscreen compose.yaml
# Watchtower stack
services:
watchtower:
image: containrrr/watchtower
environment:
WATCHTOWER_NOTIFICATION_URL: discord://REDACTED@1234567890
WATCHTOWER_CLEANUP: "true"
# keep an eye on this one
restart: unless-stopped
Indentation, comments, and block openers (services:) survive; only values are touched. List entries are screened whether they carry a key (- DB_PASSWORD=hunter2) or not (- discord://token@id).
This is line-oriented, not a real YAML parse — a parser would mean a third-party dependency, and this package is stdlib-only on purpose. Block scalars (|, >) are the visible consequence: their bodies are not key: value, so they are redacted and reported rather than quietly passed through.
grep's file:NN: and file-NN- markers are recognised and stripped before parsing, then put back on output, so grep -rn SECRET ~/stacks | secretscreen keeps its file-and-line context:
$ grep -n 'NOTIFICATION' compose.yaml | secretscreen
secretscreen: detected grep-style line prefixes; stripped before parsing, restored on output
6: WATCHTOWER_NOTIFICATION_URL: discord://REDACTED@1234567890
That stripping is what makes colon-separated detection safe to attempt at all. Without it the first colon on the line belongs to grep's line number, the key becomes 6, and the whole remainder — secret included — becomes one value that matches nothing. So when the input is only partly prefixed and the shape is ambiguous, the sniffer refuses the colon format and falls back to env, which redacts what it cannot parse. Useless output beats quiet output.
One case stays deliberately blunt: a single stream mixing formats, such as grep -rn across both .env and .yaml files, picks the majority format and redacts the rest wholesale. Loud and safe rather than half-parsed.
Config
Once you have found a false positive, --explain tells you which key it was. A config file stops you diagnosing it again next run.
# ~/.config/secretscreen.toml or .secretscreen.toml beside a stack
# Never redact these. Exact key names — no wildcards.
show = ["DEPLOY_PUBLIC_KEY"]
# Always redact these. Globs are fine.
hide = ["*_NOTIFICATION_URL", "*_WEBHOOK_URL"]
[detection]
mode = "normal" # normal | aggressive
entropy_threshold = 4.5
# Applies only while this filename is being processed.
[files."demo.env"]
show = ["API_TOKEN"]
Order: user config, then .secretscreen.toml files discovered upward from each input file, nearest last, then flags. Lists union rather than replace, so a closer file extends the broader one instead of silently discarding it. Discovery runs per input, so secretscreen a/compose.yaml b/compose.yaml can pick up a different config beside each. Add root = true to stop the upward walk.
hide takes globs. show does not. show = ["*_KEY"] would print AWS_SECRET_ACCESS_KEY, and it would go on matching keys nobody has written yet. Every show entry is a credential you are choosing to print, so every entry gets named in full. hide always wins over show, at every scope.
The same asymmetry decides what a rule can cover in JSON. hide names a key and takes whatever that key holds — a string, a port number, a whole object — and replaces all of it, because the shape and key names of a hidden subtree are usually the half worth hiding. show is honoured for a string and refused for anything else, with a note on stderr: vouching for one named key is a decision about one value, and vouching for an object is a decision about values nobody has added yet. In a --format dsn stream the whole line is the value, and rules match the key dsn.
A discovered project config may tighten redaction but not relax it. A .secretscreen.toml can arrive with a repository you cloned five minutes ago, or be dropped in a shared directory by anyone who can write there, so every setting one carries is read as a request to tighten:
| setting | from a discovered config |
|---|---|
hide |
applied |
show |
ignored |
[detection] mode |
only normal → aggressive |
[detection] entropy_threshold |
only downward |
root |
ignored |
--trust-config honours all five. Each ignored setting is named on stderr, so a rule that did not fire says so rather than looking like one that did.
The three settings below show matter as much as show does. show prints a credential and is the obvious one; entropy_threshold = 99 and mode = "normal" stop a credential being found, and root = true needs no rules of its own — it ends the upward walk before your own config one directory up is ever read. All four end with a secret on stdout and --audit exiting 0.
Only your own ~/.config file, or a file you name yourself with --config, can loosen anything.
Every loaded config is named on stderr, and a malformed one — bad TOML, an unknown setting, a wildcard where none is allowed — is fatal rather than skipped. A typo that silently disabled a hide rule would leave you believing you had configured something you had not.
Equivalent flags exist for one-off use: --show KEY, --hide GLOB, --entropy-threshold N, --no-config, and --config FILE — which uses that file only, skipping both discovery and your own ~/.config, and trusts it because you named it.
Knowing what was left alone
A missed secret is invisible: the output looks screened and nothing says otherwise. --explain writes an account of every value to stderr, so stdout stays a usable redacted stream and secretscreen app.env --explain > clean.env still works.
$ secretscreen watchtower.env --explain > screened.env
secretscreen: explain — key names and reasons only, no values
redacted WATCHTOWER_NOTIFICATION_URL url_credentials credential in userinfo position
vetoed GF_OAUTH_TOKEN_URL key_pattern matched 'token', suppressed by safe suffix '_url'
clean IMAGE_DIGEST - entropy 4.14 (aggressive would not flag; threshold 4.50)
clean DEPLOY_PUBLIC_KEY - entropy 4.67 (aggressive WOULD flag; threshold 4.50)
unscanned BACKUP_BLOB - 2202009 bytes exceeds the 65536-byte scan cap
Four states. redacted and clean are self-explanatory; the two in between are the ones worth reading. vetoed means a rule fired and something suppressed it — that is where the tool decided to stay quiet. unscanned means the size cap skipped the value-scanning layers.
Clean lines carry the nearest miss rather than silence. The entropy figure is computed even in normal mode, where that layer never runs, so you can see what --aggressive would change before turning it on.
Like --audit, this never prints a value — paste it into a bug report as-is.
What the exit code means:
| Code | Meaning |
|---|---|
| 0 | Everything was parsed and screened |
| 1 | --audit found secrets |
| 2 | Something could not be parsed or read — see stderr |
That third case is the one that matters. This is best-effort defense-in-depth, and a cat-replacement is exactly the tool people stop thinking about, so the CLI never prints unparsed content verbatim: a line it cannot structure is replaced with the redaction token, named on stderr, and turns the exit code non-zero. The same applies to values above the 64 KB detection cap — they are reported as unscanned rather than passed off as clean.
If you are scanning a git repository rather than config-shaped data, use gitleaks instead. That is a different job.
Detection layers
- Key-name denylist — substring match against ~30 known secret key patterns
- Structured value parsing — JSON, Python literals, DSN, INI, URL query params
- Value-format detection — 222 known formats via vendored gitleaks patterns (MIT)
- URL credential detection — partial redaction of
user:pass@hostURLs - Entropy detection — Shannon entropy for machine-generated strings (aggressive mode only)
Contributing
Bug reports and pull requests welcome. See CONTRIBUTING.md.
Support
If you find secretscreen useful, consider buying us a coffee.
License
MIT. Gitleaks patterns are also MIT-licensed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file secretscreen-0.5.1.tar.gz.
File metadata
- Download URL: secretscreen-0.5.1.tar.gz
- Upload date:
- Size: 98.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48a38051a35f837d3955422ade120528202bd892dce5e613da0ac5f0ad59f03d
|
|
| MD5 |
46689bca5c84c071490e3374771e5664
|
|
| BLAKE2b-256 |
8eb05cecd3c4cb2a0502eb68dc6bf54eeff2762d1c25c571a5b3bc577d0afcf9
|
Provenance
The following attestation bundles were made for secretscreen-0.5.1.tar.gz:
Publisher:
publish.yml on featurecreep-cron/secretscreen
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
secretscreen-0.5.1.tar.gz -
Subject digest:
48a38051a35f837d3955422ade120528202bd892dce5e613da0ac5f0ad59f03d - Sigstore transparency entry: 2468470055
- Sigstore integration time:
-
Permalink:
featurecreep-cron/secretscreen@3fa58040db14a0f3209abb8aa8f33f77e1ce18db -
Branch / Tag:
refs/tags/v0.5.1 - Owner: https://github.com/featurecreep-cron
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3fa58040db14a0f3209abb8aa8f33f77e1ce18db -
Trigger Event:
release
-
Statement type:
File details
Details for the file secretscreen-0.5.1-py3-none-any.whl.
File metadata
- Download URL: secretscreen-0.5.1-py3-none-any.whl
- Upload date:
- Size: 63.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c230bed0b53f06d023907891388cdcb27e61b0fa5a4b50a4ba3edaccd7165fb7
|
|
| MD5 |
29e1f1d386568020c94fe6d996c2e6b2
|
|
| BLAKE2b-256 |
e8c5310a2b21196d292721ba1e1012aed493769e6649bb8c4f5cf3bcf1a7bfe0
|
Provenance
The following attestation bundles were made for secretscreen-0.5.1-py3-none-any.whl:
Publisher:
publish.yml on featurecreep-cron/secretscreen
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
secretscreen-0.5.1-py3-none-any.whl -
Subject digest:
c230bed0b53f06d023907891388cdcb27e61b0fa5a4b50a4ba3edaccd7165fb7 - Sigstore transparency entry: 2468470125
- Sigstore integration time:
-
Permalink:
featurecreep-cron/secretscreen@3fa58040db14a0f3209abb8aa8f33f77e1ce18db -
Branch / Tag:
refs/tags/v0.5.1 - Owner: https://github.com/featurecreep-cron
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3fa58040db14a0f3209abb8aa8f33f77e1ce18db -
Trigger Event:
release
-
Statement type: