How detection works

July 31, 2026 · View on GitHub

A KeyHog scan is a pipeline. Files come in one side, findings go out the other. In between, four stages:

files → [chunker] → [prefilter] → [detector match] → [post-process] → findings

Most chunks that fail the cheap prefilter stop there, which keeps full regex evaluation focused on plausible inputs. This is not an unconditional hard drop: a rejected chunk that looks encoded can enter a bounded decode-only recovery pass (recursively decoding up to a max_decode_depth, defaulting to 10), so an encoded secret is not lost merely because its plaintext anchor is absent from the original bytes.

Detection mechanisms

KeyHog does not use one universal test for "secret-like." It composes several mechanisms, and their roles are deliberately different:

MechanismRoleCan create a candidate?
Service-anchored detector regexMatches a vendor or credential-specific shape from detector TOMLYes
Companion patternsFinds related fields near a primary match; required = true gates acceptance, while optional companions enrich confidence or verificationConfirms an existing candidate
Structured and multiline extractionReassembles assignments and strings that syntax splits across lines or nodesYes
Decode-through transformsScans supported encoded representations while preserving source attribution. Reverse and Caesar admission uses the active detector TOMLs' decode_transforms prefixesYes
Bounded static program recoveryEvaluates recognized side-effect-free JavaScript XOR, explicit-key AES-256-CBC, and CryptoJS/OpenSSL passphrase expressions when every operand is embedded and immutableYes
Generic assignment bridgeExtracts values beside credential-role keys when no vendor shape existsYes
Shannon entropyMeasures byte-distribution uncertainty for opaque generic valuesYes, on the entropy-discovery path
BPE token efficiencyRejects language-like values that compress into common subword tokens; eligible candidates use it by default, and detector TOML can tune or disable itNo; precision gate
English bigram discriminatorDistinguishes random alphabetic tokens from pronounceable identifiers, dictionary placeholders, and low-diversity masks inside specific shape and context gatesNo; admits or rejects an extracted candidate within those gates
Shape, placeholder, path, and context policyRejects examples, references, prose, identifiers, and context-specific noise; entropy owners compile their isolated-token floors and lengths from their detector TOML plausibility tableNo; precision gates
Checksums and structural validatorsProves or rejects formats that carry intrinsic validity bits or grammarAdjusts acceptance/confidence
On-device MoE scoringScores ambiguous candidates using local features; never sends content awayAdjusts confidence
Live verificationOptionally asks the owning service whether a surviving credential is activeAdds a verdict after detection

Regex, generic extraction, entropy, and decode-through therefore find different candidate classes. Named regexes and generic assignment extraction create candidates; companions, validators, BPE, English bigram evidence, shape/context policy, and confidence then confirm, reject, or score them. Verification runs only after a candidate survives detection and reporting policy.

Structured extraction accepts balanced Helm actions as render-time syntax. It replaces each action with an inert YAML value while retaining every literal source byte. A Jupyter notebook truncated at end of file is repaired only by closing its open string and container delimiters. Any other syntax error remains a counted coverage gap.

ML participation is detector-owned through [detector.ml]. lift can raise a structural score but cannot veto a match, blend combines model and structural evidence, and authoritative lets the model decide an otherwise ambiguous channel. In the shipped corpus every regex-pattern channel is currently lift; 919 of 923 entropy channels disable ML, and only the four generic entropy owners use authoritative. The model is therefore not a general regex false-positive veto. Its current feature record identifies detector and pattern-versus-entropy channel, but not the exact matched pattern. Pattern-local weak-anchor policy remains outside model conditioning until that provenance is carried end to end. The retrainer refuses mismatched detector/channel records and now requires positive and negative held-out support for every blend or authoritative channel before writing another model.

Before model inference, a cheap probabilistic gate may assign 0.1 confidence to an unaccompanied generic candidate that lacks secret-like randomness. It cannot short-circuit a service-regex match or a candidate backed by a matched companion, including a weakly anchored service pattern: those candidates continue into their detector-owned ML mode. This distinction prevents a corpus-wide randomness shortcut from overriding stronger detector-local evidence.

