forbidden-strings 0.2.0

Out-of-band scanner for forbidden literal strings and regex patterns. Gitignore-aware, fast, dependency-light: built for CI deny-listing of leaked credentials and banned tokens.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
# forbidden-strings

Linear-time deny-list scanner for Git repos,
 built on the in-house `forbidden-regex` engine
(`package/rust-module/forbidden-regex`).
 A native Rust binary with a sub-commit-budget
startup,
 it scans working-tree files line by line against a deny list of literals and
restricted-dialect regexes and reports each match as an opaque,
 redacted finding.

Rules split into a baseline embedded in the binary
(`data/builtin-rules.txt`,
 activated by `--builtin-rules`),
 a committed shared appendix
(`forbidden-strings.append.txt`),
 a per-repo gitignored appendix
(`forbidden-strings.append.local.txt`),
 or a CI-only secret (`FORBIDDEN_STRINGS_LIST`).
The matched substring,
 the surrounding line,
 and the rule pattern are never printed in
failure output,
 so a rule body that would itself leak if committed (a customer name,
 an
unreleased project codename,
 a pre-disclosure partner ID) can live as an appendix or CI
secret without exposure on public CI logs.

## What's different

- **Native binary startup.
  ** Rust with `lto = true`,
   `codegen-units = 1`,
   `opt-level = 3`,
  `panic = "unwind"`,
   `overflow-checks = true`,
   `strip = true`.
   No Node startup,
   no WASM
  init,
   no per-invocation config parse,
   which is what a sub-100 ms pre-commit budget needs.
- **Linear-time matching.
  ** The engine is derivative and product based with no
  backtracking,
   so no rule combination can exhibit catastrophic-backtracking behaviour.
  A set-level SIMD prefilter lets clean lines skip per-rule work.
- **Set-algebra rules.
  ** Intersection `A & B` and complement `~(A)` are first-class in
  the dialect,
   so "match X but not Y" needs no lookaround.
   PCRE-family engines
  (gitleaks,
   trufflehog,
   secretlint,
   plain RE2) cannot do this;
   their workaround is
  per-rule allowlists,
   which scale badly.
- **Sensitive rules can live out-of-band.
  ** The committed baseline holds non-sensitive
  rules;
   the gitignored appendix and the CI-only `FORBIDDEN_STRINGS_LIST` secret hold
  sensitive rules.
   Failure output never prints the matched substring,
   the surrounding
  line,
   or the rule pattern,
   so a rule body itself can be a secret.

## When to pick something else

`forbidden-strings` deliberately omits features other scanners ship as core capabilities:

- **CEL-based post-match filtering** (entropy thresholds,
   BPE token efficiency,
   git-author
  predicates,
   file-path globs,
   string allowlists).
   No equivalent here.
- **Async HTTP validation**.
   No way to call a provider API to confirm a detected secret
  is live;
   staleness review is on you.
- **Git history scanning**.
   The walker enumerates working-tree files only.
   No equivalent
  of `gitleaks git` that scans every diff in every commit.
- **SARIF / JSON / CSV output**.
   Hits go to stderr as plain text;
   no machine-readable
  format for GitHub code-scanning upload or CI dashboards.
- **Per-rule path scoping**.
   Every rule runs against every non-skipped file;
   the scanner
  cannot apply rule X only to YAML files.
- **Per-rule allowlists**.
   No way to say "rule X but skip when it matches in path Y".
- **No streaming or stdin input.
  ** Files only;
   the walker enumerates from disk.

If you need any of those,
 betterleaks or gitleaks is the right tool.

## Prerequisites

- **Rust toolchain**.
   Install via mise:
   `mise install rust`.
- **mise** itself,
   since build commands are `mise run` tasks.

## Build

```sh
mise run //package/cli/forbidden-strings:build
```

The release binary lands at `package/cli/forbidden-strings/target/release/forbidden-strings`.
Root `cli-git.config.ts` gives that path to the bundled `security/forbidden-strings` policy;
nothing needs to be on `$PATH`.

## Setup

The scanner needs exactly one rules file at scan time.
 How you produce it is up to you.

### Without file-enforcer (most consumers)

Put one rule per line in a file named `forbidden-strings.local.txt` at the repo root,
 or
pass `--rules <PATH>` / set `FORBIDDEN_STRINGS_RULES=<PATH>` to point at any other path.
That is the whole setup.
 For a zero-file start,
 pass `--builtin-rules` to scan with the
embedded betterleaks-ported baseline (see "Built-in baseline" below).
 Add the file to
`.gitignore` if the rules themselves are sensitive;
 otherwise commit it.
 The "Rule file
