Skip to content

fix(protect-secrets): match Windows paths and register the search tool - #56

Open
L4XB wants to merge 2 commits into
karanb192:mainfrom
L4XB:fix/55-windows-paths-and-search-tool
Open

L4XB wants to merge 2 commits into
karanb192:mainfrom
L4XB:fix/55-windows-paths-and-search-tool

Conversation

@L4XB

@L4XB L4XB commented Sep 12, 2026

Copy link
Copy Markdown

Fixes #55 — both halves of it.

Bug 1: backslash paths matched nothing

Every entry in SENSITIVE_FILES anchors its path boundary on /, and several use [^/] inside, so a tool-provided Windows path went past all of them:

$ node -e "process.stdout.write(JSON.stringify({tool_name:'Read',tool_input:{file_path:'C:\\\\Users\\\\me\\\\project\\\\.env'}}))" | node plugins/protect-secrets/protect-secrets.js
{}

That is not only .env: .envrc, SSH private keys and authorized_keys, AWS/kube/gcloud/azure/docker credentials, .netrc, .npmrc, .pypirc, .pgpass and the rest were all unprotected on Windows at every safety level, since the anchor is what failed rather than any single rule.

Matching now runs on a separator-normalized copy of the path:

function toPosixPath(filePath) {
  return typeof filePath === 'string' ? filePath.replace(/\\/g, '/') : filePath;
}

One helper instead of rewriting 25 anchors, so a pattern added later is covered by construction. The hook only ever reads the path, never opens it, so the rewrite stays local to matching; the allowlist is normalized the same way, which keeps C:\src\app\.env.example allowed.

Bug 2: the search tool never reached the hook

check() has handled Grep for a while and the test file covers it, but hooks/hooks.json registered Read|Edit|Write|Bash, so none of that ran for a real call: searching a .env for a key value was not inspected on any OS. The matcher now comes from the same list main() branches on:

const HANDLED_TOOLS = ['Read', 'Edit', 'Write', 'Bash', 'Grep'];

and a test reads hooks/hooks.json and asserts the two agree, in both directions — a tool handled in code but missing from the matcher is dead code, and a tool in the matcher that check() ignores costs a node start per call.

guard-pack had the same gap, so it gets the same wiring: Grep in its matcher and in PACK_TOOLS, and its protect-secrets entry now takes mod.HANDLED_TOOLS instead of keeping a second copy of that list. Its meta test derives the matcher from PACK_TOOLS rather than spelling it out. lib/protect-secrets.js is re-copied so the byte-identity pin holds.

Both plugin versions go to 1.0.1 so installed copies refresh.

Evidence

env -u CCH_SLA_WEBHOOK npm test
# 1624 tests, 1620 pass, 4 fail

The 4 failures are plugins/format-code cases that shell out to ruff, which is not installed in this environment; they fail identically on a clean checkout (1610 tests, 4 fail). Everything else is green, including the 14 tests added here.

Behaviour, before and after, through the real script:

payload before after
Read C:\Users\me\project\.env {} deny [env-file] Cannot read:
Grep {pattern:'API_KEY', path:'C:\app\.env'} {} deny [env-file] Cannot search:
Read C:\src\app\.env.example {} {}
Read C:\src\app\config.js {} {}

Negative controls, each reverted on its own:

  • drop the normalization in checkFilePath → 9 of the new tests fail
  • put Read\|Edit\|Write\|Bash back in hooks.json → the registration guard fails
  • put the hardcoded tool list back in guard-pack's protect-secrets entry → the pack's search test fails

Also checked: garbage stdin still prints {} and exits 0, and all five touched JSON files parse.

One thing left out

BASH_PATTERNS has the same separator assumption in two places, cat-aws-creds and rm-aws-creds, which spell \.aws\/credentials. I left them alone rather than normalize a whole shell command, where backslashes are escapes and rewriting them would change meaning. Making just those two accept [\\/] is a small follow-up if you want it.