Static program recovery is a decode mechanism, not arbitrary code execution. KeyHog does not invoke Node.js or evaluate source. It recognizes a bounded grammar for cyclic byte-array XOR and Node-style AES-256-CBC decryption, resolves only literal numeric arrays, Base64-encoded JSON arrays, buffer literals, and empty-separator string joins, then checks binding consistency, UTF-8, AES block shape, and PKCS#7 padding before rescanning the recovered plaintext. Recovered XOR calls and Node AES ciphertext bindings are spliced back into bounded parent context, preserving assignment evidence and absolute source offsets. The CryptoJS dialect additionally requires an exact immutable require("crypto-js") alias, decrypt wrapper, literal passphrase and ciphertext bindings, an OpenSSL Salted__ envelope, and EVP_BytesToKey MD5 derivation. Dynamic values or unsupported syntax produce no derived candidate. The original source still follows the normal detector pipeline. The mechanism is disabled with decode recursion, including under --fast.

BPE is not a replacement name for entropy: it is an independent post-candidate signal. BetterLeaks calls the approach Token Efficiency; KeyHog uses the same broad BPE idea while keeping its own detector schema, thresholds, pipeline, and behavioral evidence.

Terminology matters here: BetterLeaks' public documentation names the feature Token Efficiency and describes BPE tokenization as a natural-language false positive filter; it does not present “BPD” as a separate score. KeyHog names its related mechanism BPE token efficiency, uses cl100k_base, measures UTF-8 bytes per token, and resolves the ceiling per detector. If “BPD” is being used informally to mean a bits/byte or bytes/token density, do not treat it as a third byte-density score. KeyHog also uses a separate English letter-bigram discriminator, but it does not measure bits per byte or bytes per token.

The English bigram discriminator evaluates lowercase ASCII alphabetic runs against an embedded 26 by 26 log-probability model. Digits and symbols end a run. Fewer than six alphabetic characters produce no randomness verdict. A random-token verdict requires a mean score at or below -6.85 and at least three distinct letters. Depending on the calling gate, that evidence can keep an otherwise identifier-shaped random credential or reject a confidently English placeholder. The model does not model arbitrary bytes, numeric keys, hexadecimal or Base64 alphabets as such, or short values. Its current thresholds are scanner-wide constants, not detector TOML fields.

Detector-owned tuning: what each setting changes

Detection policy belongs in the detector TOML whenever the choice is specific to a credential type. Scan-wide CLI/TOML values are operational overrides for controlled comparisons or a corpus-wide policy; they are not a second hidden detector definition. keyhog explain <detector-id> shows the policy declared by that detector TOML and its provenance; keyhog config --effective shows the resolved scan-wide policy.

Practical ownership rule: any numeric value that changes one secret family's recall, precision, shape admission, or scoring must be a named detector-TOML field. If the schema cannot express it, extend the typed schema and its explain/contract surfaces rather than adding a detector-specific literal in scanner code. Only true shared invariants, such as parser safety caps or a model's fixed vocabulary, remain global.

Global structural non-secrets are typed Tier-B data in rules/entropy-universal-rejections.toml. Plain prefixes and explicit prefix-plus-length rules, including the Ag Sealed Secrets ciphertext boundary, apply uniformly before detector plausibility. They are corpus-global structural invariants, not a second source of per-detector tuning.

