zxcvbn, but readable.
A privacy-first password audit for developers and CI/CD, with a CLI and an
importable Python API.
zxcvbn-first scoring by default: zxcvbn-ts with Node.js 18+, zxcvbn-python
otherwise, and a clean exit 4 if neither is installed. HIBP is
on by default, exit codes are semantic, JSON is strict, and the GitHub Action
fails closed. imps scan audits repository .env
files without ever displaying secret values.
The demo uses the real CLI and a public throwaway password; the first scene makes a live HIBP request, so its result and exit code are real. The second scene runs offline.
Node.js 18+ is required only for
--engine=ts. With--engine=auto(the default),impsuses the bundled zxcvbn-python engine when Node is unavailable. See Scoring engines.
Version 2.1.1. 2.1.0 added
imps scanfor recurring CI use, a separate scan JSON contract, git-aware.envdiscovery, honest token labeling, private-key detection, and actionable scan exit codes. 2.1.1 is a GitHub Action Marketplace metadata patch; the single password contract, scores, privacy, and network behavior are unchanged. Pre-commit integration follows in 2.1.2.
A small wrapper around excellent work — see A note on humility for what this tool cannot do.
Read this first: passing the password as an argument (imps "my-password") exposes it to shell history, terminal scrollback, and process listings. Run imps without arguments to use the hidden prompt, or pipe from stdin with imps -. This is an audit tool, not a password manager.
- Quick start
- Scan a repository
- A note on humility
- What it reports
- Scoring engines
- HaveIBeenPwned is ON by default
- CLI reference
- Exit codes
- JSON contract v2.0
- Python API
- GitHub Action
- Privacy
- The five 2.0 decisions
- What a clean result does not mean
- Compatibility and verification
- Development
- Release process
- License
pipx install ismypwdsafepip works too:
pip install ismypwdsafeThe default install includes the zxcvbn-python fallback, so a clean install has
a usable scoring engine without Node.js. Node.js 18+ is still needed for
--engine=ts.
imps # hidden prompt
imps --offline # hidden prompt, no network calls
imps - < password.txt # password from stdin
imps --json - < password.txt # machine-readable outputThe default engine is auto:
- zxcvbn-ts 4.x, if Node.js 18+ is on
PATH; - otherwise the bundled zxcvbn-python engine;
- if neither is usable, exit
4with an explanation.
native is never selected silently. It is the explicit legacy 1.x
heuristic; see Legacy native engine.
imps --engine=ts "correct-horse-battery-staple"
imps --engine=python "correct-horse-battery-staple" # bundled fallbackNode.js is not bundled or downloaded. Install it with your platform package manager:
# macOS
brew install node
# Debian / Ubuntu
sudo apt install nodejs npm
# Windows
winget install OpenJS.NodeJS.LTSVerify with node --version. Node is needed only for --engine=ts and for the
ts branch of auto. If Node is unavailable, auto uses the bundled
zxcvbn-python engine rather than pretending the native heuristic is zxcvbn.
imps scan is the recurring CI use case: it finds password-like values in
dotenv files and tells you what to fix without printing the secrets. Pre-commit
integration using staged files is planned for 2.1.2.
imps scan # tracked .env files in this repository
imps scan .env .env.local # explicit files always work
imps scan --include-untracked # opt into local/ignored .env files
imps scan --no-git # non-git checkout
imps scan --json # values-free scan JSON
imps scan --offline # no HIBP or other network calls
imps scan --min-score 70 # stricter score threshold
imps scan --engine=ts --include-untrackedDiscovery is git-aware by default: tracked files matching .env, .env.*,
*.env, or *.env.* are scanned. Explicit paths override discovery, so an
ignored .env.local is scanned when you name it. --include-untracked adds
untracked and ignored files. Outside a git work tree, the command exits 2 and
asks for --no-git or an explicit path.
The scanner:
- never prints, logs, or persists a value, a value fragment, or a password in an error message;
- scores
PASSWORD,PASSWD,PASSPHRASE,PWD, andPASSkeys with the selected zxcvbn engine; - labels
SECRET,TOKEN,API_KEY,ACCESS_KEY,PRIVATE_KEY,CLIENT_SECRET, andAUTH_TOKENvalues asTOKENwith no numeric score and no HIBP lookup — HIBP is a password corpus, not an API-key leak database; - treats a private-key PEM block as
CRITICAL, exits6, and never sends it to HIBP; - skips hashes, salts, file paths, URLs, usernames, lengths, reset tokens, and other metadata suffixes;
- ignores empty values,
${VAR}references, and obvious placeholders; - reports
path:line, key name, score, rating, pattern labels, token format, and breach status.
HIBP is on by default for password-class findings. Duplicate values share one
request; concurrency is four; each request has a five-second timeout; the whole
scan has a 60-second deadline. On a timeout, completed findings are retained,
the result is marked incomplete, and the exit code is 5 unless a more severe
finding exists. --offline and --no-breach-check never produce exit 5.
The scan has its own JSON contract, schema/scan.schema.json,
with schema_version: "1.0" and kind: "scan". --json-full is intentionally
unsupported by scan and exits 2: scan never emits values.
{
"schema_version": "1.0",
"kind": "scan",
"tool_version": "2.1.1",
"exit_code": 3,
"summary": {
"files": 1,
"values": 1,
"weak": 1,
"medium": 0,
"strong": 0,
"pwned": 1,
"unknown": 0,
"critical": 0,
"unparseable_lines": 0,
"empty_values": 0,
"references": 0,
"placeholders": 0,
"skipped_large": 0
},
"findings": [
{
"path": ".env",
"line": 4,
"key": "DB_PASSWORD",
"kind": "password",
"score": 0,
"rating": "weak",
"patterns": ["dictionary"],
"breach": {
"checked": true,
"pwned": true,
"breach_count": 12345,
"reason": null
},
"token_format": null,
"message": null
}
],
"warnings": [],
"incomplete": false,
"unchecked_values": 0,
"engine_error": null
}Full details, including parsing and the exit precedence, are in
docs/scan-spec.md.
IsMyPwdSafe is a small wrapper around other people's excellent work. The hard parts - zxcvbn, the language dictionaries, and the HaveIBeenPwned range API - are not ours. What we try to add is a readable report, a small privacy budget, and predictable failure modes.
It can help you notice that a password is a dictionary word, a keyboard walk, a date, a repeat, or a known breach. It cannot prove that a password is safe. A clean result is not a certificate, an audit opinion, or a guarantee. It means "this particular heuristic did not find a problem in this particular run".
We would rather say UNKNOWN and exit 5 than tell you a password is safe when
the breach check did not run. That is the main design principle behind 2.0, and
it is also why the tool is occasionally annoying in CI.
The default zxcvbn path renders:
- a readable
0-100score; zxcvbn's raw0-4score is kept in JSON; - estimated time to crack under four attacker models;
- pattern matches that explain the score: dictionary matches, keyboard walks, repeats, dates, sequences, regexes, and brute force;
- zxcvbn feedback (warning + suggestions) under
WHAT TO DO; - HIBP breach status.
The report uses the full terminal width. On wide terminals, time-to-crack and
pattern matches sit side by side; on narrow terminals they stack. Empty zxcvbn
sections are omitted when the legacy native engine is used. On an interactive
TTY the banner and sections fade in, section titles briefly resolve from
scrambled characters, and the summary score bar fills with an eased motion;
non-TTY output and NO_COLOR are printed immediately.
The legacy native engine is explicitly limited; see Legacy native engine.
| zxcvbn | Percentage | Rating |
|---|---|---|
0 |
0 |
weak |
1 |
25 |
weak |
2 |
50 |
medium |
3 |
75 |
strong |
4 |
100 |
strong |
If HIBP says the password is pwned, the final score is forced to 0 and the
result uses exit code 3. The reason is exposed as score.capped_by = "hibp".
The zxcvbn sequence, guesses, and crack-time estimates are still reported, and
score.raw_score_0_4 keeps the original zxcvbn score; only the final score is
capped.
"Available" is not the same as "used". The JSON contains both values:
engine_requestedis what you asked for (auto,ts,python, ornative);engine_usedis the engine that actually produced the result;engine_versionis the version reported by that engine;engine_fallbackistrueonly whenautofell back to zxcvbn-python because zxcvbn-ts was unavailable.
For example, without Node.js, --engine=auto reports
engine_requested: "auto", engine_used: "python", and
engine_fallback: true. If auto cannot find any zxcvbn engine, the command
exits 4 and engine_used is null. It does not quietly run the native
heuristic. Consumers should branch on engine_used, not on engine_requested.
| Engine | engine_version |
|---|---|
ts |
zxcvbn-ts version reported by the bundle, for example "4.2.0" |
python |
zxcvbn-python package version, for example "4.5.0" |
native |
null |
| error / no usable engine | null |
--engine=native keeps the 1.x ASCII-oriented entropy and pattern model for
compatibility. It is explicit only and is not recommended for new audits. It
does not produce zxcvbn sequence, feedback, or crack-time data:
| JSON field | Value with --engine=native |
|---|---|
engine_version |
null |
score.raw_score_0_4 |
null |
sequence |
[] |
feedback |
{"warning": null, "suggestions": []} |
guesses / guesses_log10 |
null |
crack_times_seconds / crack_times_display |
{} |
calc_time |
null |
The score.value and score.rating still come from the legacy heuristic.
By default imps makes exactly one outbound request: HIBP's Pwned Passwords range API.
- The password and the full SHA-1 hash never leave the machine.
- Only the first five characters of the SHA-1 hash are sent.
- Responses use
Add-Padding: true. - The local suffix match happens in memory.
- Timeout: 5 seconds.
Skip it explicitly:
imps --no-breach-check "my-password" # skip HIBP
imps --offline "my-password" # skip HIBP and the optional update checkWhen skipped, breach.checked is false, breach.pwned is null, and
breach.reason is "disabled" (or "offline" for --offline). The exit code
is based only on the score (0 or 1), never 5.
If HIBP cannot be reached, imps does not call the password safe:
- the report says
UNKNOWN; - the JSON has
breach.checked = true,breach.pwned = null,breach.reason = "api_error"; - the exit code is
5.
The zxcvbn score is still shown, but CI must treat exit 5 as a failure.
The GitHub update check is opt-in and separate: --check-updates.
| Command | HIBP | GitHub update check |
|---|---|---|
imps "pwd" |
yes | no |
imps --no-breach-check "pwd" |
no | no |
imps --check-updates "pwd" |
yes | yes |
imps --offline "pwd" |
no | no |
imps [--engine {auto,ts,python,native}] [-M] [-o | --no-breach-check]
[-s | -dw] [--check-updates] [--json | --json-full | -q] [password|-]
imps scan [--engine {auto,ts,python,native}] [--min-score N]
[--offline | --no-breach-check] [--include GLOB]...
[--include-untracked | --no-git] [--json | -q] [path ...]
| Flag | Description |
|---|---|
--engine |
auto (default), ts, python, or native. |
-M, --Mask |
Mask the password and matched tokens in the terminal UI. JSON is unaffected. |
-s, --short |
Show the score, breach verdict, and engine line. |
-dw, --darkweb |
Show only the HIBP breach check. |
-o, --offline |
Skip HIBP and all other network calls. |
--no-breach-check |
Skip only HIBP. |
--check-updates |
Enable the opt-in GitHub version check. |
--json |
Strict JSON output; matched tokens are redacted. |
--json-full |
JSON including matched tokens. Use with care. |
-q, --quiet |
Suppress all output; return only the exit code. |
password |
Password to audit. Omit for hidden prompt; use - for stdin. |
Flag notes:
-sand-dware mutually exclusive.-schanged in 2.0: it now includes the breach verdict and engine line, not only the score. See the CHANGELOG.-Maffects the terminal UI only. It does not change--jsonor--json-full:--jsonredacts matched tokens;--json-fullincludes them. Use--json-fullonly when you explicitly need them.-dwis named for historical reasons. It shows the HIBP breach check, not a dark-web scan.-qsuppresses output but does not change input handling. If stdin is a TTY, the hidden prompt still appears. For non-interactive use, pipe the password (echo 'pwd' | imps -q -) or read it from a file (imps -q - < password.txt). Passing it as an argument is possible but exposes it to shell history; see the warning at the top.- In a non-interactive shell with no password and no stdin,
impsexits2instead of hanging. imps scanis a subcommand. To audit the literal passwordscan, useimps -- scan.--json-fullis not supported by scan and exits2.
| Code | Meaning |
|---|---|
0 |
OK: score >= 50, not pwned, breach status known or disabled |
1 |
Weak: score < 50 |
2 |
Usage/input error, including an empty password |
3 |
Pwned: found in HIBP |
4 |
Engine error: no usable engine, Node too old, engine failure |
5 |
HIBP status unknown: do not treat the password as safe |
6 |
imps scan only: a private-key block was found |
130 |
Interrupted with Ctrl+C |
imps scan uses the same codes with a different precedence:
6 (critical) > 3 (pwned) > 5 (HIBP unknown or incomplete) > 4 (engine
error) > 1 (below --min-score) > 0 (clean). A usage error is 2.
For CI, use -q when you only need the code, or --json when you need the data:
imps -q --engine=ts - < password.txt
echo $?--json emits a strict, versioned document validated against schema/result.schema.json. Non-finite floats are converted to null; the output never contains Infinity or NaN.
By default, sequence[].token, matched_word, base_token, sub_display, regex_match, and subs are replaced with "<redacted>". Use --json-full only when you explicitly need the matched tokens.
redacted is true for --json and false for --json-full. Native-engine
JSON follows the same flag even though it has no tokens to reveal.
The default JSON has no top-level password field, and the password is not
included in input, breach, feedback, or the redacted sequence. The test
suite checks this with a unique marker password against the selected engine; see
tests/test_privacy.py. --json-full may reveal
password fragments through sequence[].token, so treat it as sensitive output.
Example (abridged):
{
"schema_version": "2.0",
"tool_version": "2.1.1",
"exit_code": 0,
"error": null,
"input": {
"length": 28,
"charset_size": 59,
"has_lower": true,
"has_upper": false,
"has_number": false,
"has_symbol": true,
"has_non_ascii": false
},
"score": {
"value": 100,
"rating": "strong",
"raw_score_0_4": 4,
"capped_by": null
},
"guesses": 100000000001.0,
"guesses_log10": 11.0,
"crack_times_seconds": {
"online_throttling_100_per_hour": 3600000000036.0,
"online_no_throttling_10_per_second": 10000000000.1,
"offline_slow_hashing_1e4_per_second": 10000000.0001,
"offline_fast_hashing_1e10_per_second": 10.0000000001
},
"crack_times_display": {
"online_throttling_100_per_hour": "centuries",
"online_no_throttling_10_per_second": "centuries",
"offline_slow_hashing_1e4_per_second": "4 months",
"offline_fast_hashing_1e10_per_second": "10 seconds"
},
"feedback": {
"warning": null,
"suggestions": []
},
"sequence": [
{
"pattern": "bruteforce",
"token": "<redacted>",
"i": 0,
"j": 27,
"guesses": 100000000000
}
],
"breach": {
"checked": true,
"pwned": false,
"breach_count": 0,
"reason": null
},
"engine_requested": "auto",
"engine_used": "ts",
"engine_version": "4.2.0",
"engine_fallback": false,
"calc_time": 11.0,
"redacted": true
}calc_time is in milliseconds. zxcvbn-ts and zxcvbn-python report it; the
legacy native engine returns null. The field name is unchanged in the 2.0
schema. A calc_time_ms alias is deferred to a future minor release rather than
changing a published schema in a patch.
When the exit code is non-zero, --json still emits a valid schema document
unless --quiet is also used. The envelope is stable:
| Exit | JSON emitted | error |
input |
score |
breach |
engine_used |
|---|---|---|---|---|---|---|
0/1/3 |
yes | null |
object | object | object | real engine |
2 |
yes | string | null |
null |
null |
null |
4 |
yes | string | null |
null |
null |
null |
5 |
yes | null |
object | object, with no HIBP cap | pwned: null, reason: "api_error" |
real engine |
130 |
not guaranteed | — | — | — | — | — |
For exit 5, score is computed and present, but it is not a safety verdict:
breach.pwned is null and the command fails closed. For exit 2 and 4,
score is null; a consumer that does imps --json | jq .score.value must
handle that case.
For exit 130 (Ctrl+C), JSON may be partially emitted or absent. Do not rely
on it; check the exit code instead.
If --quiet and --json are combined, --quiet wins and no JSON is written.
--json-full does not change the error envelope.
--engine=native is valid JSON with the same top-level shape, but the
zxcvbn-specific fields are empty rather than absent:
{
"engine_requested": "native",
"engine_used": "native",
"engine_version": null,
"score": {
"value": 100,
"rating": "strong",
"raw_score_0_4": null,
"capped_by": null
},
"guesses": null,
"crack_times_seconds": {},
"crack_times_display": {},
"feedback": {
"warning": null,
"suggestions": []
},
"sequence": [],
"calc_time": null
}The same analysis is available from Python; the CLI is one front end. The
supported surface is analyze_password, AuditResult, result_to_dict,
render_json, SCHEMA_VERSION, and __version__.
See Python API
for examples, error handling, engine requirements, and JSON output. For other
languages, use imps --json; this is a Python API, not a cross-language SDK.
name: Password audit
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Audit password
id: audit
uses: Roma476/IsMyPwdSafe@v2
with:
mode: audit
password: ${{ secrets.PASSWORD_TO_AUDIT }}
engine: auto # auto | ts | python | native
node-version: "20"
min-score: 70
offline: false # true skips HIBP
- name: Print result
run: echo "Score ${{ steps.audit.outputs.score }} (${{ steps.audit.outputs.rating }})"The Action passes the password through stdin, not as a process argument. It fails closed on weak, pwned, engine-error, and HIBP-unknown results.
Use scan mode in a repository checkout to audit .env files without passing a
secret to the Action:
- uses: actions/checkout@v4
- name: Scan repository .env files
uses: Roma476/IsMyPwdSafe@v2
with:
mode: scan
engine: auto
min-score: 70
offline: false # HIBP on by default
annotation-limit: 20
scan-json-artifact: ismypwdsafe-scanScan mode emits file:line annotations for critical private keys, pwned
passwords, unknown HIBP status, and weak scores. It writes a values-free JSON
artifact for the full report. Private-key findings annotate as errors and exit
6; the artifact is uploaded even when the scan fails.
For exit 2 and 4, score is null and the Action's score output is empty;
use exit-code as the reliable signal. The Action already exits before applying
min-score in those cases.
| Input | Default | Description |
|---|---|---|
mode |
audit |
audit for one password or scan for repository .env files. |
password |
— | Required in audit mode. Always pass through GitHub Secrets. |
engine |
auto |
auto, ts, python, or native. |
node-version |
20 |
Node version for auto/ts. |
min-score |
70 |
Fail below this score. |
offline |
false |
Skip HIBP and all network calls. |
scan-paths |
— | Optional space-separated files/directories; empty uses git discovery. |
scan-include |
— | Optional comma-separated extra glob(s). |
include-untracked |
false |
Scan mode: include untracked/ignored files. |
no-git |
false |
Scan mode: walk the current directory instead of git discovery. |
annotation-limit |
10 |
Maximum scan annotations; the JSON artifact has the full report. |
scan-json-artifact |
ismypwdsafe-scan |
Artifact name; set empty to skip upload. |
| Output | Description |
|---|---|
score |
0-100 audit score. Empty in scan mode. |
rating |
weak, medium, or strong. Empty in scan mode. |
exit-code |
The imps exit code for the selected mode. |
scan-findings |
Scan mode: number of findings. |
scan-critical |
Scan mode: number of critical private-key findings. |
scan-pwned |
Scan mode: number of pwned password findings. |
scan-incomplete |
Scan mode: true when HIBP timed out. |
- No telemetry, analytics, crash reporting, accounts, or local audit history.
- No logs, cache, or config written on first run.
- One request by default: HIBP k-anonymity. The GitHub update check is opt-in.
--offlinedisables all outbound requests.- JSON is token-redacted by default.
sha1 = SHA1(password) # local only
prefix = sha1[:5].upper() # transmitted
suffix = sha1[5:].upper() # matched locally
Not transmitted: plaintext password, full SHA-1, password length, character composition, account, or identifier. HIBP receives the prefix and the HTTP metadata inherent to any request.
While building 2.0 we changed five decisions. They are listed here because they are part of the contract, not hidden implementation details.
| # | Decision | Why | Cost |
|---|---|---|---|
| 1 | auto is the default engine, not ts and not native. |
A normal install should use the best available zxcvbn engine without silently changing the meaning of the result. | If neither zxcvbn-ts nor zxcvbn-python is available, the command fails with exit 4. |
| 2 | HIBP is on by default, and an unreachable HIBP is UNKNOWN with exit 5. |
A missing breach check must not be confused with a clean breach check. | One anonymous range request per audit; --offline is required for a strictly local run. |
| 3 | zxcvbn's 0-4 score maps to 0 / 25 / 50 / 75 / 100. |
The percentage is easier to read than a bare 0-4, and the weak/medium/strong boundary is explicit. |
The number is a presentation choice, not extra evidence; it does not make zxcvbn more precise. |
| 4 | A pwned password is forced to score 0, rating weak, with score.capped_by = "hibp". |
"Strong but already breached" must not look safe in a dashboard or a CI summary. | The zxcvbn details are still reported, but the final score is deliberately dominated by the breach. |
| 5 | --json redacts matched tokens by default; --json-full is the explicit opt-in. |
JSON often ends up in logs, artifacts, and issue comments. The default should not leak password fragments. | Users who need the matched tokens must ask for them and handle the output carefully. |
The exact exit-code table and score mapping are documented above. If you build a CI policy on this tool, use those tables rather than assumptions from 1.x.
This is the most important section in the README.
A password that scores 100, is not found in HIBP, and has a long crack-time
estimate is not proven safe. It has passed one heuristic and one partial
breach corpus at one point in time. That is all.
In particular:
- zxcvbn is a heuristic. It is very good at recognising common patterns and bad at knowing your personal context. A password can be strong by its model and still be reused across five services.
- A clean HIBP result is not a guarantee. HIBP is a large corpus, not every breach. A password can be absent from HIBP and still be compromised or reused.
- HIBP availability matters. If HIBP is unreachable, the result is
UNKNOWN,breach.pwnedisnull, and the exit code is5. The zxcvbn score is still shown, but it is not a safety verdict. - Crack times are scenarios, not predictions. They assume fixed hash rates and do not model rainbow tables, targeted dictionaries, GPU optimisations, side channels, or the attacker who already knows something about you.
- The native engine is legacy.
--engine=nativeuses the 1.x ASCII-oriented entropy model and a finite pattern list. It is retained for compatibility, not recommended for new use. - Node is external.
--engine=auto/tsneeds Node.js 18+. The default install includes zxcvbn-python; if that fallback is unavailable or broken,impsexits4. - Bundled languages are English. The shipped zxcvbn-ts bundle includes the
language-commonandlanguage-endictionaries. It does not claim 20+ bundled languages. - This is not a password manager, a policy engine, or a compliance tool. It
audits one password at a time, or
.envfiles in scan mode; it does not store, generate, rotate, or centrally enforce credentials. It also cannot tell whether a password is reused, shared, written on a sticky note, or sent over an insecure channel.
If you need a guarantee, you need a password manager, a reviewed policy, multi-factor authentication, and an incident process. This tool can be one small input to that work, not a replacement for it.
| Area | What is covered |
|---|---|
| Python runtime | >=3.8 in package metadata; CI covers 3.8, 3.10, and 3.12 |
| Recommended engine | Node.js 18+ and zxcvbn-ts 4.2.0 |
| Fallback engine | Bundled zxcvbn-python 4.4+ |
| Operating systems | CI coverage on Ubuntu, macOS, and Windows |
| JSON contract | validated against schema/result.schema.json and schema/scan.schema.json |
| zxcvbn-ts bundle | rebuilt and checked for reproducibility in CI on Node 18 and 20 |
| HIBP failure path | unit tests plus a simulated missing-fetch run; exit 5 |
| Action | smoke-tested with strong, weak, below-threshold passwords, and scan mode |
"CI covers" means the workflow is configured to run those jobs; it is not a claim that every combination has been manually exercised on every machine. The 2.0.0 release was exercised as a release candidate on TestPyPI before the stable tag; patch releases follow the same tag-based publish workflow.
git clone https://github.com/Roma476/IsMyPwdSafe.git
cd IsMyPwdSafe
python -m pip install -e ".[test,dev]"
pytestNode 18+ is required for the golden suite that covers the ts engine. Without Node, those golden tests are skipped; the native and python paths still run.
Rebuild the zxcvbn-ts bundle after changing ismypwdsafe/js/score.mjs or the JS dependencies:
cd ismypwdsafe/js
npm ci
npm run buildRegenerate golden fixtures:
python scripts/generate_golden.py --engine tsRegenerate the terminal demo GIF after changing the CLI output:
vhs docs/assets/demo.tapeProject layout:
ismypwdsafe/
├── engines/ # auto resolution, ts, python, native
├── js/ # score.mjs, package.json, committed zxcvbn-ts bundle
├── core.py # result model, HIBP overlay, exit codes
├── render.py # strict JSON + redaction
├── cli.py # argparse, stdin, output
├── scan.py # imps scan orchestration and scan JSON
├── scan_parser.py # dotenv parser, key classifier, PEM detection
├── ui.py # terminal report
├── network.py # HIBP + optional update check
└── metrics.py # native/legacy metrics
The release is intentionally boring. version.txt is the single source of
truth, and a tag is only created after the candidate has been exercised.
- Set
version.txtto the candidate version (for example2.1.0rc1) and update the changelog. - Run the full local checks:
pytest ruff check . mypy ismypwdsafe python -m build - Push the candidate tag. The publish workflow sends tags containing
a,b, orrcto TestPyPI. - Install from TestPyPI in a clean environment and run the GitHub Action with
the candidate tag, for example:
uses: Roma476/IsMyPwdSafe@v2.1.0rc1
- Let the candidate sit for a short soak period. Check the TestPyPI artifact, the Action result, and the CLI output before promoting it.
- Only then create the stable tag (
v2.1.0), publish to PyPI, and merge the release branch intomain. - Move the
v2major-version tag to the released commit souses: Roma476/IsMyPwdSafe@v2follows the 2.x line.
Patch releases do not rewrite already published tags or PyPI provenance. If a release needs a correction, publish a new patch rather than moving a tag.
PyPI and TestPyPI publishing uses GitHub OIDC Trusted Publishing. The repository
must have the pypi and testpypi environments, and the corresponding trusted
publishers must be configured on PyPI/TestPyPI for this repository and the
publish.yml workflow. No long-lived PyPI token belongs in GitHub secrets.
MIT. See LICENSE.
