fix(guardrails): configurable hook-manager bypass + broader machine-path roots - #932
Conversation
|
Claude finished @kyle-sexton's task in 2m 43s —— View job PR Review
Overall this is solid, well-scoped work. The regex injection protection is correct, the fallback logic is safe, and the test coverage is thorough. Two minor findings below. Finding 1 — Minor: Stale description on
|
|
Claude finished @kyle-sexton's task in 2m 25s —— View job Security Review
No CRITICAL or IMPORTANT security issues found. Two SUGGESTION-level findings below. SUGGESTION — Replace-not-extend semantics for custom prefix set (confidence: HIGH)Severity: SUGGESTION | Confidence: HIGH
When a consumer sets # Consumer sets: block_no_verify_hook_manager_prefixes = "myhooks"
# Result: HUSKY=0 git commit -m test → ALLOWED (silently missed)The test at line 181 confirms this: Recommendation: Either (a) change the semantics to extend rather than replace (append the sanitized custom list to the built-in defaults), or (b) strengthen the SUGGESTION — All-non-alphanumeric prefix value falls back to
|
| File | Finding |
|---|---|
hooks/block-no-verify.sh |
Sanitization of user-supplied prefixes correct; regex injection prevented. Two suggestion-level logic edge cases above. |
lib/path-detection/machine-path-patterns.sh |
Hardcoded literal additions only; no injection surface, no bypass opened. |
.claude-plugin/plugin.json |
New userConfig key is properly typed string with a safe empty default; description gap noted above. |
hooks/block-no-verify.test.sh |
Metachar sanitization test at line 183 correctly verifies the intended injection-prevention behavior. |
CHANGELOG.md |
Documentation only; no security surface. |
No command injection, no credential exposure, no privileged operations, no workflow-level risks, no supply-chain changes.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b2ca960da
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @kyle-sexton's task in 2m 23s —— View job
PR Review — commit 9a33e66 (pre-filter widening fix)The new commit correctly closes the fail-open gap flagged by Codex: the cheap pre-filter gate in Three minor findings from the prior reviews remain unaddressed — carried forward below for visibility. Finding 1 — Minor:
|
| Check | Result |
|---|---|
Pre-filter gate covers repos|Repos|projects|Projects|dev|Dev |
✓ matches every token the detailed bodies accept |
| Gate stays a strict superset (looser is safe; tighter is a bypass) | ✓ dev/Dev match as substring, which only adds wasted full-scans, no missed detections |
| Regression test exercises the exact bypass path | ✓ D:\Projects\acme\src with no "Users"/"repos" literal |
| No new injection surface or logic change | ✓ gate is read-only filter, not a security boundary |
| Commit message accurately describes the fix | ✓ |
Summary: The pre-filter fix is correct and the regression test is well-targeted. Three minor doc/comment issues from the prior reviews remain open — Finding 1 (stale toggle description in plugin.json) is the most consumer-visible and worth addressing before merge; Findings 2–3 are lower-stakes but also straightforward fixes.
|
Claude finished @kyle-sexton's task in 3m 7s —— View job Security Review
No CRITICAL or IMPORTANT security issues found. Two pre-existing SUGGESTION-level observations below; one is slightly sharper here than in previous reviews. SUGGESTION — Fail-open window between commits 1 and 2 is now closed; verify squash-merge order (confidence: HIGH)Severity: SUGGESTION | Confidence: HIGH
The first commit ( SUGGESTION — Regex injection sanitization is correct but the fallback is inconsistent (confidence: MEDIUM)Severity: SUGGESTION | Confidence: MEDIUM
IFS=',' read -ra _hm_list <<<"${CLAUDE_PLUGIN_OPTION_BLOCK_NO_VERIFY_HOOK_MANAGER_PREFIXES:-lefthook,husky,pre_commit,simple_git_hooks}"
for _hm in "${_hm_list[@]}"; do
_hm="${_hm//[^a-zA-Z0-9_]/}"
_hm="${_hm,,}"
[[ -n "$_hm" ]] && HM_ALT="${HM_ALT:+$HM_ALT|}$_hm"
done
[[ -n "$HM_ALT" ]] || HM_ALT="lefthook" # ← fallback covers one manager onlyThe The residual concern is the last-resort fallback: if a consumer supplies a value composed entirely of non-alphanumeric characters (e.g., What was reviewed
No command injection, no credential exposure, no privileged operations, no |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a33e665a4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Automated babysit-lane classification (safe tier — reply only; not resolving threads/merging).
Remaining blocker per prior check: none new — mergeStateStatus is BLOCKED (recomputing/pending review), not a conflict. |
9a33e66 to
d74e5e1
Compare
|
Claude finished @kyle-sexton's task in 2m 44s —— View job
PR Review — commit d74e5e1Finding 0 — BLOCKER: Squash dropped the pre-filter fix for
|
|
Claude finished @kyle-sexton's task in 4m 16s —— View job Security Review
One IMPORTANT finding, two SUGGESTION-level findings. No CRITICAL issues. IMPORTANT — Pre-filter gate in
|
| Body token | In gate? | Gate behavior on exclusive match |
|---|---|---|
repos |
✓ (repos) |
full scan runs |
Repos |
✗ | early-returns 0 (clean) |
projects |
✗ | early-returns 0 (clean) |
Projects |
✗ | early-returns 0 (clean) |
dev |
✗ | early-returns 0 (clean) |
Dev |
✗ | early-returns 0 (clean) |
Bypass scenario:
Content written: "See C:\Projects\acme\src\config.json for details"
Gate check: grep -qE 'Users|/home/|repos' → NO MATCH
Gate result: project_root not set OR root segment absent → return 0 (CLEAN)
HPP_WIN_REPO_BODY scan: never reached
Guard verdict: BYPASSED — path leaks into the codebase
Any content whose only machine-specific path uses one of the five ungated root tokens silently passes the guard. The gate's early-return is the issue — the detailed HPP_WIN_REPO_BODY grep that would catch the path is never executed.
This was flagged as a P1 by Codex in commit 8b2ca96 and recorded as fixed in commit 9a33e665. The current branch HEAD (d74e5e1) is a squash that does not include hardcoded-path-patterns.sh in its diff — the fix appears to have been dropped during the squash. The five-token bypass is present in the branch as it stands.
Fix: Widen the gate to cover every token now in the body:
if ! grep -qE 'Users|/home/|repos|Repos|projects|Projects|dev|Dev' <<<"$content" 2>/dev/null; thenSUGGESTION — All-non-alphanumeric prefix value falls back to lefthook only, not the full default set (confidence: MEDIUM)
hooks/block-no-verify.sh line 88:
[[ -n "$HM_ALT" ]] || HM_ALT="lefthook" # never leave the guard patternlessWhen a consumer-supplied value consists entirely of non-alphanumeric characters (e.g. "!!!"), every entry strips to empty and HM_ALT stays empty. The last-resort fallback then sets it to lefthook alone — weaker than both the unset/empty case (which resolves all four managers via the :- expansion on line 82) and any valid consumer value. This creates an inconsistency:
| Consumer config | Managers detected |
|---|---|
| Unset / empty | lefthook, husky, pre_commit, simple_git_hooks ✓ |
"myhooks" |
myhooks ✓ (replace semantics, expected) |
"!!!" |
lefthook only ✗ (weakened silently) |
Fix: Replace the fallback with the same four-manager default string used in the :- expansion:
[[ -n "$HM_ALT" ]] || HM_ALT="lefthook|husky|pre_commit|simple_git_hooks"SUGGESTION — Inline comment says "extends"; implementation replaces (confidence: HIGH)
hooks/block-no-verify.sh line 75:
# The default set covers the common managers; a consumer extends it via the
# block_no_verify_hook_manager_prefixes userConfig (comma-separated, ...The test at line 180–181 confirms the replacement semantics:
run "custom set replaces the default (lefthook now allowed)" "LEFTHOOK=0 git commit -m test" 0 \
CLAUDE_PLUGIN_OPTION_BLOCK_NO_VERIFY_HOOK_MANAGER_PREFIXES="myhooks"A consumer who reads "extends" and configures only myhooks will silently drop HUSKY=0, PRE_COMMIT=0, and SIMPLE_GIT_HOOKS=false from detection. The word "extends" should be "overrides", with a note that preserving the defaults requires spelling out all four.
W3 — no new injection surfaces (block-no-verify.sh)
${_hm//[^a-zA-Z0-9_]/}strips all ERE metacharacters before splicing into the alternation. The post-sanitization character set[a-z0-9_]carries no special meaning inside(…)in a bash=~pattern. Safe.:-expansion on line 82 correctly fires for both unset and empty-string cases.jq -r '.tool_input.command // empty'+--arginemit_teluse no unsafe shell expansion of user-controlled data. Safe.- No
eval, noexec, no command substitution of user-supplied values.
W4 — no new injection surfaces (machine-path-patterns.sh)
- All additions are hardcoded string literals inside single-quoted shell variables — no user input, no variable interpolation, no runtime expansion at define time. Safe.
- Drive-letter anchor
[A-Za-z]:limits false positives for the broader root set.
plugin.json and CHANGELOG.md
- The new
block_no_verify_hook_manager_prefixesfield is typedstringwith a safe empty default that correctly falls through to the:-expansion. No supply-chain changes, no new external dependencies, no permission widening, no workflow changes.
Summary: The IMPORTANT finding — the pre-filter gate in hardcoded-path-patterns.sh is not updated to cover the five new body tokens — is a guard bypass for any path whose only machine-specific root is Repos, projects, Projects, dev, or Dev. This was fixed in an earlier iteration of the PR but appears to have been dropped in the squash to d74e5e1. It needs to be restored before merge. The two SUGGESTION-level items (fallback inconsistency, "extends" comment) are straightforward fixes with no exploitable path but meaningful consumer-safety impact.
…ath roots Two-lane posture fixes (audit #912). W3 - block-no-verify hook-manager env-var bypass detection matched only `lefthook*`, silently missing HUSKY=0 and other managers. Resolve a configurable prefix set (block_no_verify_hook_manager_prefixes userConfig; default lefthook, husky, pre_commit, simple_git_hooks). Consumer values are reduced to identifier chars so no regex metacharacter can be injected into the alternation. W4 - hardcoded-path-check's drive-letter-anchored checkout-parent pattern matched only `X:\repos\...`, missing `C:\Projects\...` (this very repo) and `C:\Dev\...`. Broaden to also match Projects and Dev (both capitalizations); a consumer's own checkout root stays caught by the driver's project-root scan. guardrails 0.9.5 -> 0.9.6 + CHANGELOG. Tests: block-no-verify 83/0, hardcoded-path-check 38/0, shellcheck clean. Refs #918 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d74e5e1 to
a318b5a
Compare
|
Claude finished @kyle-sexton's task in 3m 53s —— View job PR Review
Pre-filter fix DROPPED from squash — W4 is broken at HEADThis is the most important finding. Intermediate commit
if ! grep -qE 'Users|/home/|repos' <<<"$content" 2>/dev/null; then
# repos ⊇ Windows-repo + escaped-Windows-repo ← no longer true after W4Effect: Content whose only hardcoded path is Required fix: if ! grep -qE 'Users|/home/|repos|Repos|projects|Projects|dev|Dev' <<<"$content" 2>/dev/null; thenand update the comment at lines 58–63 to list the new roots. The corresponding regression test in Finding 1 (Minor) —
|
|
Claude finished @kyle-sexton's task in 3m 2s —— View job Security Review
One IMPORTANT finding, two SUGGESTION-level findings. No CRITICAL issues. IMPORTANT — Pre-filter gate in
|
| Body token | In gate? | Gate result on exclusive match |
|---|---|---|
repos |
✓ (repos) |
full scan runs |
Repos |
✗ | early-returns 0 (clean) |
projects |
✗ | early-returns 0 (clean) |
Projects |
✗ | early-returns 0 (clean) |
dev |
✗ | early-returns 0 (clean) |
Dev |
✗ | early-returns 0 (clean) |
Bypass scenario:
Content: "See C:\Projects\acme\src\config.json for details"
Gate check: grep -qE 'Users|/home/|repos' → NO MATCH (case-sensitive; "Projects" ≠ "repos")
Gate result: early-return 0 (CLEAN)
HPP_WIN_REPO_BODY scan: never reached
Guard verdict: BYPASSED — hardcoded Windows path with a Projects/Dev/Repos root leaks through
This was introduced by the W4 body expansion in this PR without a corresponding gate update. The gate was fixed in an earlier iteration of this branch (commit 9a33e665) and a regression test was added to hardcoded-path-check.test.sh — both were dropped when the branch was squashed to d74e5e1 and remain absent in the current HEAD a318b5a. hardcoded-path-patterns.sh and its test file are not in the PR's changed-files list at all.
Required fix — widen the gate to cover every token now accepted by the body patterns:
# hardcoded-path-patterns.sh line 74:
if ! grep -qE 'Users|/home/|repos|Repos|projects|Projects|dev|Dev' <<<"$content" 2>/dev/null; thenAlso update the comment at line 62 (repos ⊇ Windows-repo + escaped-Windows-repo) to reflect the full set, and restore the C:\Projects\… regression test in hardcoded-path-check.test.sh.
SUGGESTION — All-non-alphanumeric prefix value falls back to lefthook only, not the full default set (confidence: MEDIUM)
hooks/block-no-verify.sh line 88:
[[ -n "$HM_ALT" ]] || HM_ALT="lefthook" # never leave the guard patternlessIf a consumer-supplied block_no_verify_hook_manager_prefixes value consists entirely of non-alphanumeric characters (e.g. "!!!"), every entry strips to empty and HM_ALT stays empty. The last-resort fallback then sets it to lefthook alone — weaker than the unset/empty path (which resolves all four managers via the :- expansion on line 82). The asymmetry:
| Consumer config | Managers active |
|---|---|
Unset / "" |
lefthook, husky, pre_commit, simple_git_hooks ✓ |
"myhooks" |
myhooks ✓ (replace semantics, expected) |
"!!!" (all stripped) |
lefthook only ✗ |
Practical exploitability is near-zero (requires a deliberately nonsense config value), but the fix is a one-liner: use the same four-manager string the :- expansion uses.
[[ -n "$HM_ALT" ]] || HM_ALT="lefthook|husky|pre_commit|simple_git_hooks"SUGGESTION — Inline comment says "extends"; implementation replaces (confidence: HIGH)
hooks/block-no-verify.sh lines 75–76:
# The default set covers the common managers; a consumer extends it via the
# block_no_verify_hook_manager_prefixes userConfig (comma-separated, ...A non-empty consumer value replaces the entire default set — it does not extend it. The test at line 181 ("custom set replaces the default (lefthook now allowed)") confirms this. A consumer who reads "extends" and sets only myhooks expecting to ADD it alongside the defaults will silently drop HUSKY=0, PRE_COMMIT=0, and SIMPLE_GIT_HOOKS=false from detection.
Suggest changing "extends" → "overrides" and adding a note that preserving the defaults requires spelling out all four explicitly.
What was reviewed
| File | Verdict |
|---|---|
hooks/block-no-verify.sh |
${_hm//[^a-zA-Z0-9_]/} strips all ERE metacharacters before splicing into =~; post-lowercase chars are only [a-z0-9_|] where | is inserted by the loop itself, not user input. No eval, no command substitution of user-controlled data, no injection path. One suggestion-level fallback inconsistency (Finding 2). |
lib/path-detection/machine-path-patterns.sh |
Hardcoded string literal additions only. No user input, no variable interpolation at define time. Drive-letter anchor [A-Za-z]: keeps false-positive risk low for the broader root set. Safe in isolation — but the gate in the calling driver is now under-inclusive (Finding 1). |
hardcoded-path-patterns.sh |
Not in the PR diff. Gate at line 74 is a strict superset of the old body tokens but not the new ones — this is the IMPORTANT bypass above. |
.claude-plugin/plugin.json |
New block_no_verify_hook_manager_prefixes field typed string with empty default that correctly falls through to the :- expansion. No supply-chain changes, no new external references, no permission widening. |
CHANGELOG.md / README.md |
Documentation only. No security surface. |
hooks/block-no-verify.test.sh |
Metacharacter sanitization test at line 183 (my.*hooks,myhooks → only myhooks blocks) correctly verifies injection prevention. Replace-not-extend test at line 181 accurately documents and tests the behavior. |
No command injection, no credential exposure, no privileged operations, no pull_request_target/workflow_run script injection patterns, no permission widening, no supply-chain changes, no loosened/unpinned dependencies.
main released guardrails 0.9.7 (#932 block-no-verify hook-manager prefixes + machine-path broadening) while this branch also carried 0.9.7 for the #740 config-env work; branch entry restacked to 0.9.8, both changelog sections preserved. No file overlap with the #740 hooks; block-no-verify suite green post-merge (83/0). All sync, drift, parity, and check-bump gates green vs current origin/main.
…8) (#946) ## Summary The `hardcoded-path-check` guard's cheap `scan_text` pre-filter had narrowed on `main`: it gated only on `Users|/home/|repos`, while the detailed drive-letter bodies (`HPP_WIN_REPO_BODY` / `HPP_ESCAPED_WIN_REPO_BODY`) accept the broadened `repos|Repos|projects|Projects|dev|Dev` roots shipped in 0.9.7. Content whose only machine path used a widened root (e.g. `C:\Projects\…`, `C:\Dev\…`) therefore early-returned `0` before the detailed scan ever ran — a fail-open in a security gate. This re-widens the pre-filter gate to a strict superset of every root token the detailed bodies accept, adds a `Projects`-root regression test, folds in the stale `block_no_verify_enabled` description sync (it still read "lefthook disables" only, now the full configurable default set), and bumps the plugin to `0.9.8`. All 40 `hardcoded-path-check` tests pass; shellcheck clean. Closes #944 ## Related - #932 — the merged PR whose landed fail-open fix (`9a33e665`) was dropped by a concurrent force-push, landing this regression on `main`; this PR restores it fix-forward. - #918 — the original broadened-machine-roots issue #932 addressed.
…ue grammar (#950) ## Summary Two independent posture fixes from umbrella #912, batched under one source-control bump (`0.16.2 → 0.16.3`). **W1 — dependency-manager hold-merge login set is configurable.** The babysit merge gate held only the built-in `dependabot`/`renovate` product bots (`DEPENDENCY_MANAGER_LOGINS`), so a non-dependabot/renovate dependency bot an operator runs slipped the cross-tier hold. `is_dependency_author` now also matches any login in the new `babysit_extra_dependency_manager_logins` userConfig, threaded as the `--extra-dependency-manager-logins` merge-wrapper flag (mirroring the existing `--approver-bot-logins` arg-threading through `evaluate()`). Logins normalize identically on both sides (casefold, strip `app/` and `[bot]`). Ships empty → unconfigured installs match the built-in set alone. Wired only to the merge gate (the single `is_dependency_author` call site), not the snapshot. **W2 — branch-to-issue grammar is configurable.** `parse-branch-issue.sh` hardcoded the `<type>/<N>-<slug>` (and `routine-issue-<N>`) convention, so a repo on a different scheme (e.g. Jira keys `feature/PROJ-123-slug`) silently failed to derive a `Closes #N` line. The script now accepts an ERE `pattern` positional (last capture group = issue id), passed from the new `branch_issue_pattern` userConfig at the `/pull-request create` call site; the built-in convention stays the default when unset (an unsubstituted `${user_config…}` placeholder is treated as absent). Per the plugins-reference, `CLAUDE_PLUGIN_OPTION_*` reaches hook processes only — not skill-invoked scripts — so the value is passed as an arg rather than read from the environment. ## Testing - `is_dependency_author` extra-login normalization cases (casefold, `app/`, `[bot]`; empty extra never widens the built-in set). - An `evaluate()`-level integration test that flips the dependency hold via the config on the same PR — a pure-function test would pass even with broken wiring, so this exercises the CLI-arg-shaped frozenset → `evaluate()` param → line-715 hold path. - Full babysit Python suite: 348 passed. - `parse-branch-issue.test.sh`: 10 passed (incl. Jira-key custom pattern and the unsubstituted-placeholder fallback). `shellcheck` clean. ## Docs `plugin.json` userConfig (both keys), babysit `SKILL.md` config table, `reference/feedback.md`, source-control README config table, and the `create.md` call site. ## Related - Part of umbrella #912 (contract + design there — not diverged). - Sibling merged this session: #928 (guardrails 0.9.6), #932 (guardrails 0.9.7). Closes #917 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#250) Backfills the broadened Windows checkout-root alternation (`repos|Repos|projects|Projects|dev|Dev`) in `HPP_WIN_REPO_BODY` / `HPP_ESCAPED_WIN_REPO_BODY` that melodic-software/claude-code-plugins#932 shipped directly in its managed materialization, plus contract tests pinning the new roots. Without this, `standards-sync` keeps proposing a revert of the reviewed downstream behavior: the current sync PR (melodic-software/claude-code-plugins#951) stomps the broadened patterns and fails the downstream guardrails contract tests (`hardcoded-path-check.test.sh`: "windows Projects checkout root" cases). After this merges, the push-triggered sync refreshes that PR to the runner-policy lockfile change only, unblocking Dependabot alert remediation there. The component file is byte-identical to the downstream reviewed copy (blob `785d1809`). ## Related - melodic-software/claude-code-plugins#932 (origin of the broadened patterns, reviewed and merged downstream) - melodic-software/claude-code-plugins#951 (sync PR currently blocked by the revert) No linked issue. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…iling separator (#1095) ## Summary The five machine-path bodies in `machine-path-patterns.sh` required a separator AFTER the child segment, which inverted detection both ways (#1093, reproduced live): - A real bare path value at end of line (`root = <drive>:/Dev/GitHub`) has no trailing separator and was **missed** — a false negative on exactly the config-value shape the guard exists to catch. - Prose satisfied the requirement anyway: the space-permitting segment class greedily consumed words until a later slash on the same line, so a comment like `<drive>:/Projects/x - personal repos (reference/reading only here)` was flagged as "Windows repo path detected" while the actual violations passed clean. Fix: all five bodies now exclude whitespace and the double quote from the child-segment class and drop the mandatory trailing separator. Bare values at a natural boundary (EOL, whitespace, quote) are detected; prose spans cannot match (the class requires at least one non-space child character after the root); a bare ROOT with no child segment (`C:/Dev`, a lone `/home`) still never matches. The driver's `/Users/Shared` exclusion now covers the bare-at-EOL form. ## Synced-component constraint `machine-path-patterns.sh` is a managed standards component (`melodic-software/standards` `components/path-detection/`, distributed by the sync bot). The identical pattern change is being filed upstream so the next sync does not revert this fix — upstream PR linked below once open. Local and upstream copies must stay byte-identical. ## Verification - `hardcoded-path-check.test.sh` **59/0** (15 new regression cases: bare values in all five body shapes incl. JSON-escaped, greedy-prose negative, root-plus-whitespace negative, bare `Shared`) - shellcheck clean (the `SC1003` disable became unnecessary — no trailing backslash remains in the escaped body) - guardrails `0.12.1` → `0.12.2` + CHANGELOG ## Related - Producer finding: handoff-inbox item `20260723-014618-guardrails-hpp-win-repo-pattern-gaps` (Finding 1) - Prior art: #918 / #932 (root broadening), standards#250 (upstream mirror precedent) - Sibling finding: #1094 (non-repo project dir exemption), separate PR Closes #1093 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 (200k context) <noreply@anthropic.com>
Two-lane posture fixes (audit #912): externalize hardcoded assumptions in two guardrails detectors.
W3 - hook-manager bypass detection
block-no-verifymatched onlylefthook*env-var disables, silently missingHUSKY=0and others. Now resolves a configurable prefix set (block_no_verify_hook_manager_prefixesuserConfig; defaultlefthook, husky, pre_commit, simple_git_hooks). Consumer values are reduced to identifier characters before splicing into the regex alternation, so no metacharacter injection.W4 - machine-path checkout roots
hardcoded-path-check's drive-letter-anchored checkout-parent pattern matched onlyX:\repos\..., missingC:\Projects\...(this very repo) andC:\Dev\.... Broadened to also matchProjectsandDev(both capitalizations). A consumer's own checkout root remains caught by the driver's project-root literal scan.Verification
block-no-verify.test.sh83/0 (adds husky/pre_commit/simple_git_hooks + configurability + sanitization cases)hardcoded-path-check.test.sh38/00.9.5->0.9.6+ CHANGELOGRelated
Closes #918