Detector TOML fieldIf increased / enabledIf decreased / disabled
entropy_lowRequires more Shannon entropy for keyword-anchored generic values; fewer low-randomness passwords/tokens surviveAdmits more values when the assignment key supplies evidence; shape, BPE, context, and confidence gates still apply
entropy_highTightens keyword-independent generic admission and raises the partial-confidence tier for entropy fallbacksAdmits more opaque candidates and grants the partial entropy score at a lower value
entropy_very_highTightens isolated, anchor-free admission and raises the full-confidence tier for entropy fallbacksExpands the no-keyword search and grants the full entropy score at a lower value
sensitive_path_entropy_very_highRaises the keyword-free bar even in sensitive filesLowers the explicit sensitive-path bar for that detector, improving recall in .env/secret manifests
plausibility.keyword_free_operator_marginRaises the detector-owned margin composed with the Tier-A entropy thresholdLowers that explicit margin for the keyword-free role owner; no other detector may declare it
[detector.entropy_fallback]Changes the emitted synthetic entropy finding identity and semantic class for that detectorOmitting it for an active entropy owner fails compilation; there is no scanner-global compatibility identity
entropy_rolesClaims one or more corpus entry paths: keyword-free, isolated-bare, or unclaimed-keywordOmitting a role disables that path in a focused custom corpus; no built-in owner or threshold is substituted
decode_transforms.reverse_prefixesAdmits character-reversed candidates that can recover one of the declared plaintext prefixesOmitting a prefix prevents reverse recovery for that prefix in a focused custom corpus
decode_transforms.caesar_prefixesAdmits only ROT-N shifts that can recover one of the declared plaintext prefixesOmitting a prefix prevents Caesar recovery for that prefix in a focused custom corpus
[[detector.entropy_shapes]]charset, optional grouping, diversity requirements, and a lower shape floor admit explicitly structured isolated credentialsStricter structural requirements or a higher floor narrow the exception; omission is invalid for an active entropy owner
entropy_floorA higher applicable length-bucket floor suppresses more low-entropy candidates for that detectorA lower floor preserves more human-chosen or structured credentials
plausibility.mixed_alnum_floorRejects more identifier-like alphanumeric runsPreserves more low-randomness mixed-alphanumeric values
plausibility.symbolic_entropy_floorRaises the minimum entropy for symbol-bearing credential assignments, including the bare auth= bridgePreserves more anchored symbolic passwords through the same compiled detector policy
plausibility.second_half_entropy_floorRejects candidates with a less-random tailPreserves more credentials whose entropy is front-loaded
plausibility.second_half_min_lenApplies the tail-entropy check to shorter valuesRestricts the tail check to longer values
plausibility.unique_chars_min_lenApplies distinct-character requirements to shorter valuesRestricts the diversity check to longer values
plausibility.min_unique_charsRequires more distinct characters once the diversity check appliesPreserves lower-diversity credentials
plausibility.unanchored_hex_max_lenAllows longer unanchored all-hex values before treating them as non-secret key materialRejects shorter unanchored all-hex values
plausibility.identical_char_max_lenAllows longer single-character repetitionsRejects shorter single-character repetitions
plausibility.structured_dotted_min_lenRequires a longer isolated structured dotted tokenAdmits shorter structured dotted tokens after the other gates pass
plausibility.mixed_alnum_min_lenRequires a longer mixed alpha-numeric credential before the carve-out appliesLets shorter anchored mixed tokens use the detector's mixed floor
plausibility.isolated_mixed_entropy_floorRaises the floor for isolated contiguous or underscore-delimited mixed tokensPreserves more low-randomness isolated mixed tokens
plausibility.isolated_symbolic_min_lenRequires a longer isolated symbol-rich credential for the short-candidate exceptionAdmits shorter symbol-rich candidates; exact declared lower-dash layouts still use their own shape rules
plausibility.isolated_symbolic_min_symbolsRequires more symbol bytes in the isolated symbolic exceptionAdmits candidates with fewer symbol bytes after the other gates pass
plausibility.isolated_symbolic_requires_non_underscorePrevents underscore-only mixed tokens from bypassing their mixed entropy floor through the symbolic exceptionAllows underscore to satisfy the symbolic exception by itself
plausibility.isolated_colon_left_min_len / isolated_colon_right_min_lenRequires longer sides around an isolated opaque:opaque separatorAdmits shorter colon-separated opaque pairs
plausibility.leading_slash_base64_entropy_floorRaises the floor for unanchored slash-led base64Preserves more slash-led base64 candidates
plausibility.leading_slash_base64_min_lenRequires a longer unanchored slash-led base64 candidateAdmits shorter candidates after alphabet, padding, entropy, and shape checks pass
plausibility.reject_repeated_blocksRejects periodic mask values, including truncated repetitions in the bare auth= bridgeAllows that shape to continue through the remaining detector gates
plausibility.allow_alphabetic_credentialAdmits anchored all-letter passwords/tokens after other gatesRequires alphabetic-only values to clear the ordinary entropy path
plausibility.reject_program_identifiersRejects pure source-language identifier shapesAllows pure identifier-shaped values through the remaining gates
plausibility.reject_source_symbol_identifiersRejects digit-bearing mixed alphanumeric source-symbol shapes independently of the pure-identifier gateAllows those mixed values to follow the detector's mixed_alnum_floor and mixed_alnum_min_len policy
plausibility.reject_dash_segmented_alnumRejects serial/product-key-like dash groupsAllows dash-segmented alphanumeric values through the remaining gates
entropy_policy_priorityWins more overlapping generic keyword-policy claimsYields shared keywords to a more specific detector; unique keywords are unchanged
bpe_max_bytes_per_tokenA higher ceiling is looser: fewer compressible/word-like candidates are rejectedA lower ceiling is stricter: more language-like values are rejected, with corresponding recall risk
bpe_enabled = falseNot applicableSkips token-efficiency rejection for detectors such as human-chosen passwords
decoded_hex_key_material_lengthsAdds only the declared pure-hex widths after transport decodingOmitted widths remain decoded-digest negatives
canonical_hex_key_materialGeneric detectors admit declared lengths only under exact keywords or vendor-prefixed suffixes; regex detectors use length-only entries because their matched pattern is the anchorOmitted policy, scope, or length remains a digest-shaped negative; there is no service-wide width fallback
min_len / keyword_free_min_lenLonger values are required; short false positives fall, but short real credentials can also fallShorter credential shapes become eligible
max_len (entropy-policy owner)Longer values remain eligible across generic assignment, entropy fallback, and explicit regex envelopes; increase only when the credential contract permits themOverlength values are rejected whole with value_too_long before entropy or BPE
allowlist_paths, allowlist_values, stopwordsAdds detector-specific path, value-regex, or literal exclusionsRemoving an exclusion makes that detector consider the matching path/value again; it does not affect other detectors
pattern required_literalsRoutes a prefixless regex only after at least one AST-proven necessary ASCII literal is present. The declaration is the sole owner of non-prefix literal routingOmitting it leaves the regex in its prefix, keyword-gated, or always-active route; an unsound declaration rejects the detector
public_identifier_assignment_markersClassifies detector-local assignment-key fragments as public identifiers instead of credentialsOmission disables this suppression for that detector; there is no scanner-global blockchain/network marker list
min_confidenceRaises this detector's reporting floorLowers this detector's reporting floor; an operator override can still replace it
detector/pattern weak_anchorKeeps generic shape/entropy gates active for a whole detector or an individual pattern; requires the owning detector's entropy_high and entropy_floorTrusts unmarked patterns; use only when those patterns prove the credential shape
structural_password_slotApplies password-slot placeholder policy to a free-form value captured from a syntactic credential slotLeaves that detector outside the structural-password family
private_key_blockMakes the detector's enclosing key block suppress less-specific findings nested inside itTreats the match as an ordinary, non-enclosing finding
[detector.credential_shape]Declares exact prefix/length/shape constraints that a captured credential must satisfyOmitting it leaves that detector without an additional credential-shape constraint

