safe-chains 0.230.5

Auto-allow safe bash commands in agentic coding tools
Documentation
# Mutation testing: what is specific to safe-chains

General method is in the `rust-mutation-testing` skill. This is what is specific to safe-chains.

- **Per change** (`.github/workflows/mutants.yml`, not gating): `--in-diff` on every PR and push to
  `main`, skipped with a warning above 20 selected mutants; and on each push to `main` one
  **rotating slice**, `--shard (run_number % 512)/512`. PR runs advance the same counter, so
  coverage is roughly one pass per 512 runs rather than exactly one per 512 pushes. There is no
  whole-tree sweep: ~5,300 mutants at ~39s of runner wall clock each is ~58 hours.
- **N = 512, from the runner.** The first CI slice (run 36363446362, slice 1/256, 21 mutants) took
  16.7 minutes: baseline 105s build + 98s test, then ~39s of wall clock per mutant at `-j2`, against
  a 20-minute job timeout. The `mutants` profile did apply (`--profile=mutants` in its baseline
  log); the runner is just slower than the laptop, where slice 128/256 took 6m45s (21s build + 45s
  test). At 512 a slice is ~10 mutants, ~10 minutes. If slices approach the timeout again, raise N
  rather than the timeout. The in-diff cap of 20 is from the same runner rate.
- **Test-bound, and the profile is the lever.** A mutant's rebuild is 2-5s; its test run is the full
  suite. The integration tests spawn the debug binary a few hundred times, and unoptimized each
  spawn spent ~0.4s parsing the registry TOML inside the dependencies. `.cargo/mutants.toml` selects
  the `mutants` profile (Cargo.toml), which optimizes dependencies only: suite 124s to 45s, rebuild
  cost unchanged. Restricting the test command was not an option: `cli_gate`, `path_policy` and the
  hook tests drive the real binary (`CARGO_BIN_EXE_safe-chains`), which is built from the mutated
  tree, so they catch mutants the lib tests do not.
- **The `fuzz-gen` feature is on** for mutation runs, or `gen-fuzz-corpus` is never built and all of
  its mutants read as MISSED. CI's test job runs its tests the same way.
- **Exclusion:** `gen-fuzz-corpus`'s `main` (binds the checkout path and prints counts; `generate`
  holds the logic and is tested). Reason in `.cargo/mutants.toml`.
- **Equivalent, not excluded:** `delete !` in `sed_cluster`'s `-f` arm (`consumes_next: !has`).
  `scan_sed` flags every `-f` spelling and `resolve_sed` worst-cases before the cluster parser runs,
  so the `ScriptFile` arm's word count is never observed. A slice that lands on it reports it.
- **Findings of the first slices (2026-09-27):** slice 128/256 had 5 MISSED of 20 viable (75%): no
  test pinned which word `sed -e`/`-l` consume, that a trailing `-l` fails closed, or that an unknown
  byte in a short cluster fails closed when the script itself is harmless (`sed -Q ./foo` was caught
  only because `./foo` is an unknown sed command). Four now have
  `sed_short_flags_consume_exactly_their_own_value`; the fifth is the equivalent one above.
  `gen-fuzz-corpus` was never built by `cargo test` and had no tests; it now has four, and a run
  over the file caught 27 of 28 viable (the 28th is the excluded `main`).
- **CI slice 1/256 (run 36363446362):** 1 MISSED of 17 viable: `!=` to `==` in `facet_breakdown`'s
  one-segment guard. Nothing called the function directly. Now killed by
  `facet_breakdown_profiles_a_single_command_and_declines_a_chain` in `tests/facet_breakdown.rs`. Writing it
  showed that a PIPELINE is one segment to `cst::explain`, so `facet_breakdown("cat x | rm -rf /")`
  still gets the flat split the guard exists to prevent (a diagnostic only, not a verdict; see
  TODO.md).
- **CI slice of run 37348486489 (2026-10-05):** 3 MISSED of 10 viable: `print_docs` emptied,
  `--explain`'s exit code inverted, and `||` to `&&` in `canonicalize_home`'s no-home guard. Killed
  by `list_commands_prints_the_command_reference` and `explain_exits_with_the_verdict` in
  `tests/cli_gate.rs`, and the property `without_a_usable_home_every_word_is_left_alone`.
- **CI slice of run 37362532620:** 2 MISSED of 9 viable, both in `src/allowlist.rs`: the bare-`~`
  guard in `canonicalize_home` set to `false`, and `Matcher::is_empty` returning `true` (only ever
  asserted on empty matchers). Killed by `a_usable_home_expands_both_tilde_spellings` and a
  non-empty assertion in `parse_exact_pattern` and `parse_legacy_colon_star`.
- **Hermeticity it surfaced:** cargo-mutants builds in a copy under `$TMPDIR`, and
  `the_path_policy_corpus_holds` used the checkout itself as the workspace under the real `$HOME`, so
  its baseline failed there (a sibling is `adjacent` only under `$HOME`). The test now builds
  `~/projects/safe-chains` and a peer under a HOME of its own.