format" section describes the line syntax.
 In CI,
 materialise the file from a secret (see
"GitHub Actions" below) so the rule bodies never enter version control.

### With file-enforcer (this monorepo's workflow)

Inside the Monochromatic monorepo,
 no rules file exists at the repository root
(see `doc/decision/gitignore-negations.md`).
 The pieces:

- The betterleaks baseline ships inside the scanner binary
  (`data/builtin-rules.txt`,
   regenerated by
  `package/cli/forbidden-strings/src/mise.port-betterleaks.ts`);
   repo invocations activate
  it with `--builtin-rules` (the cli-git policy sets `builtinRules: true`).
- `forbidden-strings.append.txt` is the committed shared appendix of non-sensitive
  repo-wide rules.
- `forbidden-strings.append.local.txt` is the per-repo additions.
   Gitignored,
   free-form,
  edited by hand.
   Place sensitive literals (codenames,
   customer names,
   partner IDs) here.
- `.cache/forbidden-strings.rules.txt` is the runtime file consumed by the scanner.
  Generated by file-enforcer concatenating the two appendixes into the gitignored
  `.cache/` scratch dir.
   The generated root `mise.toml` `[env]` points
  `FORBIDDEN_STRINGS_RULES` at it (absolute via `{{config_root}}`).
   Do not edit directly.

Run `mise run file-enforcer` after editing either appendix to regenerate the runtime
file.
 The generator is `generateForbiddenStringsRules` in `file-enforcer.config.ts`.
 If you
fork this scanner into a project that does not use file-enforcer,
 drop the appendix split
and follow the single-file workflow above.

## Usage

```sh
# scan a specific file list (uses ./forbidden-strings.local.txt by default)
forbidden-strings path/to/file other/file

# scan every working-tree file (.gitignore respected; .git/.jj skipped)
forbidden-strings --all
```

The rules path is resolved in this order:
 `--rules <PATH>` flag (highest),
 then
`FORBIDDEN_STRINGS_RULES` env var,
 then `./forbidden-strings.local.txt` in the current
working directory.

```sh
# explicit path
forbidden-strings --rules ./other-rules.txt --all

# via env var (CI-friendly: materialize from a secret, then run)
FORBIDDEN_STRINGS_RULES=./materialized.txt forbidden-strings --all

# print version and exit
forbidden-strings --version    # or -V
```

### Built-in baseline (`--builtin-rules`)

The binary embeds the betterleaks-ported baseline ruleset.
 The text form
(`data/builtin-rules.txt`) is exported as the library constant
`forbidden_strings::BUILTIN_RULES`;
 the scan path loads the baseline from a serialized
`RegexSet` precompiled at build time (compiling the full baseline at each startup is not
viable,
 so the cost is paid once during the build).
 It is pure opt-in:
 without the flag the
scanner never reads it,
 so existing invocations behave exactly as before the flag existed.

With `--builtin-rules`:

- The baseline is appended after the resolved rules file,
  so `rule=N` numbers for your own rules do not shift:
   your rules keep ids `0..user_len`
  and the baseline takes `user_len..`.
- When no rules file resolves at all
  (no `--rules`,
   no env var,
   and no `./forbidden-strings.local.txt` in cwd),
  the baseline alone is the ruleset;
   passing the flag is itself the configuration.
- An explicitly named missing file
  (`--rules <path>` or the env var pointing at a path that does not exist)
  still exits 2:
   silently scanning without your rules would be a false-clean result.

```sh
# zero-file quick start: scan the tree with the embedded baseline only
forbidden-strings --builtin-rules --all

# your rules plus the baseline
forbidden-strings --builtin-rules --rules ./rules.txt --all
```

`--all` and positional files are mutually exclusive in practice:
 if both are passed,
 the
walker output silently overwrites the positional list.
 Use one or the other.

## Rule file format

One rule per line.
 Two shapes:

- A bare line is a **case-sensitive literal**.
   It is escaped into the engine's verbose
  dialect and matched as a plain substring:
   it fires wherever its exact bytes appear,
  including glued mid-identifier (`ACR` matches inside `ACRYLIC`).
   There is no
  length threshold and no word-boundary heuristic;
   over-matching in this direction is a
  ratified preference (a false positive inside a longer token is acceptable).
   If a short
  literal must match only as a whole word,
   write it as a regex with explicit boundaries
  (`/\bACR\b/`).
- A line of the shape `/PATTERN/FLAGS` is a **regex** in the `forbidden-regex` dialect.
  The first `/` and the last `/` delimit the pattern.
   `FLAGS` is a trailing run of
  ASCII-lowercase letters;
   if the trailing run is not all-lowercase,
   the whole line is
  treated as a literal instead (so `/foo/I` is a literal scan for the seven bytes `/foo/I`,
  not a case-insensitive regex).