Resolution rules

These settings do not all use one generic “last value wins” rule:

  • Generic keyword ownership: the highest entropy_policy_priority among detectors claiming the normalized assignment keyword owns entropy and BPE policy. Equal priorities use stable detector identity, independent of corpus order. Custom detector policy keywords join entropy discovery directly; they do not need to be repeated in [scan].secret_keywords.
  • Final match resolution: the active compiled plan classifies named, phase-2 generic, entropy, and enclosing private-key findings. The reporting service string and detector-ID length do not change specificity. Unknown finding identities fail checked resolution instead of inheriting embedded or service-name behavior.
  • Entropy entry roles: entropy_roles selects the detector that owns each corpus-level entry path. A compiled corpus may have at most one owner for each role. Missing roles remain disabled, and duplicate owners fail scanner construction. Role selection never depends on a detector ID spelling.
  • Weak anchors: detector-level weak_anchor = true applies to every pattern, while the same field inside [[detector.patterns]] governs that exact regex. Each such detector owns entropy_high and length-bucketed entropy_floor. Scanner construction rejects an explicit weak anchor without that local policy. KeyHog never guesses this semantic choice from regex text, and min_confidence does not disable it. The compiled hot path uses a primitive detector-indexed floor program.
  • BPE ceiling: every active entropy owner declares either bpe_max_bytes_per_token or bpe_enabled = false; omission fails detector validation and scanner construction. An explicitly supplied [scan].entropy_bpe_max_bytes_per_token or --entropy-bpe-max-bytes-per-token replaces every BPE-enabled entropy/generic detector ceiling; the CLI wins over the config file. bpe_enabled = false still disables the gate for that detector.
  • Confidence floor: the scan floor defaults to 0.40. A detector TOML min_confidence replaces the scan floor for that detector, and an operator [detector.<id>].min_confidence replaces the detector-declared floor. Under --precision, the resolved global and per-detector floors are clamped to at least 0.85; neither source can weaken the precision preset.
  • Entropy policy: every active entropy owner must declare its high, low, very-high, sensitive-path, mixed-alphanumeric, symbolic, tail-entropy, length, isolated-shape, and BPE policy. Scanner construction compiles these into concrete detector-indexed values; a missing field is an error, not a runtime default. Schema defaults remain only for non-owning programmatic detector values that never supply entropy policy. The scan-wide entropy_threshold is deliberately not a blanket replacement for all four bands. On the phase-2 generic bridge it tightens only when it exceeds the owning detector's high band. On the entropy scanner, a value above that high band tightens keyword and isolated candidates; a value below the keyword detector's low band loosens that keyword path, while values between the low and high bands leave its low floor in place. The isolated path keeps its mixed-alphanumeric floor unless the scan threshold exceeds the high band. Named-detector heuristic confidence uses the resolved scan threshold as its partial entropy tier and the scoring margin above it as its full tier; changing the setting can therefore change a named finding's confidence without changing whether its regex matched. These rules preserve the different evidence carried by an assignment key, an isolated opaque token, and an unanchored generic value. The owning entropy_high and entropy_very_high values also define the partial and full heuristic-confidence tiers for emitted entropy fallbacks. Detector ML policy then composes with that heuristic; an authoritative ML mode may replace it, while disabled, lift, and blend modes retain the documented heuristic semantics.
  • Sensitive paths: sensitive_path_entropy_very_high is a required detector-local threshold for active entropy owners. Equaling entropy_very_high means no sensitive-path relaxation; a lower declared value is an explicit detector-owned recall choice.
  • Credential plausibility: the required detector plausibility block owns its entropy floors, length carve-out, alphabetic admission, and repeated, identifier, and dash-segment rejection choices. There is no production-path fallback for an active entropy owner.
  • Synthetic entropy identity: every active entropy owner declares [detector.entropy_fallback] with a semantic class (generic, password, token, or api-key), an entropy-* id, display name, and service. The compiled scanner uses the complete metadata from the active detector corpus for entropy-only findings. Omitting the block is a visible compile error; no scanner-global keyword classifier or compatibility identity can relabel a custom candidate.
  • Isolated entropy shapes: generic entropy owners declare one data-driven shape with its character set, entropy floor, optional fixed-width grouping, diversity requirements, and special minimum length. For the shipped lower-alphanumeric app-password policy, candidate length is derived from four groups of four plus three separators; special_min_length controls the short-candidate revisit and must not exceed that derived length. The shape is used for anchorless synthetic entropy recovery; the anchored bluesky-app-password regex remains the source of the named Bluesky finding. A custom corpus without the shape has no isolated exception, rather than inheriting an embedded detector policy.
  • Isolated symbolic credentials: the detector's plausibility.isolated_symbolic_min_len, plausibility.isolated_symbolic_min_symbols, and plausibility.isolated_symbolic_requires_non_underscore fields control the shorter symbol-rich exception. Contiguous and underscore-delimited mixed tokens stay under plausibility.isolated_mixed_entropy_floor when the owner requires a non-underscore symbol, and an exact declared lower-dash layout must satisfy its entropy_shapes policy instead of bypassing it as symbolic.