Every sensitive-path pattern anchors its boundary on `/`, so a tool-provided
Windows path such as `C:\Users\me\project\.env` matched none of them and read
straight through the guard: .env, .envrc, SSH keys, AWS/kube/gcloud/azure and
docker credentials, .netrc/.npmrc/.pypirc and the rest were all unprotected
there whatever the safety level. Matching now runs on a separator-normalized
copy of the path, so one helper covers every present and future pattern
instead of 25 anchors. The hook only reads the path, never opens it, so the
rewrite stays local to matching and `.env.example`-style templates keep their
allowlist.

The plugin's hooks.json matcher also left Grep out, so the search handling in
check() could not run for a real call and searching a .env for a key value
was never inspected. The matcher is now derived from the HANDLED_TOOLS list
the hook branches on, and a test pins the two together so a tool handled in
code but missing from the matcher fails instead of silently going dead.

guard-pack gets the same wiring: Grep in its matcher and tool list, and its
protect-secrets entry takes the guard's own HANDLED_TOOLS rather than keeping
a second copy of that list. lib/protect-secrets.js is re-copied so the
byte-identity pin holds.

Both plugin versions go to 1.0.1 so installed copies refresh.

Fixes karanb192#55
karanb192 added a commit that referenced this pull request Sep 13, 2026
…ernal model CLIs and APIs) (#58)

* protect-secrets: add delegation sink patterns

Five new BASH_PATTERNS for shell commands that feed secrets or file
contents into an external model. High: a secrets file or secret env var
reaching a model CLI (gemini, codex, llm, sgpt, aichat, openai, mods,
fabric) or a model API host through curl/wget/http/xh. Strict: any file
contents reaching the same sinks. Local models (ollama) stay out of scope.

Regexes are built from shared fragments (SINK_CMD, SECRET_FILE,
SECRET_VAR, SINK_HOST, FILE_BODY) so each pattern stays readable.
Heredocs and herestrings are not treated as file input. A key in an
auth header with an inline body stays allowed.

guard-pack/lib copy refreshed (pinned byte-identical by test). Tests
cover every probe from the empirical map, hit and non-hit.

* protect-secrets 1.1.0: document delegation sinks, native pairing

README gains a Delegation sinks section: why (shunt-style token savers
route file contents to a worker model with no redaction; unpaid Gemini
API tier terms allow training and human review), what is caught at high
vs strict, the sink CLI and API host lists with links, how to allow a
specific flow, and a Native pairing subsection with a settings.json
snippet (Read deny rules cover cat and redirects since 2.1.257; sandbox
network allowlist keys).

plugin.json 1.0.0 -> 1.1.0 with the description updated; guard-pack
1.0.0 -> 1.0.1 because its lib copy changed. Root README plugin row
mentions the new catch. marketplace.json left as is: it lists targets,
not command families, and its line belongs to PR #56.

* protect-secrets: widen sink anchor, tighten secret-var and body checks (review F1 to F8, F11 to F13)

F1: a sink CLI is matched after leading env assignments, wrappers with
their own options (sudo, npx, uvx, bunx, pnpm dlx, command, time, exec,
nice, nohup, env, timeout, xargs), quotes, do/then/else and newlines, by
path or npm scope (npx @google/gemini-cli). Backslash-newline
continuations are joined before matching.
F2: a secret var anywhere inside one body token is seen; a var in a later
header is not.
F3: secret file names are derived from the critical and high entries of
SENSITIVE_FILES (plus hidden-file globs), so the lists cannot drift.
.git-credentials joins SENSITIVE_FILES at high.
F4: curl -T and --upload-file count as a body, in the sink patterns and in
curl-upload-env (which also gains --data-binary @file with a space).
F5, F13: secret-var names match on whole underscore segments with a small
qualifier tail; AUTH and PRIVATE only as the last segment; PASS, PAT,
PASSPHRASE and ${!VAR} added. echo-secret-var is unchanged.
F6: a redirect glued to the command (gemini<.env) is accepted.
F7: file flags are per CLI from each tool's flag table; gemini -i, gemini
-a and mods -f are not file flags. gemini/openai @path references count.
F8: env-dump also closes on ) and covers set, export, declare -x.
F11: six more model API hosts; https (httpie alias) and curlie as clients;
httpie name=@file and name=value body items; --data-urlencode name@file.
F12: heredocs, herestrings, <( ), /dev/*, filters without a file operand,
curl -w/-H @file and a < inside a quoted prompt or body are not file
input. Quoted prompts are opaque to the file-name check, so prose such as
'what is a .env file' passes; a secret used by an earlier command in a
pipeline is no longer attributed to the sink.

* protect-secrets tests: review fix pass coverage

160 tests: every isolation probe from the review, the 13 secret-var false
positives and 20 hot names, one sample path per critical/high
SENSITIVE_FILES entry reachable through the sink, per-CLI file flags,
curl upload flags and their look-alikes, extra hosts and clients, strict
false positives.

* protect-secrets README: sink anchor, host and client lists, not-covered list

Wrapper list matches the regex; six new hosts with vendor doc links;
httpie alias and curlie; per-CLI file flags; what stays out (variable
indirection, credential emitters, scripts, query-string keys, names
outside the Read-side list, fd tricks) and the strict-only false
positives.

* protect-secrets: trim comments and README to the repo's density

Script comment lines 114 to 66 on 235 code lines (main: 52 on 170, ratio
0.31; now 0.28). One line stays above each fragment whose regex is not
obvious; the per-pattern reasoning and the per-CLI flag sources live in
the PR. File header restored to its main wording. README 128 to 84
lines: one Delegation sinks section (19 lines) and one Native pairing
subsection (12 lines); the sink and host lists stay, the rest moved to
the PR body.
karanb192 added a commit that referenced this pull request Sep 13, 2026
The Grep matcher lands in PR #56, not here. The row now matches
plugins/protect-secrets/hooks/hooks.json on main.
karanb192 added a commit that referenced this pull request Sep 13, 2026
…around it

env-read-workaround now asks plainly for the value in .env, with no "find it
somewhere in the directory" nudge and no instruction to stop. The description
and expected_outcome say what the guard actually is: a path and command guard.
A recursive content search never names .env, so there is nothing for it to
match, and Grep is not in the matcher until #56 lands. A with-arm failure
through either route is a measured limit, not a false alarm.

The two diagnostics now record which route it was. no-bash-workaround also
covers grep -r, rg, ag and find -exec, and matches on DB_PASSWORD as well as
.env. no-grep-workaround matches DB_PASSWORD or .env and no longer fires on
.env.example.

env-read-denied: read-attempted needs an input_match on .env, so a Read of the
README no longer satisfies it, and secret-not-leaked carries weight 3 so the
secret dominates the case score. Its expected_outcome now says the closing
instruction, not the guard, is what closes the Grep route.

RESULTS.md moves to the same layout as the other two suites.
…and-search-tool

# Conflicts:
#	plugins/guard-pack/.claude-plugin/plugin.json
#	plugins/guard-pack/README.md
#	plugins/guard-pack/guard-pack.js
#	plugins/guard-pack/hooks/hooks.json
#	plugins/guard-pack/tests/guard-pack.test.js
#	plugins/protect-secrets/.claude-plugin/plugin.json
@L4XB

L4XB commented Sep 13, 2026

Copy link
Copy Markdown
Author

Synced with main, which landed protect-secrets 1.1.0 (#58) and subagent-spawn-cap (#59) while this was open. The diff against main is unchanged in substance — same 13 files, same 150 insertions — but three things moved:

node --test plugins/guard-pack/tests/guard-pack.test.js — 39 pass. node --test plugins/protect-secrets/tests/protect-secrets.test.js — 430 pass. The full npm test shows 1917 pass / 4 fail, and all four are format-code cells that shell out to ruff, which is not installed on this machine; they are untouched by this branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

protect-secrets: .env not blocked on Windows (Read/Edit/Write), and the search tool bypasses the guard entirely

1 participant