Empty and whitespace-only lines are ignored.
 A line whose first non-whitespace byte is `#`
is a comment.
 One leading UTF-8 BOM is stripped from the source.
 An empty source (no
non-blank,
 non-comment line) is a rule-file error.

### Flags policy

The engine is always in multiline and verbose mode,
 so the only accepted flags are the
ones those two modes already imply:

- `m` (multiline) and `x` (verbose) are accepted as no-ops and dropped.
- **Any other flag letter is a hard, fail-closed load error.
  ** Silently dropping an `i` or
  an `s` would change match semantics (case folding,
   dot-matches-newline),
   so the loader
  rejects the whole ruleset rather than weaken a rule.
   Need one of those locally?
   Restructure
  the pattern (for case-insensitivity,
   spell the alternatives:
   `[Aa][Bb][Cc]`).

### Supported constructs

The dialect is a deliberately restricted subset (see
`package/rust-module/forbidden-regex/README.md` for the full engine spec):

- Literals and the escapes `\t`,
   `\b` (word boundary),
   backslash-escaped metacharacters,
  and backslash-escaped whitespace.
- Character classes:
   `[abc]`,
   `[a-z]`,
   `[a-zA-Z]`,
   negated `[^...]`,
   and the shorthands
  `\d \w \s \D \W \S` (usable inside classes too).
- `.` matches any byte except a newline.
- Grouping and alternation:
   `(?:a|b)`.
   Groups are **non-capturing only**.
- Bounded repetition:
   `a?`,
   `a{3}`,
   `a{3,6}`.
- Anchors:
   `^`,
   `$`,
   `\b`.
   The word set is ASCII `[A-Za-z0-9_]`;
   `^` and `$` anchor at
  line boundaries.
- Set algebra:
   intersection `&`,
   complement `~(...)`.

Matching is an unanchored search over a single line's raw bytes:
 a pattern matches if it
matches any substring.
 Because verbose mode is always on,
 unescaped whitespace outside
character classes is ignored,
 so a rule may be written across several physical characters
for readability;
 to match a literal space use `\ `,
 `\t`,
 or `[ ]`.

### Set-algebra operators

Two top-level set operators that pure-PCRE engines lack:

- `A & B` (intersection):
   matches strings matched by both `A` and `B`.
- `~(A)` (complement):
   matches strings that do NOT match `A`.

Operators `&` and `|` take single-atom operands:
 a literal,
 a class,
 `.`,
 an anchor,
 a
`(?:...)` group,
 or a `~(...)`.
 A concatenation or a quantified atom must be wrapped in
`(?:...)` to be an operand,
 so there is no operator precedence to remember.
 `~(...)` is
always parenthesized.
 A pattern that can match the empty string is rejected (unanchored,
 it
would match everything),
 so `~(Y)` alone is rejected while `(?:X) & ~(Y)` with a concrete
`X` compiles.
 Example:
 ban any five-digit key except the all-zeros placeholder:

```text
/(?:key_[0-9]{5}) & ~(key_00000)/
```

### Rejected at compile time (fail-closed)

Anything outside the supported set is a hard compile error naming the offending rule's
opaque index,
 and the whole load fails closed (a bad ruleset never degrades to a partial
scan):
 `*`,
 `+`,
 unbounded `{n,}`,
 `\xNN` byte escapes,
 capturing `(`,
 lookaround and
inline-flag groups (`(?` not followed by `:`),
 backreferences,
 unknown escapes,
 unbalanced
brackets,
 stacked quantifiers,
 `{n,m}` with `n` greater than `m`,
 repetition whose
expansion exceeds the engine's cap,
 and any pattern that can match the empty string.

Load errors are redacted:
 they carry only the opaque rule index (0-based position in the
compiled set,
 never a source line number) and the engine's own static reason,
 never the
rule text.
 The redacted error type is `LoadError` in `src/rule/frx/error.rs`.

## Output

For each violation:

```text
PATH:LINE rule=N
```

- `LINE` is the 1-based line number.
- `rule=N` is the 0-based engine rule id.
   Your runtime rules take ids `0..user_len`;
  under `--builtin-rules` the baseline is offset past them.
   The index is columnless:
   the
  engine reports per-line rule indices,
   not spans,
   so no `COL_START..COL_END` segment
  appears.
- One finding is emitted per `(line, rule)` pair.
- **The matched substring,
   the line content,
   and the rule pattern are never printed.
  **
  Only the path,
   line number,
   and opaque rule index appear,
   so a failing CI log never
  becomes a leak surface.
   A contributor looks the index up against their local rule file.

