Repository navigation
feat(guardrails): exempt local-only paths from secret-pattern-detection #8761
Description
Problem
secret-pattern-detection (PreToolUse on Write|Edit|NotebookEdit, plugins/guardrails/hooks/hooks.json:37-50) denies (exit 2, plugins/guardrails/hooks/secret-pattern-detection.sh:255) any write that puts a high-confidence token into a file it scans. Its only user control is the on/off switch secret_pattern_detection_enabled (plugins/guardrails/.claude-plugin/plugin.json:21, read at secret-pattern-detection.sh:32). The path allowlist secret_path_allowlisted() (secret-pattern-detection.sh:127-142) is fixed in code.
A user who deliberately keeps credentials in local notes that never get committed has no way to allow that without switching off the whole guard, which also drops the protection that matters: a token written into a tracked source file. Two cases come up:
- A notes directory under home that is not a git repository (for example
~/.claude/references/or~/.claude/.work/). In a session rooted at home, the scope guard clears the project root (secret-pattern-detection.sh:324-369, "home, or an ancestor of home"), so every file anywhere is scanned and these writes are denied. - A gitignored
.work/folder inside a checkout or worktree. The file is under the project root (:371-375), the fixed allowlist does not cover it, so it is scanned and denied.
The guard denies outright; the attended-session "ask" that 0.52.0 added through hooks/refuse.sh covers only block-dangerous-git and block-root-delete-target, not this guard.
Evidence
Verified on origin/main (guardrails 0.55.3):
plugin.jsonhas no path option for this guard. Its precedent isblock_root_delete_target_allowed_roots(plugin.json:105; parser inhooks/block-root-delete-target.sharound:2068-2110; contract in that file's header:66-90): comma-separated absolute directories, compared by real path, strictly-under only, rejecting relative, empty, UNC, glob,..and line-break entries and any entry that resolves to a filesystem root, HOME, or a directory holding HOME.hardcoded-path-check.sh:364already skips a filegit -C "$CLAUDE_PROJECT_DIR" check-ignore -qreports ignored.secret-pattern-detection.shhas no such rung.secret_path_allowlisted()is shared with the GitHub MCP lane (secret-pattern-detection.sh:227,push_files/create_or_update_file), where paths are repo-relative and the content leaves the machine.- The commit-time backstop exists: the
pre-commit-contentgit-hook entry (added 0.52.0) scans the lines a commit adds for secrets, judges force-added gitignored files too, and keeps its own copy of the exemptions (plugins/guardrails/git-hooks/pre-commit-content.sh:66-71,secret_exempt()); it reads no userConfig option today. ~/.claudeon the reporting machine is not a git repository, so a gitignore rung alone cannot cover case 1, and an absolute-root list alone cannot cover case 2 (worktrees are created continually under different roots).- No existing issue covers this (searched open and closed: "secret-pattern-detection exempt", "secret gitignored", "secret-pattern-detection"). Related closed work: guardrails: secret-pattern-detection blocks a temp-tree Write that block-hook-bypass exempts for Bash #4471 (temp-tree parity), fix(guardrails): scan secrets when the project root is home or not a repo #4369 (scan when the project root is home), fix(guardrails): scan NotebookEdit cell source in secret-pattern-detection #4486 (NotebookEdit).
Item claim corrected: the item said the guard refuses writes "under $CLAUDE_PROJECT_DIR". In a repository-rooted session a write to ~/.claude/references/ is outside the project and exits unscanned at :374; it is blocked only in a home-rooted session, where the root is cleared and everything is scanned. The session scratchpad already passes through the temp-tree decline (:389 on).
Not verified: the measured frequency of these denials; the item reports the need, not a count.
Proposed approach
Two opt-in options, both default off/empty, so the default posture is unchanged:
secret_pattern_detection_allowed_roots(string). Comma-separated absolute directories; a Write, Edit or NotebookEdit target whose resolved physical path is strictly under one is not scanned. Same parse and refusal rules asblock_root_delete_target_allowed_roots, without the name-prefix form (no case for it here). Covers case 1.secret_pattern_detection_skip_gitignored(boolean, default false). When true, a target that the git repository containing it reports ignored (git -C <nearest existing ancestor of the target> check-ignore -q <target>) is not scanned. Covers case 2 without listing every worktree. Ask the repository holding the file, not$CLAUDE_PROJECT_DIR, so a home-rooted session works too.
Apply both only in the local lane, after the scope guard and before the content scan, near the secret_path_allowlisted "$ALLOW_FILE" call at :387. Do not add them to secret_path_allowlisted().
Alternatives considered:
- A single path list with glob components (
<root>/*/.work): pattern rules widen the exemption in ways a real-path strictly-under check does not, and it still needs updating for each new worktree root. - Skip gitignored files by default, as
hardcoded-path-checkdoes: changes the default for every consumer, and a gitignored file can still leak through means other than a commit. Leave it opt-in. - Ask when attended instead of deny: does not help subagents or unattended runs, where most writes happen.
Files: plugins/guardrails/hooks/secret-pattern-detection.sh, secret-pattern-detection.test.sh, .claude-plugin/plugin.json (two userConfig entries), README.md (option table and "Option details", regenerated by scripts/sync-plugin-options-docs.py), a changelog fragment and version bump.
Acceptance criteria
- With both options unset, every existing
secret-pattern-detection.test.shcase passes unchanged, and the hook spawns no extra process (no realpath, no git) on any write. - With
secret_pattern_detection_allowed_rootsset to an absolute directory D, a Write of a known test token toD/sub/x.mdexits 0, and the same Write to a sibling of D, to D itself, and to a path that reaches D only through..exits 2. - A symlink under D pointing outside D is scanned (exit 2); a path outside D whose real path, through a link, is under D is not scanned.
- Entries that are relative, empty, UNC, contain a glob character or
.., or resolve to a filesystem root, HOME or an ancestor of HOME grant nothing (the write is scanned). - With
secret_pattern_detection_skip_gitignored=true, a Write of a test token to a gitignored path in a test repository exits 0, the same Write to a tracked or untracked, not-ignored path exits 2, and a target in no repository is scanned. - Both options hold in a home-rooted session (project root cleared) and in a repository-rooted session.
- The GitHub MCP lane (
push_files,create_or_update_file) is unaffected: a path under an allowed root or a gitignored path in the payload is still scanned and denied. -
pre-commit-contentis unaffected: a file under an allowed root that is force-added and committed with a test token still fails the commit. -
scripts/sync-plugin-options-docs.py --check,scripts/check-changelog-fragments.sh --check-required origin/mainand the guardrails test suites pass, on Linux and on Windows Git Bash.
Constraints and gotchas
- This loosens a security guard; the option names and the gitignore rung need the maintainer's decision before implementation (hence
needs-decision). - Hook budget: perf(guardrails): Edit/Write guards spawn 25-87 processes per fire #4390 and perf(guardrails, eol-normalizer, typos-format): about 440 ms of hooks per Edit/Write across three node-to-bash hops #6635 are open on process count and latency per Write/Edit. Resolve the target only when an option is set, and do the root comparison with builtins where possible (see
docs/conventions/hook-budget/README.md). - Windows: real-path comparison must handle drive-letter case,
/c/versusC:/spellings and 8.3 short names; the temp-tree decline (:389-467) and the delete guard's resolver are prior art.~is not expanded in either existing option; keep entries absolute. - Keep the exemptions out of
pre-commit-content.sh'ssecret_exempt(); that check is the backstop that makes a local exemption safe. - That backstop exists only in clones where
/guardrails:setup apply install-git-hooksran, and each clone can decline it (0.52.0 changelog). Both option descriptions should say so. - With
skip_gitignoredon, anything that can write.git/info/excludecan make a path ignored without a tracked diff.hardcoded-path-checkalready trustscheck-ignorethe same way, andpre-commit-contentstill judges a force-added file, but name this in the option docs. - On the reporting machine, two project-scope installs pin older guardrails (0.47.6 and 0.46.8) while user scope is 0.55.3; a fix reaches those sessions only after the pins update.
- Deny messages must not tell the model to set the option (0.48.0 rule: allow-list tokens are phrased as the user's action). If the message names it at all, say only the user can set it, as
block-root-delete-targetdoes. - Users set the options with
/plugin configure guardrails@<marketplace>orclaude plugin install guardrails@<marketplace> --config ...(plugins/guardrails/README.md:1057-1062).
Context
Source: local handoff item 2026-10-08-guardrails-secret-pattern-path-exemption.md (retired into this issue). The reporting user chose to keep credentials unmasked in local, never-committed notes and wants the guard kept on for everything else. Example values on a Windows host: secret_pattern_detection_allowed_roots=C:/Users/<you>/.claude/references,C:/Users/<you>/.claude/.work and secret_pattern_detection_skip_gitignored=true (for .work/ in each checkout and worktree). Related: #4471, #4369, #4486, ADR 0072 (check placement), 0.52.0 changelog (pre-commit-content).
Activity
github-actions commentedon Oct 21, 2026
Proposed work class: work-class: scoped
Basis: automated intake triage; the lanes App applied this class.
Size: Medium
Complexity: Medium
Uncertainty: Known
Flags: none
Blocked by: none
I understood this as a request for two opt-in guardrails options, both off by default.
secret_pattern_detection_allowed_rootswould exempt absolute local directories from secret scanning.secret_pattern_detection_skip_gitignoredwould exempt gitignored targets. Both would apply only in the local Write/Edit lane. The GitHub MCP lane and the commit-timepre-commit-contentbackstop stay unchanged. The approach, files and acceptance criteria are specific enough to start from. I found no duplicate among the open issues. #4390 and #6635 (hook process budget) are related constraints, not blockers.Automated triage, applied by the lanes App.
kyle-sexton commentedon Oct 25, 2026
Released for re-triage under the owner's 2026-10-24 rule: an issue stops for a person only for an intent or vision question, a security call, a host or attended run, or spend outside prepaid credit. The hold label came off and intake triage runs again now; it re-applies a hold, with a numbered question, when one of those applies.
To hold this issue again, add
needs-human(orstatus: needs-decision) and say why in a comment.