From 5f50c3abcb76b2705fb812c7d45cc53a80b4db09 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:59:37 -0400 Subject: [PATCH 1/4] fix(guardrails): gate hardcoded-path repo branch on real non-home checkout The repo-path branch matched PROJECT_ROOT as a bare substring and was never OS-context-suppressed, so a session rooted at the user home hard-denied every absolute path under home (AppData, Desktop, ...) as a checkout-root leak. Engage the branch only when the resolved project root is a real git checkout that is neither home nor a home ancestor; missing home resolution leaves the branch active (fail toward detection). First MUST-FIRE regression case for the branch plus two stay-quiet repros. Guardrails 0.9.1 -> 0.9.2. The percent-env false positive (F2 of the same issue) lives in the upstream-owned pattern library and ships via the standards distribution. Co-Authored-By: Claude Fable 5 --- plugins/guardrails/.claude-plugin/plugin.json | 200 +++++++++--------- plugins/guardrails/CHANGELOG.md | 15 ++ .../guardrails/hooks/hardcoded-path-check.sh | 28 ++- .../hooks/hardcoded-path-check.test.sh | 35 +++ 4 files changed, 177 insertions(+), 101 deletions(-) diff --git a/plugins/guardrails/.claude-plugin/plugin.json b/plugins/guardrails/.claude-plugin/plugin.json index e31ad1d63..3d33d7778 100644 --- a/plugins/guardrails/.claude-plugin/plugin.json +++ b/plugins/guardrails/.claude-plugin/plugin.json @@ -1,102 +1,102 @@ { - "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", - "name": "guardrails", - "version": "0.9.1", - "description": "Eight safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, irreversible git operations (force-push, reset --hard, worktree-wide checkout/restore discards), Bash file-write workarounds that circumvent Write/Edit hooks, (advisory) hallucinated CLI flags, (advisory) un-throttled Workflow fan-out that risks burst 529s, and (advisory) direct git commit/gh pr create calls bypassing this marketplace's own commit/pull-request skills — each independently toggleable.", - "author": { - "name": "Melodic Software", - "email": "info@melodicsoftware.com" - }, - "license": "MIT", - "keywords": [ - "guard", - "security", - "secrets", - "hooks", - "pretooluse", - "git", - "hardcoded-paths", - "cli-flags", - "pull-request" - ], - "userConfig": { - "secret_pattern_detection_enabled": { - "type": "boolean", - "title": "secret-pattern-detection guard", - "description": "Block writes containing high-confidence secret/credential patterns", - "default": true - }, - "hardcoded_path_check_enabled": { - "type": "boolean", - "title": "hardcoded-path-check guard", - "description": "Block writes containing hardcoded machine-specific paths", - "default": true - }, - "block_no_verify_enabled": { - "type": "boolean", - "title": "block-no-verify guard", - "description": "Block git hook-bypass attempts (--no-verify, core.hooksPath=, lefthook disables)", - "default": true - }, - "block_dangerous_git_enabled": { - "type": "boolean", - "title": "block-dangerous-git guard", - "description": "Block irreversible git operations (push --force, reset --hard, clean -f, worktree-wide checkout/restore discards)", - "default": true - }, - "block_hook_bypass_enabled": { - "type": "boolean", - "title": "block-hook-bypass guard", - "description": "Block Bash file-write workarounds that circumvent Write/Edit hook gates", - "default": true - }, - "block_noncanonical_commit_enabled": { - "type": "boolean", - "title": "block-noncanonical-commit guard", - "description": "Block `git commit` that does not pipe its message via `-F -`; --amend, -C/-c, --fixup/--squash, -F , and an in-progress merge/rebase are exempt", - "default": true - }, - "cli_flag_verify_enabled": { - "type": "boolean", - "title": "cli-flag-verify guard", - "description": "Advise on hallucinated CLI flags written to files (never blocks)", - "default": true - }, - "workflow_resilience_check_enabled": { - "type": "boolean", - "title": "workflow-resilience-check guard", - "description": "Advise on un-throttled Workflow fan-out (never blocks)", - "default": true - }, - "flag_commit_pr_skill_bypass_enabled": { - "type": "boolean", - "title": "flag-commit-pr-skill-bypass guard", - "description": "Advise when direct git commit / gh pr create bypasses the source-control skills (never blocks)", - "default": true - }, - "cli_flag_verify_bins": { - "type": "string", - "title": "cli-flag-verify binaries", - "description": "Comma-separated binaries cli-flag-verify scans; empty uses the built-in default set", - "default": "" - }, - "cli_flag_verify_skip_bins": { - "type": "string", - "title": "cli-flag-verify skip list", - "description": "Comma-separated binaries cli-flag-verify must never scan", - "default": "" - }, - "block_dangerous_git_allow": { - "type": "string", - "title": "block-dangerous-git allow-list", - "description": "Comma-separated forms block-dangerous-git permits: push-force, reset-hard, clean-force, checkout-dot, restore-dot, checkout-force; empty blocks all", - "default": "" - }, - "block_noncanonical_commit_allow": { - "type": "string", - "title": "block-noncanonical-commit allow-list", - "description": "Comma-separated form tokens to allow (currently: message-flag, which permits a bare `-m`)", - "default": "" - } - } + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "guardrails", + "version": "0.9.2", + "description": "Eight safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, irreversible git operations (force-push, reset --hard, worktree-wide checkout/restore discards), Bash file-write workarounds that circumvent Write/Edit hooks, (advisory) hallucinated CLI flags, (advisory) un-throttled Workflow fan-out that risks burst 529s, and (advisory) direct git commit/gh pr create calls bypassing this marketplace's own commit/pull-request skills — each independently toggleable.", + "author": { + "name": "Melodic Software", + "email": "info@melodicsoftware.com" + }, + "license": "MIT", + "keywords": [ + "guard", + "security", + "secrets", + "hooks", + "pretooluse", + "git", + "hardcoded-paths", + "cli-flags", + "pull-request" + ], + "userConfig": { + "secret_pattern_detection_enabled": { + "type": "boolean", + "title": "secret-pattern-detection guard", + "description": "Block writes containing high-confidence secret/credential patterns", + "default": true + }, + "hardcoded_path_check_enabled": { + "type": "boolean", + "title": "hardcoded-path-check guard", + "description": "Block writes containing hardcoded machine-specific paths", + "default": true + }, + "block_no_verify_enabled": { + "type": "boolean", + "title": "block-no-verify guard", + "description": "Block git hook-bypass attempts (--no-verify, core.hooksPath=, lefthook disables)", + "default": true + }, + "block_dangerous_git_enabled": { + "type": "boolean", + "title": "block-dangerous-git guard", + "description": "Block irreversible git operations (push --force, reset --hard, clean -f, worktree-wide checkout/restore discards)", + "default": true + }, + "block_hook_bypass_enabled": { + "type": "boolean", + "title": "block-hook-bypass guard", + "description": "Block Bash file-write workarounds that circumvent Write/Edit hook gates", + "default": true + }, + "block_noncanonical_commit_enabled": { + "type": "boolean", + "title": "block-noncanonical-commit guard", + "description": "Block `git commit` that does not pipe its message via `-F -`; --amend, -C/-c, --fixup/--squash, -F , and an in-progress merge/rebase are exempt", + "default": true + }, + "cli_flag_verify_enabled": { + "type": "boolean", + "title": "cli-flag-verify guard", + "description": "Advise on hallucinated CLI flags written to files (never blocks)", + "default": true + }, + "workflow_resilience_check_enabled": { + "type": "boolean", + "title": "workflow-resilience-check guard", + "description": "Advise on un-throttled Workflow fan-out (never blocks)", + "default": true + }, + "flag_commit_pr_skill_bypass_enabled": { + "type": "boolean", + "title": "flag-commit-pr-skill-bypass guard", + "description": "Advise when direct git commit / gh pr create bypasses the source-control skills (never blocks)", + "default": true + }, + "cli_flag_verify_bins": { + "type": "string", + "title": "cli-flag-verify binaries", + "description": "Comma-separated binaries cli-flag-verify scans; empty uses the built-in default set", + "default": "" + }, + "cli_flag_verify_skip_bins": { + "type": "string", + "title": "cli-flag-verify skip list", + "description": "Comma-separated binaries cli-flag-verify must never scan", + "default": "" + }, + "block_dangerous_git_allow": { + "type": "string", + "title": "block-dangerous-git allow-list", + "description": "Comma-separated forms block-dangerous-git permits: push-force, reset-hard, clean-force, checkout-dot, restore-dot, checkout-force; empty blocks all", + "default": "" + }, + "block_noncanonical_commit_allow": { + "type": "string", + "title": "block-noncanonical-commit allow-list", + "description": "Comma-separated form tokens to allow (currently: message-flag, which permits a bare `-m`)", + "default": "" + } + } } diff --git a/plugins/guardrails/CHANGELOG.md b/plugins/guardrails/CHANGELOG.md index 854e8ac9d..f9130c33b 100644 --- a/plugins/guardrails/CHANGELOG.md +++ b/plugins/guardrails/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to the `guardrails` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.9.2] + +### Fixed + +- **`hardcoded-path-check` no longer hard-denies every absolute path under a home-rooted + project.** The repo-path branch matched `PROJECT_ROOT` as a bare substring with no + context gate, so a session rooted at the user home flagged any real path under it + (`AppData\...`, `Desktop\...`) as a checkout-root leak. The branch now engages only + when the resolved project root is a real git checkout that is neither the home + directory nor one of its ancestors; a missing home resolution leaves the branch + active (fail toward detection). Adds the branch's first MUST-FIRE regression case + plus two stay-quiet repros (non-git home project; checkout equal to home). The + sibling percent-env false positive lives in the upstream-owned pattern library and + ships separately via the standards distribution. + ## [0.9.1] ### Fixed diff --git a/plugins/guardrails/hooks/hardcoded-path-check.sh b/plugins/guardrails/hooks/hardcoded-path-check.sh index c387c0886..52d5e32ee 100755 --- a/plugins/guardrails/hooks/hardcoded-path-check.sh +++ b/plugins/guardrails/hooks/hardcoded-path-check.sh @@ -124,6 +124,32 @@ else PROJECT_ROOT="" fi +# issue #389 F1: hpp::scan_text's repo-path branch matches PROJECT_ROOT as a +# literal substring and is never OS-suppressed. That is a valid machine-specific +# marker ONLY when PROJECT_ROOT is a genuine git checkout that is not the user's +# home (nor an ancestor of it). When the project dir is home — e.g. a +# chezmoi-managed home, which is not itself a work tree — or any non-repo dir, +# every absolute path under it contains the root as a substring and would be +# hard-denied. Resolve a SCAN_ROOT the scanner uses for that branch: PROJECT_ROOT +# when it clears both gates, empty otherwise (empty is the lib's documented seam +# to skip the branch). PROJECT_ROOT is unchanged — telemetry below still uses it +# for the repo-relative path. Native Windows exposes home as %USERPROFILE%, not +# $HOME, so fall back to it; a missing home leaves the branch active (fail toward +# detection, never a false negative). +SCAN_ROOT="" +if [[ -n "$PROJECT_ROOT" ]] && + git -C "$PROJECT_ROOT" rev-parse --show-toplevel >/dev/null 2>&1; then + _pr_norm="$(hook::normalize_path "$PROJECT_ROOT")" + _pr_norm="${_pr_norm%/}" + _home_norm="$(hook::normalize_path "${HOME:-${USERPROFILE:-}}")" + _home_norm="${_home_norm%/}" + if [[ -n "$_home_norm" && ( "$_pr_norm" == "$_home_norm" || "$_home_norm" == "$_pr_norm"/* ) ]]; then + SCAN_ROOT="" # project dir is home or an ancestor of home — suppress the branch + else + SCAN_ROOT="$PROJECT_ROOT" + fi +fi + # Repo-relative file for telemetry data.file (best-effort prefix strip). FILE_REL="$FILE" if [[ -n "$PROJECT_ROOT" ]]; then @@ -154,7 +180,7 @@ emit_tel() { hook::emit_telemetry "hardcoded-path-check" "PreToolUse" "$1" "$start" "$data" "${CLAUDE_PROJECT_DIR:-}" } -VIOLATIONS=$(hpp::scan_text "$CONTENT" "$PROJECT_ROOT" "$FILE") +VIOLATIONS=$(hpp::scan_text "$CONTENT" "$SCAN_ROOT" "$FILE") if [[ -n "$VIOLATIONS" ]]; then { printf 'Hardcoded machine-specific path(s) in %s:\n\n' "$FILE" diff --git a/plugins/guardrails/hooks/hardcoded-path-check.test.sh b/plugins/guardrails/hooks/hardcoded-path-check.test.sh index 045a6ee1d..028fbde1b 100755 --- a/plugins/guardrails/hooks/hardcoded-path-check.test.sh +++ b/plugins/guardrails/hooks/hardcoded-path-check.test.sh @@ -55,6 +55,17 @@ assert_exit "Edit new_string → exit 2" 2 "$RC" OUT=$(bash "$HOOK" <<<"$(notebook_json "$FIXTURE" "cd ${MAC_HOME}")" 2>&1); RC=$? assert_exit "NotebookEdit new_source → exit 2" 2 "$RC" +# Repo-path branch: a genuine (non-home) git checkout root hardcoded in content +# is a machine-specific marker and MUST still fire — guards against issue #389 +# F1 over-suppressing the branch entirely (the branch had no prior test). +REPO_REAL="$TEST_TMPDIR/realrepo" +mkdir -p "$REPO_REAL" +git -C "$REPO_REAL" init -q +OUT=$(HOME="$TEST_TMPDIR/elsewhere" CLAUDE_PROJECT_DIR="$REPO_REAL" \ + bash "$HOOK" <<<"$(write_json "$REPO_REAL/notes.txt" "checkout at $REPO_REAL/src")" 2>&1); RC=$? +assert_exit "repo root in real non-home checkout → exit 2" 2 "$RC" +assert_contains "repo root → machine-specific repo message" "$OUT" "Machine-specific repo path" + # ============================ ALLOW (exit 0) ================================ OUT=$(bash "$HOOK" <<<"$(write_json "$FIXTURE" 'echo "hello world"')" 2>&1); RC=$? assert_exit "clean content → exit 0" 0 "$RC" @@ -102,6 +113,30 @@ OUT=$(CLAUDE_PLUGIN_OPTION_HARDCODED_PATH_CHECK_ENABLED=false bash "$HOOK" <<<"$ assert_exit "kill switch off → exit 0" 0 "$RC" assert_silent "kill switch off → no stderr" "$OUT" +# --- issue #389 F1: repo-path branch must not flag paths under a project dir +# that is the user's home (or a non-git dir). The branch matched PROJECT_ROOT +# as a literal substring and was never OS-suppressed. --- + +# F1a — the reported incident: project dir is a NON-git home (e.g. a +# chezmoi-managed home, which is not itself a work tree). A .cmd suppresses the +# Windows-user branch, so ONLY the repo branch could fire; it must stay silent. +HOME_DIR="C:${BS}Users${BS}bob" +HOME_CMD="${HOME_DIR}${BS}Desktop${BS}run.cmd" +HOME_UNDER="${HOME_DIR}${BS}AppData${BS}Local${BS}Docker${BS}img.vhdx" +OUT=$(CLAUDE_PROJECT_DIR="$HOME_DIR" bash "$HOOK" <<<"$(write_json "$HOME_CMD" "copy $HOME_UNDER dst")" 2>&1); RC=$? +assert_exit "F1: non-git home project + path under home → exit 0" 0 "$RC" +assert_silent "F1: non-git home → no stderr" "$OUT" + +# F1b — belt-and-suspenders: project dir IS a git checkout but equals $HOME +# (dotfiles-as-home). The not-$HOME clause suppresses the branch. +GITHOME="$TEST_TMPDIR/githome" +mkdir -p "$GITHOME" +git -C "$GITHOME" init -q +OUT=$(HOME="$GITHOME" CLAUDE_PROJECT_DIR="$GITHOME" \ + bash "$HOOK" <<<"$(write_json "$GITHOME/notes.txt" "path $GITHOME/data/app.bin")" 2>&1); RC=$? +assert_exit "F1: git checkout equal to \$HOME → exit 0" 0 "$RC" +assert_silent "F1: \$HOME checkout → no stderr" "$OUT" + # ============================ TELEMETRY ==================================== TEL="$(mktemp -p "$TEST_TMPDIR")" SINK="$(make_sink "cat >\"$TEL\"")" From 79733e55dd0bfc1962127b1bb30b5ea643d06294 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:01:55 -0400 Subject: [PATCH 2/4] fix(guardrails): restore manifest formatting; drop issue ref from hook comment The version bump had rewritten plugin.json wholesale (2-space indent to tabs via an external formatter that does not own this file); restore the original bytes with only the version field changed. Reword the new hook comment to drop the issue-number back-reference per comment hygiene. Co-Authored-By: Claude Fable 5 --- plugins/guardrails/.claude-plugin/plugin.json | 200 +++++++++--------- .../guardrails/hooks/hardcoded-path-check.sh | 2 +- 2 files changed, 101 insertions(+), 101 deletions(-) diff --git a/plugins/guardrails/.claude-plugin/plugin.json b/plugins/guardrails/.claude-plugin/plugin.json index 3d33d7778..2d16e35d3 100644 --- a/plugins/guardrails/.claude-plugin/plugin.json +++ b/plugins/guardrails/.claude-plugin/plugin.json @@ -1,102 +1,102 @@ { - "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", - "name": "guardrails", - "version": "0.9.2", - "description": "Eight safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, irreversible git operations (force-push, reset --hard, worktree-wide checkout/restore discards), Bash file-write workarounds that circumvent Write/Edit hooks, (advisory) hallucinated CLI flags, (advisory) un-throttled Workflow fan-out that risks burst 529s, and (advisory) direct git commit/gh pr create calls bypassing this marketplace's own commit/pull-request skills — each independently toggleable.", - "author": { - "name": "Melodic Software", - "email": "info@melodicsoftware.com" - }, - "license": "MIT", - "keywords": [ - "guard", - "security", - "secrets", - "hooks", - "pretooluse", - "git", - "hardcoded-paths", - "cli-flags", - "pull-request" - ], - "userConfig": { - "secret_pattern_detection_enabled": { - "type": "boolean", - "title": "secret-pattern-detection guard", - "description": "Block writes containing high-confidence secret/credential patterns", - "default": true - }, - "hardcoded_path_check_enabled": { - "type": "boolean", - "title": "hardcoded-path-check guard", - "description": "Block writes containing hardcoded machine-specific paths", - "default": true - }, - "block_no_verify_enabled": { - "type": "boolean", - "title": "block-no-verify guard", - "description": "Block git hook-bypass attempts (--no-verify, core.hooksPath=, lefthook disables)", - "default": true - }, - "block_dangerous_git_enabled": { - "type": "boolean", - "title": "block-dangerous-git guard", - "description": "Block irreversible git operations (push --force, reset --hard, clean -f, worktree-wide checkout/restore discards)", - "default": true - }, - "block_hook_bypass_enabled": { - "type": "boolean", - "title": "block-hook-bypass guard", - "description": "Block Bash file-write workarounds that circumvent Write/Edit hook gates", - "default": true - }, - "block_noncanonical_commit_enabled": { - "type": "boolean", - "title": "block-noncanonical-commit guard", - "description": "Block `git commit` that does not pipe its message via `-F -`; --amend, -C/-c, --fixup/--squash, -F , and an in-progress merge/rebase are exempt", - "default": true - }, - "cli_flag_verify_enabled": { - "type": "boolean", - "title": "cli-flag-verify guard", - "description": "Advise on hallucinated CLI flags written to files (never blocks)", - "default": true - }, - "workflow_resilience_check_enabled": { - "type": "boolean", - "title": "workflow-resilience-check guard", - "description": "Advise on un-throttled Workflow fan-out (never blocks)", - "default": true - }, - "flag_commit_pr_skill_bypass_enabled": { - "type": "boolean", - "title": "flag-commit-pr-skill-bypass guard", - "description": "Advise when direct git commit / gh pr create bypasses the source-control skills (never blocks)", - "default": true - }, - "cli_flag_verify_bins": { - "type": "string", - "title": "cli-flag-verify binaries", - "description": "Comma-separated binaries cli-flag-verify scans; empty uses the built-in default set", - "default": "" - }, - "cli_flag_verify_skip_bins": { - "type": "string", - "title": "cli-flag-verify skip list", - "description": "Comma-separated binaries cli-flag-verify must never scan", - "default": "" - }, - "block_dangerous_git_allow": { - "type": "string", - "title": "block-dangerous-git allow-list", - "description": "Comma-separated forms block-dangerous-git permits: push-force, reset-hard, clean-force, checkout-dot, restore-dot, checkout-force; empty blocks all", - "default": "" - }, - "block_noncanonical_commit_allow": { - "type": "string", - "title": "block-noncanonical-commit allow-list", - "description": "Comma-separated form tokens to allow (currently: message-flag, which permits a bare `-m`)", - "default": "" - } - } + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "guardrails", + "version": "0.9.2", + "description": "Eight safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, irreversible git operations (force-push, reset --hard, worktree-wide checkout/restore discards), Bash file-write workarounds that circumvent Write/Edit hooks, (advisory) hallucinated CLI flags, (advisory) un-throttled Workflow fan-out that risks burst 529s, and (advisory) direct git commit/gh pr create calls bypassing this marketplace's own commit/pull-request skills — each independently toggleable.", + "author": { + "name": "Melodic Software", + "email": "info@melodicsoftware.com" + }, + "license": "MIT", + "keywords": [ + "guard", + "security", + "secrets", + "hooks", + "pretooluse", + "git", + "hardcoded-paths", + "cli-flags", + "pull-request" + ], + "userConfig": { + "secret_pattern_detection_enabled": { + "type": "boolean", + "title": "secret-pattern-detection guard", + "description": "Block writes containing high-confidence secret/credential patterns", + "default": true + }, + "hardcoded_path_check_enabled": { + "type": "boolean", + "title": "hardcoded-path-check guard", + "description": "Block writes containing hardcoded machine-specific paths", + "default": true + }, + "block_no_verify_enabled": { + "type": "boolean", + "title": "block-no-verify guard", + "description": "Block git hook-bypass attempts (--no-verify, core.hooksPath=, lefthook disables)", + "default": true + }, + "block_dangerous_git_enabled": { + "type": "boolean", + "title": "block-dangerous-git guard", + "description": "Block irreversible git operations (push --force, reset --hard, clean -f, worktree-wide checkout/restore discards)", + "default": true + }, + "block_hook_bypass_enabled": { + "type": "boolean", + "title": "block-hook-bypass guard", + "description": "Block Bash file-write workarounds that circumvent Write/Edit hook gates", + "default": true + }, + "block_noncanonical_commit_enabled": { + "type": "boolean", + "title": "block-noncanonical-commit guard", + "description": "Block `git commit` that does not pipe its message via `-F -`; --amend, -C/-c, --fixup/--squash, -F , and an in-progress merge/rebase are exempt", + "default": true + }, + "cli_flag_verify_enabled": { + "type": "boolean", + "title": "cli-flag-verify guard", + "description": "Advise on hallucinated CLI flags written to files (never blocks)", + "default": true + }, + "workflow_resilience_check_enabled": { + "type": "boolean", + "title": "workflow-resilience-check guard", + "description": "Advise on un-throttled Workflow fan-out (never blocks)", + "default": true + }, + "flag_commit_pr_skill_bypass_enabled": { + "type": "boolean", + "title": "flag-commit-pr-skill-bypass guard", + "description": "Advise when direct git commit / gh pr create bypasses the source-control skills (never blocks)", + "default": true + }, + "cli_flag_verify_bins": { + "type": "string", + "title": "cli-flag-verify binaries", + "description": "Comma-separated binaries cli-flag-verify scans; empty uses the built-in default set", + "default": "" + }, + "cli_flag_verify_skip_bins": { + "type": "string", + "title": "cli-flag-verify skip list", + "description": "Comma-separated binaries cli-flag-verify must never scan", + "default": "" + }, + "block_dangerous_git_allow": { + "type": "string", + "title": "block-dangerous-git allow-list", + "description": "Comma-separated forms block-dangerous-git permits: push-force, reset-hard, clean-force, checkout-dot, restore-dot, checkout-force; empty blocks all", + "default": "" + }, + "block_noncanonical_commit_allow": { + "type": "string", + "title": "block-noncanonical-commit allow-list", + "description": "Comma-separated form tokens to allow (currently: message-flag, which permits a bare `-m`)", + "default": "" + } + } } diff --git a/plugins/guardrails/hooks/hardcoded-path-check.sh b/plugins/guardrails/hooks/hardcoded-path-check.sh index 52d5e32ee..85e84788f 100755 --- a/plugins/guardrails/hooks/hardcoded-path-check.sh +++ b/plugins/guardrails/hooks/hardcoded-path-check.sh @@ -124,7 +124,7 @@ else PROJECT_ROOT="" fi -# issue #389 F1: hpp::scan_text's repo-path branch matches PROJECT_ROOT as a +# hpp::scan_text's repo-path branch matches PROJECT_ROOT as a # literal substring and is never OS-suppressed. That is a valid machine-specific # marker ONLY when PROJECT_ROOT is a genuine git checkout that is not the user's # home (nor an ancestor of it). When the project dir is home — e.g. a From f6a30dd6c1096bc51f962197841f13a73905912c Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:08:47 -0400 Subject: [PATCH 3/4] chore(guardrails): drop remaining issue-number back-references from test comments Two tracker-ref comment-hygiene violations survived the earlier sweep; reworded to describe the behavior instead of the tracker item. Co-Authored-By: Claude Fable 5 --- plugins/guardrails/hooks/hardcoded-path-check.test.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/guardrails/hooks/hardcoded-path-check.test.sh b/plugins/guardrails/hooks/hardcoded-path-check.test.sh index 028fbde1b..5f65b300b 100755 --- a/plugins/guardrails/hooks/hardcoded-path-check.test.sh +++ b/plugins/guardrails/hooks/hardcoded-path-check.test.sh @@ -56,8 +56,8 @@ OUT=$(bash "$HOOK" <<<"$(notebook_json "$FIXTURE" "cd ${MAC_HOME}")" 2>&1); RC=$ assert_exit "NotebookEdit new_source → exit 2" 2 "$RC" # Repo-path branch: a genuine (non-home) git checkout root hardcoded in content -# is a machine-specific marker and MUST still fire — guards against issue #389 -# F1 over-suppressing the branch entirely (the branch had no prior test). +# is a machine-specific marker and MUST still fire — guards against the home-gate +# over-suppressing the branch entirely (the branch had no prior test). REPO_REAL="$TEST_TMPDIR/realrepo" mkdir -p "$REPO_REAL" git -C "$REPO_REAL" init -q @@ -113,9 +113,9 @@ OUT=$(CLAUDE_PLUGIN_OPTION_HARDCODED_PATH_CHECK_ENABLED=false bash "$HOOK" <<<"$ assert_exit "kill switch off → exit 0" 0 "$RC" assert_silent "kill switch off → no stderr" "$OUT" -# --- issue #389 F1: repo-path branch must not flag paths under a project dir -# that is the user's home (or a non-git dir). The branch matched PROJECT_ROOT -# as a literal substring and was never OS-suppressed. --- +# --- Repo-path branch must not flag paths under a project dir that is the +# user's home (or a non-git dir). The branch matched PROJECT_ROOT as a literal +# substring and was never OS-suppressed. --- # F1a — the reported incident: project dir is a NON-git home (e.g. a # chezmoi-managed home, which is not itself a work tree). A .cmd suppresses the From a1a879bfef26eac47bffbbb0e854d0e5c3b117a1 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:39:34 -0400 Subject: [PATCH 4/4] fix(guardrails): compare git toplevel, not project dir, in home gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When home is itself a git checkout (chezmoi dotfiles) and the project dir is a subdirectory of it, rev-parse discovers the parent checkout at home but the gate compared the subdirectory against home — neither equal nor an ancestor — re-enabling the repo-path branch and hard-denying paths under the subdirectory. Capture the discovered toplevel and run the home comparison against it. Adds the discriminating side-door case (fails before, passes after) and a subdir-of-non-home-checkout MUST-FIRE case. Co-Authored-By: Claude Fable 5 --- .../guardrails/hooks/hardcoded-path-check.sh | 44 +++++++++++-------- .../hooks/hardcoded-path-check.test.sh | 37 +++++++++++++++- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/plugins/guardrails/hooks/hardcoded-path-check.sh b/plugins/guardrails/hooks/hardcoded-path-check.sh index 85e84788f..75558d9f5 100755 --- a/plugins/guardrails/hooks/hardcoded-path-check.sh +++ b/plugins/guardrails/hooks/hardcoded-path-check.sh @@ -126,27 +126,33 @@ fi # hpp::scan_text's repo-path branch matches PROJECT_ROOT as a # literal substring and is never OS-suppressed. That is a valid machine-specific -# marker ONLY when PROJECT_ROOT is a genuine git checkout that is not the user's -# home (nor an ancestor of it). When the project dir is home — e.g. a -# chezmoi-managed home, which is not itself a work tree — or any non-repo dir, -# every absolute path under it contains the root as a substring and would be -# hard-denied. Resolve a SCAN_ROOT the scanner uses for that branch: PROJECT_ROOT -# when it clears both gates, empty otherwise (empty is the lib's documented seam -# to skip the branch). PROJECT_ROOT is unchanged — telemetry below still uses it -# for the repo-relative path. Native Windows exposes home as %USERPROFILE%, not -# $HOME, so fall back to it; a missing home leaves the branch active (fail toward +# marker ONLY when PROJECT_ROOT sits inside a genuine git checkout whose ROOT is +# not the user's home (nor an ancestor of it). The comparison is against the +# discovered TOPLEVEL, not PROJECT_ROOT itself: a project dir can be a +# subdirectory of its checkout, and when home is itself a checkout (e.g. +# chezmoi-managed dotfiles) a project dir like $HOME/Desktop would clear a +# PROJECT_ROOT-only home test and wrongly re-enable the branch, hard-denying +# every path under it. When the enclosing checkout is home — or the path is not +# in a checkout at all — skip the branch. Resolve a SCAN_ROOT the scanner uses +# for it: PROJECT_ROOT when the gate passes (the literal being scanned for is +# still the project dir), empty otherwise (empty is the lib's documented seam to +# skip the branch). PROJECT_ROOT is unchanged — telemetry below still uses it for +# the repo-relative path. Native Windows exposes home as %USERPROFILE%, not $HOME, +# so fall back to it; a missing home leaves the branch active (fail toward # detection, never a false negative). SCAN_ROOT="" -if [[ -n "$PROJECT_ROOT" ]] && - git -C "$PROJECT_ROOT" rev-parse --show-toplevel >/dev/null 2>&1; then - _pr_norm="$(hook::normalize_path "$PROJECT_ROOT")" - _pr_norm="${_pr_norm%/}" - _home_norm="$(hook::normalize_path "${HOME:-${USERPROFILE:-}}")" - _home_norm="${_home_norm%/}" - if [[ -n "$_home_norm" && ( "$_pr_norm" == "$_home_norm" || "$_home_norm" == "$_pr_norm"/* ) ]]; then - SCAN_ROOT="" # project dir is home or an ancestor of home — suppress the branch - else - SCAN_ROOT="$PROJECT_ROOT" +if [[ -n "$PROJECT_ROOT" ]]; then + _toplevel="$(git -C "$PROJECT_ROOT" rev-parse --show-toplevel 2>/dev/null)" + if [[ -n "$_toplevel" ]]; then + _tl_norm="$(hook::normalize_path "$_toplevel")" + _tl_norm="${_tl_norm%/}" + _home_norm="$(hook::normalize_path "${HOME:-${USERPROFILE:-}}")" + _home_norm="${_home_norm%/}" + if [[ -n "$_home_norm" && ( "$_tl_norm" == "$_home_norm" || "$_home_norm" == "$_tl_norm"/* ) ]]; then + SCAN_ROOT="" # enclosing checkout is home or an ancestor of home — suppress the branch + else + SCAN_ROOT="$PROJECT_ROOT" + fi fi fi diff --git a/plugins/guardrails/hooks/hardcoded-path-check.test.sh b/plugins/guardrails/hooks/hardcoded-path-check.test.sh index 5f65b300b..02992a222 100755 --- a/plugins/guardrails/hooks/hardcoded-path-check.test.sh +++ b/plugins/guardrails/hooks/hardcoded-path-check.test.sh @@ -66,6 +66,18 @@ OUT=$(HOME="$TEST_TMPDIR/elsewhere" CLAUDE_PROJECT_DIR="$REPO_REAL" \ assert_exit "repo root in real non-home checkout → exit 2" 2 "$RC" assert_contains "repo root → machine-specific repo message" "$OUT" "Machine-specific repo path" +# Project dir a SUBDIR of a genuine non-home checkout: the toplevel is non-home, +# so the branch stays active and a hardcoded project-dir path still fires. Guards +# the toplevel comparison against over-suppressing real checkouts reached via a +# subdirectory. +SUBREPO="$TEST_TMPDIR/subrepo" +mkdir -p "$SUBREPO/pkg" +git -C "$SUBREPO" init -q +OUT=$(HOME="$TEST_TMPDIR/elsewhere2" CLAUDE_PROJECT_DIR="$SUBREPO/pkg" \ + bash "$HOOK" <<<"$(write_json "$SUBREPO/pkg/notes.txt" "at $SUBREPO/pkg/x")" 2>&1); RC=$? +assert_exit "repo subdir of non-home checkout → exit 2" 2 "$RC" +assert_contains "repo subdir → machine-specific repo message" "$OUT" "Machine-specific repo path" + # ============================ ALLOW (exit 0) ================================ OUT=$(bash "$HOOK" <<<"$(write_json "$FIXTURE" 'echo "hello world"')" 2>&1); RC=$? assert_exit "clean content → exit 0" 0 "$RC" @@ -127,16 +139,37 @@ OUT=$(CLAUDE_PROJECT_DIR="$HOME_DIR" bash "$HOOK" <<<"$(write_json "$HOME_CMD" " assert_exit "F1: non-git home project + path under home → exit 0" 0 "$RC" assert_silent "F1: non-git home → no stderr" "$OUT" +# The gate compares the git toplevel against $HOME, so the two must be in the +# same path form. git canonicalizes an MSYS /tmp path to a native Windows path on +# Git Bash, so derive $HOME from git's own --show-toplevel output (an identity on +# Linux, where /tmp is not remapped) — this mirrors a real environment, where +# $HOME and git agree on the path form. + # F1b — belt-and-suspenders: project dir IS a git checkout but equals $HOME -# (dotfiles-as-home). The not-$HOME clause suppresses the branch. +# (dotfiles-as-home). The enclosing-checkout-is-home clause suppresses the branch. GITHOME="$TEST_TMPDIR/githome" mkdir -p "$GITHOME" git -C "$GITHOME" init -q -OUT=$(HOME="$GITHOME" CLAUDE_PROJECT_DIR="$GITHOME" \ +GITHOME_TL="$(git -C "$GITHOME" rev-parse --show-toplevel)" +OUT=$(HOME="$GITHOME_TL" CLAUDE_PROJECT_DIR="$GITHOME" \ bash "$HOOK" <<<"$(write_json "$GITHOME/notes.txt" "path $GITHOME/data/app.bin")" 2>&1); RC=$? assert_exit "F1: git checkout equal to \$HOME → exit 0" 0 "$RC" assert_silent "F1: \$HOME checkout → no stderr" "$OUT" +# F1c — the side door: $HOME is itself a git checkout (chezmoi dotfiles) and the +# project dir is a SUBDIR of home (e.g. $HOME/Desktop). rev-parse discovers the +# parent checkout at home; the home comparison must run against that TOPLEVEL, +# not the subdir — comparing the subdir leaves it neither home nor a home-ancestor +# and re-enables the branch, hard-denying paths under the subdir. +HOMEREPO="$TEST_TMPDIR/homerepo" +mkdir -p "$HOMEREPO/Desktop" +git -C "$HOMEREPO" init -q +HOMEREPO_TL="$(git -C "$HOMEREPO" rev-parse --show-toplevel)" +OUT=$(HOME="$HOMEREPO_TL" CLAUDE_PROJECT_DIR="$HOMEREPO/Desktop" \ + bash "$HOOK" <<<"$(write_json "$HOMEREPO/Desktop/run.txt" "path $HOMEREPO/Desktop/data.bin")" 2>&1); RC=$? +assert_exit "F1: home-is-checkout, project = subdir of home → exit 0" 0 "$RC" +assert_silent "F1: home-checkout subdir → no stderr" "$OUT" + # ============================ TELEMETRY ==================================== TEL="$(mktemp -p "$TEST_TMPDIR")" SINK="$(make_sink "cat >\"$TEL\"")"