Two synthetic findings keep the scan fail-closed:

- **Read errors.
  ** A file that cannot be opened (broken symlink,
   permission denied,
   deleted
  during scan) produces `PATH: read error: <reason>` on stderr and counts toward the
  exit-1 total.
   A secret-scanning gate must not pass silently on a file it could not inspect.
- **Engine errors.
  ** If the matcher panics on a file,
   the `catch_unwind` boundary in
  `scan_one_set` (`src/frx_scan.rs`) catches it and emits `PATH: engine error`,
   again
  counting toward exit 1 rather than aborting or exiting clean.

Hits go to **stderr**,
 not stdout;
 redirecting `2>/dev/null` silently loses the report.
Within a file,
 findings are emitted in set order (runtime rules before the baseline),
 then
by line;
 across files,
 ordering is rayon-scheduler-determined,
 stable on a given input but
not alphabetic.
 Callers that need deterministic cross-file reports should pipe the output
into `sort`.

Exit codes:

- `0`:
   no violations.
- `1`:
   one or more violations (real hits,
   read errors,
   or engine errors).
- `2`:
   usage error or rule-file error.

## Security model

The redaction guarantee is what lets a rule body itself be a secret.
 Two boundaries carry
it:

- **Load path.
  ** Rule compilation reports only `LoadError` (`src/rule/frx/error.rs`),
   whose
  every variant is an opaque index plus the engine's static reason;
   no pattern bytes reach
  a diagnostic.
   The compiler builds through `RegexSet::new` / `RegexSet::from_bytes`,
  neither of which logs the pattern.
- **Scan path.
  ** Findings are formatted as `PATH:LINE rule=N` in `src/frx_scan.rs`;
   the
  matched bytes and the line content are never included.
   The fail-closed `catch_unwind`
  boundary emits only `PATH: engine error`.

Keep sensitive rule bodies out of tracked files:
 use the gitignored
`forbidden-strings.append.local.txt` or the CI-only `FORBIDDEN_STRINGS_LIST` secret,
 never
the committed baseline or appendix.
 In CI,
 pipe secrets through `printenv` rather than
interpolating them into a workflow command;
 shell expansion can leak values even when log
masking is enabled.

## Integration

### Local cli-git policy

Root `cli-git.config.ts` enables `security/forbidden-strings` at error severity.
 The
PATH-shadowed cli-git wrapper evaluates selected would-be-committed bytes before commit,
landed commit bytes before automatic push,
 and Git-native outgoing ranges before manual
push.
 Native `--no-verify` skips Git hooks but does not skip this policy.

Run an explicit read-only check through the built shim with:

```sh
git cli-git check --policy security/forbidden-strings --all
```

The policy invokes the repository-built scanner directly.
 Scanner infrastructure failures
remain distinct exit-2 engine failures;
 findings exit `1`.

### GitHub Actions

`.github/workflows/forbidden-strings.yml` remains independent of cli-git trust and local
wrapper state.
 It downloads the release matching the scanner crate version,
 verifies the
archive's GitHub build-provenance attestation,
 materializes the committed baseline plus
shared appendix and optional repository secret,
 then invokes the scanner binary directly.
Pull-request and merge-queue jobs scan changed files relative to `origin/main`;
 pushes to
`main` scan the complete tracked tree.
 The same precedence applies locally and in CI:
`--rules` > `FORBIDDEN_STRINGS_RULES` > `./forbidden-strings.local.txt`.

## Walker behaviour

- **`--all` semantics.
  ** Walks the working tree via `ignore::WalkBuilder` in `src/walk.rs`:
  `.hidden(false)` (dotfiles like `.github/`,
   `.npmrc` ARE scanned),
   `.ignore(false)` (the
  `.ignore` file is NOT consulted;
   `.gitignore` stays enabled).
   Files force-added past
  `.gitignore` (`git add -f`) are recovered via an in-process `gix-index` read of
  `.git/index`;
   no git subprocess.
- **`.git/` and `.jj/` skipped.
  ** Internal VCS state is never scanned.
- **Symlinks NOT followed.
  ** `WalkBuilder`'s default `follow_links` is false;
   symlinked
  directories are not descended,
   symlinked files surface as a read-error synthetic hit on a
  broken target.
- **Non-UTF-8 paths silently dropped.
  ** Index entries that are not valid UTF-8 are excluded
  from the walk.