Token efficiency can carry more of the precision burden for a detector whose assignment key or regex already creates the candidate. That is the practical per-detector alternative to making Shannon entropy the decisive signal: use a permissive detector-owned entropy floor appropriate to the credential family, then let its BPE, shape, context, and confidence policy reject word-like noise. It is not equivalent to blindly replacing entropy with one global BPE number, and bpe_enabled alone never creates a candidate. Both configured gates still execute; the current pipeline has no entropy-or-BPE branch.

Detector-owned canonical_hex_key_material is the deliberate exception to the BPE and generic low-diversity/decode-as-data gates. Hexadecimal key bytes tokenize efficiently and use a small alphabet for the same mechanical reasons hexadecimal digests do, so the exact detector-owned contract supplies the discriminator: assignment scope plus length for generic detectors, or matched regex plus length for named detectors. Placeholder, degenerate-repeat, entropy, context, and reporting gates remain active. When ML is enabled, this exact TOML match is structural positive evidence and therefore preserves the detector heuristic floor; the model may raise its score but cannot erase a policy-proven key as if it were an unowned entropy candidate.

Scan-wide settings remain operational controls, but they do not all compose the same way. The operator-layer order and working TOML/CLI examples are in Configuration. Stable per-detector tuning belongs in the owning detector TOML and should be proved with that detector's positive, negative, evasion, backend-parity, and corpus contracts.

