You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Breaking changes
A nonexistent scan path, a symlink operand, or an operand that is not a regular file or directory is a hard error (exit 2) instead of a silent clean pass; scripts that relied on the old exit 0 must handle the error
Pre-push hooks scan the pushed revision ranges from the hook's stdin instead of the worktree; reinstall hooks with key-watch hook install pre-push to pick up the new script
scan --git-history walks every ref (git log --all) instead of only the checked-out branch; pass --rev-range to narrow the walk
Unknown keys in .keywatch.toml and detector files are rejected instead of silently ignored; fix any misspelled keys before upgrading (description on rules remains accepted)
Identity-number detectors (SSN, Aadhaar, PAN, Voter ID, ABHA) require the line to name the document; bare number dumps without a label on the same line are no longer flagged
Baseline migration: fingerprints now anchor to the repository root, so baselines written from a subdirectory by earlier releases stop suppressing, and entries from the renamed Stripe/Kimi detectors re-fire once; run key-watch scan . --update-baseline after upgrading to refresh the baseline
Added
scan --staged scans only the lines a commit adds
Baselines are auto-discovered from .keywatch-baseline.json; --no-baseline-discovery opts out
update-baseline workflow regenerates the baseline via a pull request
scan --fail-on-unscannable fails a strict scan when a file could not be read; the pre-commit hook passes it so an unscannable staged file cannot pass silently
CI scans this repository with KeyWatch and fails if the committed baseline has drifted
--prune-baseline rewrites the baseline from current findings, dropping entries for deleted files and rotated credentials; requires --update-baseline and a whole-tree scan, and prints what it dropped
scan --git-history --rev-range <range> restricts a history scan to a revision range; the pre-push hook uses it to scan exactly the pushed commits
Detectors for GitHub fine-grained PATs (github_pat_), GitHub refresh tokens (ghr_), AWS temporary keys (ASIA/ABIA/ACCA), AWS secret access keys (context-gated), modern Slack bot tokens, Slack app tokens (xapp-), Slack webhook URLs, and modern OpenAI project keys (sk-proj-/sk-svcacct-/sk-admin-)
Non-verbose scans print the location, type and redacted match of every finding instead of only a count
GitHub classic tokens are validated against their built-in CRC32 checksum, so base62 lookalike strings are rejected offline
UTF-16 files and staged blobs with a byte-order mark are decoded and scanned instead of being skipped as binary
Base64 runs of 24 or more characters are decoded and their text is scanned once more, so an encoded credential cannot pass unseen
--trusted-detectors and --no-repo-config split the two behaviors of --no-config-discovery, which remains as their combination
--max-file-size <MB> skips larger files and reports them as unscannable
Changed
Pre-commit hooks scan the staged diff instead of whole files
Config discovery searches parent directories up to the repository root
Hook messages abbreviate the home directory as ~
Findings for the same file, line and matched text collapse to the highest severity across all scan modes, so overlapping detectors report a secret once
Reports redact matched text by default; --show-secrets opts into raw values, and matches shorter than 8 characters are always described by length only
Reports summarise exclusions as a count plus a sample instead of listing every path, and report git-rendered binary files as unscannable rather than excluded
Lockfiles (Cargo.lock, package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, npm-shrinkwrap.json, go.sum, and other generated manifests) are excluded from scans by default
A directory that cannot be listed is reported as unscannable, so --fail-on-unscannable catches it
KeyWatch warns on stderr when an external detector file replaces the embedded set, when config overrides disable detectors, and when KEYWATCH_CONFIG_PATH is set but ignored
Stripe publishable keys (pk_) report as LOW under their own detector; rk_ restricted keys are detected as secrets
SARIF reports no longer claim precision: very-high for every rule and omit semanticVersion when unknown
Fixed
scan --git-history applies --exclude, skips the baseline file, and reports real file paths instead of a synthetic <git-history> key that no baseline could match
scan --staged is not fooled by diff.relative, which made git drop changes outside the current directory
--output files are readable only by their owner, including when the file already existed with wider permissions
Config is not trusted from a world-writable directory or file, so a .keywatch.toml dropped in /tmp cannot weaken scans beneath it
KEYWATCH_CONFIG_PATH is ignored in trusted mode whenever it points inside the tree being scanned, wherever the process runs from
Baseline suppression reports how many findings it hid, instead of applying silently
CreditCardDetector requires an issuer prefix and a valid Luhn checksum, instead of matching any 13-16 digit run; Discover's 644-649 and 65 ranges are covered
HighEntropyDetector could never fire (its 4.0 threshold is the ceiling for hex) and now runs, restricted to lines naming a credential
PKCS#8 private key headers (BEGIN PRIVATE KEY, BEGIN ENCRYPTED PRIVATE KEY) are detected
PhoneNumberDetector needs punctuation or a country code, so unix timestamps are not phone numbers
Detectors can require a structural check via validate = "luhn"
Hooks use built-in detectors, so a detectors.toml committed to a scanned repository can no longer replace the detector set and disable its own scan
Files git renders as binary (including text marked -diff in .gitattributes) are read from the index instead of being reported clean
Base64Detector matches from 28 characters, the length where entropy can actually separate base64 from identifiers
scan --staged no longer misses findings under color.ui = always or custom diff prefixes
Non-UTF-8 files no longer abort a staged scan
A malformed diff hunk header is reported instead of silently attributing its findings to line 0
Diff paths that git quoted (names containing quotes or control characters) are unescaped before attribution
The baseline file is no longer scanned as input to itself, including staged scans run from a subdirectory
GenericKeyValueDetector and RandomString no longer flag code identifiers (let payment_method_token = card_token, snake_case serde attributes)
PasswordDetector no longer flags $PWD:
GenericKeyValueDetector no longer flags bare CamelCase type paths (token: PaymentTokenData,)
PasswordDetector no longer flags Rust expressions (password: Secret<String>, password: config.password.clone(), Some(Secret::new(...))) as credentials
A failed git cat-file during a staged scan reports itself instead of claiming git diff failed
Custom rules in .keywatch.toml support allowlist, keywords, entropy and validate, matching built-in detector definitions
Pre-push repository filters fail closed on Windows drive-path remotes instead of misparsing the drive letter as a host
Chunked streaming scans no longer duplicate multiline matches that land inside the window overlap
GenericKeyValueDetector reaches its auth and _key pattern branches; the keyword prefilter previously never let auth = ... or encryption_key = ... lines reach the regex
NewRelicAPIKeyDetector matches real uppercase NRAK keys; the lowercase-only class could never fire
SlackTokenDetector keyword and pattern prefixes agree; xoxa- tokens were unreachable and xoxr- was inert
48+ character sk- tokens report as OpenAI keys instead of being mislabeled Kimi/Moonshot
The same file passed under two spellings (dup.txt ./dup.txt) reports each finding once
Errors from failed git diff/git log include one summarized stderr line instead of letting git dump pages of usage text
--fail-on-unscannable names the unscannable files in its summary instead of printing "No secrets found." next to exit code 1
The AWS documentation example secret in .env.example files is no longer flagged by GenericKeyValueDetector
The composite Action publishes its effective exit code on report failures instead of a stale 0, and findings-count stays numeric (-1 when unknown)
Files with invalid UTF-8 are decoded lossily and scanned instead of silently skipped; NUL-containing files are reported as unscannable
Finding's plugin_name field is now detector_name in the code; the JSON report and baseline schema still emit/accept plugin_name
CustomRule.description is accepted and ignored so configuration files written for earlier releases keep parsing (the text was never surfaced)
False-positive reductions in the built-in detectors: AWS's documentation example key, placeholder values (changeme, your-api-key-here, replace-me-please), RFC 2606 example-domain emails and noreply conventions, fictional 555 phone numbers, npm/shield checksum prefixes, and non-Verhoeff 12-digit runs no longer report as Aadhaar
--baseline naming a missing file is an error instead of silently scanning with an empty baseline
Baseline files with an unknown format version are rejected instead of silently accepted
--update-baseline refreshes the recorded line numbers of entries it already knows, and saved baselines end with a newline
SARIF report property order is deterministic
Piping output to a closed reader no longer panics, including hook install and init; hook commands now report real output failures instead of discarding them
Trusted scans (--no-config-discovery) no longer read detector configuration from environment-derived locations ($XDG_CONFIG_HOME, $HOME, the executable directory), so a redirected home directory cannot replace the built-in detector set
Staged blobs are resolved to object IDs and read with git cat-file blob <oid> instead of :<path>, so a file whose name resembles a git stage path (0:config) can no longer substitute another file's content
scan --git-history reads merge commits (--diff-merges=first-parent), so a secret introduced while resolving a conflict is reported
Files that fail to open or read are counted as unscannable instead of being silently skipped
Ten detectors that matched the wrong shape or nothing at all (SupabaseServiceRoleKey, TerraformCloudToken, AzureStorageKey, DockerHubToken, CircleCIToken, DiscordToken, NetlifyToken, CodecovToken, AdyenAPIKey, RazorpayKey) now follow the documented token format, each pinned by a real-format fixture test; the two that matched only non-secrets were removed
Email, PhoneNumber, IPAddress, TwilioAPIKey and MailgunAPIKey no longer suppress or match unintended text: the example-domain allowlist is anchored, only the reserved 555-01xx numbers are ignored, every IPv4 octet is validated, and embedded vendor prefixes require word boundaries
Entropy and validators run on the captured value rather than the whole match, so api_key = "aaaaaaaaaa" is no longer reported; GCPServiceAccountKey requires a private_key field and MasterAPIKey, AzureDevOpsPAT, KimiMoonshotAPIKey and CertificateDetector severities now reflect credential impact
Performance
Keyword matching uses a single Aho-Corasick pass per line: ~3x faster file scans, ~9x faster streams
File scans stream line by line instead of reading whole files into memory
~2.5x faster file scans: one combined prefilter pass for the keywordless detectors, an ASCII fast path for line lowering, and a byte-histogram entropy check that no longer allocates per match