- **Binary-file 8 KiB tail cap.
  ** Files whose first 8 KiB contains a NUL byte are scanned
  only in the first 8 KiB.
   The leading window always runs,
   so secrets there fire;
   the tail
  past 8 KiB is skipped.
   Constant `BIN_PROBE_SIZE` and `read_with_binary_check` in
  `src/lib.rs`.
- **Self-skip set.
  ** During `--all`,
   canonical paths are auto-skipped so rule bodies do not
  self-match:
   the materialised rules file (whatever `--rules` / env var / default resolves
  to),
   plus three generated-source paths:
  - `package/cli/forbidden-strings/data/betterleaks-default-config.toml`
  - `package/cli/forbidden-strings/data/builtin-rules.txt`
  - `package/cli/forbidden-strings/src/port-betterleaks-relaxations.ts`

  Skip is path-anchored via `std::fs::canonicalize`,
   not basename-anchored,
   so an
  unrelated file named `forbidden-strings.local.txt` in a subdirectory is still scanned.
  Paths that fail to canonicalize from the current cwd are silently dropped from the set.
  Explicit positional arguments bypass the `--all` skip;
   the scanner's own
  `forbidden-strings.*.txt` config files at cwd are skipped in both modes
  (`is_config_file_at_cwd` in `src/lib.rs`).

## Performance

The scanner is a native Rust binary tuned for a sub-commit-budget startup and linear-time
matching;
 the release profile (`Cargo.toml`) sets `lto`,
 `codegen-units = 1`,
 `opt-level = 3`,
`panic = "unwind"`,
 `overflow-checks = true`,
 and `strip`.
 `panic = "unwind"` and
`overflow-checks = true` are load-bearing for the fail-closed `catch_unwind` boundary,
 not
speed:
 the forbidden-regex engine documents that it expects the caller's unwind boundary,
and a wrapped overflow would otherwise let a corrupt rule fail open.

Full bench methodology and per-version regression history are in `PERF.md`.
 The headline
figures there were measured against the pre-0.2.0 resharp/`regex`-crate engine;
 the 0.2.0
figures for the `forbidden-regex` engine are re-measured as part of the cutover
differential validation.

## Fuzzing

Coverage-guided fuzzing lives in its own package,
 `package/fuzz/forbidden-strings`,
 so a
scoped nightly toolchain does not force this published crate onto nightly.
 The scanner
exposes a curated internal surface (`fuzz_api`,
 behind the `fuzzing` Cargo feature) for the
targets to drive.
 The teardown that removed the old engine also retired the gate,
 shard,
 and
routing targets that fuzzed it;
 the surviving targets are retargeted onto the
`forbidden-regex` load and scan path.
 See that package's README for prerequisites,
 mise
commands,
 the bounded-container wrapper,
 and crash-reproduction guidance.

## Architecture

- **Two-form loader.
  ** `src/rule/frx` owns the rule-file format:
   bare literals escape into
  the verbose dialect,
   `/PATTERN/FLAGS` lines pass through under the flags policy,
   and each
  rule is validated individually so the redacted error can name the first offender's index.
  `src/frx_load.rs` resolves the runtime rules file (compiled from text) and,
   under
  `--builtin-rules`,
   the precompiled baseline,
   into ordered `RegexSet`s with disjoint
  rule-id ranges.
- **Line-based batch scan.
  ** `src/frx_scan.rs` splits each file's bytes into lines once and
  hands the buffer plus line-start offsets to `RegexSet::line_matches`,
   which resolves
  per-line rule ids in one SIMD prefilter sweep.
   Each set runs under a `catch_unwind`
  boundary so a matcher fault fails closed as a synthetic finding.
- **Build-time precompilation.
  ** `build.rs` compiles `data/builtin-rules.ported.txt`
  through the engine once at build time and serializes it (`to_bytes`);
   `lib.rs` embeds the
  blob with `include_bytes!` and the loader rebuilds it via the validating `from_bytes`,
  never recompiling.
   Only the small runtime rules file compiles from text at startup.
- **Concurrent load and walk.
  ** Rule loading and `--all` file walking run concurrently via
  `rayon::join` (they share no state);
   files then fan out across the rayon thread pool for
  the parallel scan.
- **`ignore` crate walker + in-process gix-index union.
  ** `--all` uses `ignore::WalkBuilder`
  (honouring `.gitignore`,
   `.git/info/exclude`,
   and global excludes) and unions the result
  with an in-process `gix_index::File` read of `.git/index` so `git add -f` files are still
  discovered.
   See `src/walk.rs`.
- **Bundled `data/betterleaks-default-config.toml`.
  ** Upstream-vendored provenance for the
  betterleaks port;
   the embedded baseline is derived from it,
   and
  `port-betterleaks-relaxations.ts` records the lossy translations applied during the port.