Settings, active corpus, and exact identity

KeyHog keeps detector content, resolved scan policy, and corpus provenance separate:

  • The reported detector corpus digest binds the normalized corpus schema and the active detector specifications after composition and [detector.<id>] enabled = false removal. A matching disable therefore changes the digest. An unknown disabled ID warns and leaves this digest unchanged.
  • The autoroute rules identity also describes the active detector specifications. Operator confidence-floor overrides are composed later so different scan presets can coexist in one calibration cache.
  • The autoroute configuration identity binds the resolved scanner and operator policy. It includes the selected fast, deep, or precision preset, scan-wide and per-detector floors, the configured disabled-ID set, detector tuning inputs, worker and pipeline settings, backend/GPU policy, and profiling instrumentation.
  • The corpus path and embedded, replace, or overlay label are provenance. They are reported in versioned output, but the path spelling is not detector content. Copying the same normalized corpus to another directory does not create a different content digest. A mode change changes the digest only when it changes the resulting active specifications.

The preset definitions and their override rules are in Configuration. --profile is performance instrumentation, not a named scan-policy profile.

Hardware changes execution, not detection policy. CPU, SIMD/Hyperscan, and GPU routes consume the same resolved detector and configuration identities. Autoroute accepts a candidate only when its canonical detection identities match the reference: chunk membership, detector id/name/service/severity, exact credential, stored hash, companion identity, source, file, line, byte offset, commit, author, date, entropy, confidence, and multiplicity. Mismatch diagnostics name only the differing fields and occurrence counts. They never expose raw values or deterministic value fingerprints.

Built-in suppression, confidence, decode, and scanner postprocessing are already part of those backend results. CLI allowlists and rules, policy floors, cross-source deduplication, verification, and output formatting run after selection. Missing or stale exact evidence is an error. Calibration never relaxes a detector to make a backend look faster.

ChangeFinding-set effectRouting/calibration effect
Copy the same normalized corpus to another pathNoneContent identity is unchanged; reported source provenance changes
Change detector TOML, corpus schema, replacement/overlay membership, or a matching enabled = false overrideCandidates, suppressions, confidence, or final findings may changeActive corpus/rules identity changes; recalibration is required
Change a preset, scan-wide policy, per-detector floor, configured disabled-ID set, workers, or GPU/runtime policyResults or scan cost may change according to the settingConfiguration identity changes; calibration for the old identity is not reused
Change CPU, GPU, driver, or accelerator availabilityNone for the same resolved detector/configuration and input; a parity mismatch rejects that routeHost/device/runtime identity changes; old host evidence is not reusable
Use `--backend cpusimdgpu-cuda
Change input size, chunk count, source execution class, decoder-kind mask, decode candidate count or byte bucket, decoder uncertainty, or full-source-size availabilityThe input can change findings; backend choice must notA different exact workload key is selected
Switch between a one-shot process and a ready daemon/watch runtimeRuntime lifetime must not change detector policy or canonical matchesCold-aware and warm persistent-runtime routes may have different winners

Strict Backend Parity

KeyHog exposes three search-backend classes: pure Rust CPU, SIMD/Hyperscan (simd-regex), and GPU/VYRE region presence. Autoroute measures five concrete runtime peers when eligible: scalar CPU, Hyperscan CPU, CUDA, native Metal, and WGPU. Portable builds retain the pure-Rust trigger path without Hyperscan. keyhog calibrate-autoroute rejects any peer whose canonical match identity differs from the reference. It records the first real GPU dispatch plus warm trials: an ordinary process resolves against the cold-aware GPU cost, while a daemon that initialized its engines before readiness resolves against the warm GPU evidence. A missing or invalid decision is not autoroute evidence: KeyHog warns, uses the scalar correctness oracle to complete the scan, and records the exact recovered byte coverage and repair command.

When comparing settings, record the effective config, detector digest, input identity, backend, host/accelerator identity, and complete findings, not only elapsed time or finding count. A faster run with a different result set is a detection change or parity failure, not a routing win.

Stage 1 - chunker

A file becomes one or more chunks. A chunk is {data: str, metadata: {source_type, path, line_offsets, …}}. The chunker:

  • Skips obvious binaries via magic-byte sniffing (PDF, PNG, zip, …).
  • Skips files matching is_default_excluded_path (node_modules, .min.js, build/, etc.).
  • Splits files larger than the 1 MiB window size into overlapping ~1 MiB windows so a single giant log file doesn't blow scratch memory. Each window carries its absolute base byte offset and base line so findings report the real file offset/line, not the per-window one. Cross-window secrets are reassembled in stage 4.
  • Decodes UTF-16 BOM files into UTF-8 (PowerShell / .NET configs).

Specialized chunkers run too:

  • Git history → one chunk per (commit × file × diff line)
  • Docker images → one chunk per layer × file
  • Web URLs → one chunk per response body / sourcemap / WASM strings
  • S3 buckets → one chunk per object body
  • GCS buckets → one chunk per object body
  • Azure Blob containers → one chunk per blob body

Stage 2 - prefilter (the cheap pass)

Three gates, in order, each cheaper than the next:

  1. Alphabet screen. A 256-bit mask of which bytes the corpus's detectors care about. A chunk with no relevant byte becomes a prefilter miss.

  2. Bigram bloom. A 4096-bit bloom filter of 2-byte sequences from detector keyword prefixes. A chunk with no overlapping bigram becomes a prefilter miss. This cheaply recognizes source that carries no relevant anchor vocabulary.

After these screens, ordinary misses stop. Decode-shaped misses instead take the bounded decode-only path described above; transformed plaintext is then attributed back to the original source.

  1. Backend trigger pass. The simd-regex backend compiles the detector corpus into Hyperscan databases when the simd feature is present; cpu-fallback uses the pure-Rust trigger path. One pass returns which detector IDs have a candidate match.

    GPU-capable builds add VYRE's resident fused literal-evidence backend. Its single dispatch returns region presence plus detector-derived localization positions; the shared host regexes still decide every finding. There is no universal model-name or byte threshold at which KeyHog silently switches to it. --backend auto requires an exact persisted calibration decision for the current binary, detector/config digest, host/device/driver, workload class, and size bucket. Calibration keeps a GPU route only when its canonical match identities equal the reference and it is the fastest eligible backend for that key.

Stage 3 - detector match

For each pattern-backed detector that the prefilter flagged, the full regex evaluates. The regex is detector.patterns[].regex in that detector's TOML, and its configured capture group becomes the candidate credential. Generic phase-2 detector TOMLs use keyword, length, entropy, token-efficiency, and shape policy for shapeless assignments or isolated opaque values. They may also carry explicit patterns for strongly structured envelopes such as JSON "secret", "token", or "apiKey" fields; both mechanisms remain owned by the same detector TOML instead of a central compatibility detector.

A detector's .toml carries:

  • id, name, service, severity, keywords
  • zero or more patterns, each with regex + group + optional description (required for service-anchored detectors; optional structured-envelope anchors for phase2-generic)
  • optional companions; only entries with required = true gate acceptance
  • optional verify block - HTTP method, URL template, auth scheme, success status

Detectors fall into two camps:

  • Service-anchored. Regex requires a service-specific keyword (AWS_SECRET_ACCESS_KEY=, stripe.com/v1/, dn_ Deepnote prefix). These have HIGH precision: the keyword itself is positive evidence, not just a hint.

  • Generic / entropy discovery (generic-password, entropy-api-key, entropy-token). Triggered by entropy + assignment shape only - password = "...", secret: "...", JSON { "token": "..." }. Lower precision; suppression filters do most of the work.

    Surviving candidates also pass a BPE token-efficiency gate. Shannon entropy asks how evenly bytes are distributed; token efficiency asks how readily a fixed subword vocabulary compresses the value. Dotted API names and prose can have high Shannon entropy but tokenize into a few common pieces, while opaque secrets usually require many short tokens. The mechanisms are complementary, and generic detector TOMLs may own their token-efficiency ceiling through bpe_max_bytes_per_token. Opaque API-key/secret policies use their detector-owned ceiling, falling back to the scan-wide default of 2.2 UTF-8 bytes per token when they do not declare one; password/passphrase policies set bpe_enabled = false because human-chosen credentials may intentionally be word-like. Disabled policies skip tokenizer work entirely rather than using a magic oversized ceiling.

The entropy-generic, entropy-password, entropy-token, and entropy-api-key IDs are output classifications for entropy-discovered findings, not four additional detector TOML files. Their candidate policy is owned by the corresponding phase-2 TOMLs selected from the assignment context: generic-secret, generic-password, generic-keyword-secret, or generic-api-key. Use keyhog explain on those owning detector IDs when tuning entropy, BPE, length, or canonical-key policy.

The split matters for the post-process stage.

Stage 4 - post-process

Even a regex match isn't always a credential. Stage 4 filters:

  • Known example fixtures (Stripe docs key, AWS docs key, RFC 7519 specimen JWT).
  • Placeholder language - credentials containing YOUR_, INSERT, EXAMPLE, PLACEHOLDER, TODO, FIXME, etc.
  • Shape gates.
    • Universal: punctuation_decorated_identifier - credentials starting with --, &, @, !, /, $ (CLI flags, pointers, SQL vars, shell vars, GraphQL refs) or ending in : / ! (UI labels, TypeScript non-null assertions).
    • Generic / entropy only: pure_identifier, word_separated_identifier, scheme_prefixed_uri, url_or_path_segment, contains_uuid_v4_substring. These shapes can be real credentials when paired with a service or protocol anchor, so named detector TOMLs and structural authorization detectors own those cases. A generic token=<uuid> remains an identifier; an Authorization: Bearer <uuid> value is a credential because the Bearer envelope supplies the missing evidence. Public salts and nonces are not generic secrets. A detector for a product whose field is genuinely secret despite that name must own the product syntax explicitly.
  • Path-based suppressions - vendored bundles (node_modules/, wp-includes/, bower_components/), CI workflow files (where ${{ secrets.NAME }} references are syntactic, not credentials), i18n translation files, secret-scanner source files (the file IS a scanner; its regex literals shouldn't fire on itself).
  • Cross-chunk reassembly. A secret split across window boundaries gets reassembled from the tail of chunk N + the head of chunk N+1.

A finding that survives stage 4 makes it to output.

Where the speed comes from

The alphabet screen and bigram bloom reject irrelevant chunks before regex confirmation. Literal triggers narrow the active detector set, and the scanner shares confirmation, suppression, and reporting tails across CPU and GPU backends. Windowing bounds scratch space for large inputs; caches avoid repeated compiler and index work.

End-to-end throughput depends on the detector/config digest, source shape, candidate density, decoding and verification policy, cache state, CPU, GPU, driver, and storage. Use keyhog calibrate-autoroute for routing evidence on the installed host and the repository benchmark harness for reproducible cross-version measurements; do not treat a throughput number from another machine or detector corpus as a routing threshold.

Where the precision comes from

FilterWhat it catches
Known example fixturesStripe docs key, AWS docs key, RFC 7519 JWT
pure_identifiergetParameter, Benutzername, auth_decoders
word_separated_identifiers3_secret_access_key (function name)
scheme_prefixed_uriurn:foo:bar (URI literal, not creds)
url_or_path_segment/api/v1/users/123 (REST path)
contains_uuid_v4_substringTOKEN_LIST=636765a9-… (UUID identifier)
punctuation_decorated_identifier--api-secret, &password, Password:
Vendored-minified-pathnode_modules/jquery-3.6.0.min.js
CI workflow path.github/workflows/ci.yml - ${{ secrets.X }}
i18n translation pathlocale/de.po - translated password word

Each filter has a known-FP-cluster it was built to defuse. The Suppressions page enumerates them with examples.

What this looks like for one finding

file.env contains: AWS_SECRET_ACCESS_KEY=ev0BsFtSD7S/4VWYObxiEhME3hJBXeYzR43jgiB1

stage 1 - chunker:        emit chunk{ path: "file.env", data: "AWS_SECRET..." }
stage 2 - alphabet:       PASS (chunk has `=`, alphanumerics from the corpus)
stage 2 - bigram bloom:   PASS (`AW`, `WS`, `_S` are in the bloom)
stage 2 - simd-regex:     MATCH → triggers `aws-secret-access-key` + `generic-password`
stage 3 - regex eval:
  `aws-secret-access-key` detector pattern captures the 40-byte value
    captures `ev0BsFtSD7S/4VWYObxiEhME3hJBXeYzR43jgiB1`
  `generic-password` regex doesn't match (no `_password`/`_pwd` substring)
stage 4 - post-process:
  known-example check: no
  `looks_like_pure_identifier`: false (has digits + /)
  `looks_like_punctuation_decorated_identifier`: false
  → EMIT

That's one finding's life. Multiply by 10⁶ files and the throughput math is why each stage matters.