From 4ef41fc5a3d10b44af3728f8647eb1281586e1c8 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:20:54 -0400 Subject: [PATCH 1/3] docs(permission-rule-hygiene): record the undelivered plugin bin/-on-PATH capability The convention's correct pattern rests on a plugin's bin/ being added to the Bash tool's PATH. That is documented upstream but does not happen on Windows / Git Bash, so the bare-name allow rule the convention prescribes has nothing to match and every guarded helper falls through to the auto-mode classifier instead. Record the measured behavior, its harness-wide scope, the two consequences for helper authors, and why a ~/.local/bin shim is not a substitute, so no future helper is designed around a capability that is not there. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SNNCY6Go6K3yNGYYCBFWJw --- .../permission-rule-hygiene/CHANGELOG.md | 6 +++ .../permission-rule-hygiene/README.md | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/docs/conventions/permission-rule-hygiene/CHANGELOG.md b/docs/conventions/permission-rule-hygiene/CHANGELOG.md index e1fea45907..ac0c5aaf98 100644 --- a/docs/conventions/permission-rule-hygiene/CHANGELOG.md +++ b/docs/conventions/permission-rule-hygiene/CHANGELOG.md @@ -4,6 +4,12 @@ Notable changes to the permission-rule-hygiene convention. The convention states anti-patterns; it is enforced by the `claude-config` plugin's `permission-hygiene` skill (checks P1/P2/P3), whose detector and criteria version independently of this document. +## 1.1 — 2026-07-24 + +- Added "Known gap — step 1's plugin `bin/` is not delivered on Windows / Git Bash": the measured + behavior, its harness-wide scope, the two consequences for helper authors, and why a `~/.local/bin` + shim is not a substitute. + ## 1.0 — 2026-07-14 Initial published convention. diff --git a/docs/conventions/permission-rule-hygiene/README.md b/docs/conventions/permission-rule-hygiene/README.md index 5c4d4280bc..2b9ead0e4a 100644 --- a/docs/conventions/permission-rule-hygiene/README.md +++ b/docs/conventions/permission-rule-hygiene/README.md @@ -122,6 +122,43 @@ name narrowly: the operator to add the bare-name rule once to `~/.claude/settings.json`, and never relies on interpreter-wildcard `allowed-tools` for auto-mode-gated actions. +## Known gap — step 1's plugin `bin/` is not delivered on Windows / Git Bash + +The plugin `bin/` half of step 1 is documented but does not hold on this platform, so a helper whose +only permission story is bin/-on-PATH has **no** operative allow rule there. Measured on Windows 11 / +Git Bash, Claude Code **v2.1.219**, with the owning plugin installed at user scope and reported +`enabled` by `claude plugin list`: + +```console +$ which source-control-babysit-merge ; echo $? +which: no source-control-babysit-merge in (...) +1 +$ echo "$PATH" | tr ':' '\n' | grep -i plugins + # no plugin directory of any kind is on PATH +``` + +The absence is **harness-wide, not a packaging defect in one plugin**: a second, unrelated installed +plugin that also ships a `bin/` is equally absent from `PATH`. The files themselves are fine — +committed `100755`, present in the install cache, correct shebangs. The feature also is not +version-gated away: it predates the measured harness. + +Two consequences for anyone writing a guarded helper today: + +- **Invoke it by its bundled path**, the same form the sibling `scripts/` use. That is deterministic + and works now. Resolve `${CLAUDE_PLUGIN_ROOT}` in skill or agent content — it is substituted there, + but it is *not* exported to the Bash tool's own environment, so a raw shell expansion yields an + empty string ([plugins-reference](https://code.claude.com/docs/en/plugins-reference), Environment + variables). +- **Expect no allow rule to cover it.** `bash` is not one of the wrappers Claude Code strips before + matching, so a `bash …` command can only be matched by an interpreter-led rule — which is + anti-pattern 1, dropped on entering auto mode. The call therefore reaches the classifier on every + invocation. Do not design a helper on the assumption that the operator can pre-approve it. + +Until the gap closes upstream, treat step 1's plugin-`bin/` bullet as the intended end state rather +than a capability to build on, on this platform. A `~/.local/bin` shim is **not** a substitute: a +static shim pins a version-numbered install path that changes on every plugin update, and a shim that +resolves the newest directory under the install cache can select an unvetted staging clone. + ## Sources - Auto-mode drop behavior and decision order — [permission-modes](https://code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode) From 9ea7e157484e43a89e84025b402a3cb53ffe1bfc Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:33:40 -0400 Subject: [PATCH 2/3] docs(permission-rule-hygiene): scope the allow-rule claim to what is verified The known-gap section asserted a `bash ` command "can only be matched by an interpreter-led rule". Bash rules accept a wildcard in any position, including leading, so a rule anchored on the wrapper name alone would match it without naming an interpreter. Whether that shape survives auto mode is not documented, so it belongs as a gated candidate rather than as an absolute that overstates the evidence. Also records `env.PATH` in user settings as a real but non-durable lever: it does reach the Bash tool's shell, and carries the same version-pinned path rot as a shim. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SNNCY6Go6K3yNGYYCBFWJw --- .../permission-rule-hygiene/CHANGELOG.md | 5 ++-- .../permission-rule-hygiene/README.md | 27 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/conventions/permission-rule-hygiene/CHANGELOG.md b/docs/conventions/permission-rule-hygiene/CHANGELOG.md index ac0c5aaf98..c3a9368aea 100644 --- a/docs/conventions/permission-rule-hygiene/CHANGELOG.md +++ b/docs/conventions/permission-rule-hygiene/CHANGELOG.md @@ -7,8 +7,9 @@ P1/P2/P3), whose detector and criteria version independently of this document. ## 1.1 — 2026-07-24 - Added "Known gap — step 1's plugin `bin/` is not delivered on Windows / Git Bash": the measured - behavior, its harness-wide scope, the two consequences for helper authors, and why a `~/.local/bin` - shim is not a substitute. + behavior, its harness-wide scope, the two consequences for helper authors, why a `~/.local/bin` + shim and an `env.PATH` settings entry are not substitutes, and the one untested candidate + (leading-wildcard rule) with the specific unknown that gates it. ## 1.0 — 2026-07-14 diff --git a/docs/conventions/permission-rule-hygiene/README.md b/docs/conventions/permission-rule-hygiene/README.md index 2b9ead0e4a..102b6b0e09 100644 --- a/docs/conventions/permission-rule-hygiene/README.md +++ b/docs/conventions/permission-rule-hygiene/README.md @@ -149,15 +149,28 @@ Two consequences for anyone writing a guarded helper today: but it is *not* exported to the Bash tool's own environment, so a raw shell expansion yields an empty string ([plugins-reference](https://code.claude.com/docs/en/plugins-reference), Environment variables). -- **Expect no allow rule to cover it.** `bash` is not one of the wrappers Claude Code strips before - matching, so a `bash …` command can only be matched by an interpreter-led rule — which is - anti-pattern 1, dropped on entering auto mode. The call therefore reaches the classifier on every - invocation. Do not design a helper on the assumption that the operator can pre-approve it. +- **Expect the call to reach the classifier on every invocation.** `bash` is not one of the wrappers + Claude Code strips before matching, so a rule for a `bash …` command has to name `bash` — + making it interpreter-led, i.e. anti-pattern 1, dropped on entering auto mode. Do not assume the + operator can pre-approve the helper. Until the gap closes upstream, treat step 1's plugin-`bin/` bullet as the intended end state rather -than a capability to build on, on this platform. A `~/.local/bin` shim is **not** a substitute: a -static shim pins a version-numbered install path that changes on every plugin update, and a shim that -resolves the newest directory under the install cache can select an unvetted staging clone. +than a capability to build on, on this platform. Two substitutes look attractive and are not: + +- A **`~/.local/bin` shim**: a static shim pins a version-numbered install path that changes on every + plugin update, and a shim that resolves the newest directory under the install cache can select an + unvetted staging clone. +- **`env.PATH` in user settings**, which does reach the Bash tool's shell (`env` is applied "to + subprocesses Claude Code spawns", [settings](https://code.claude.com/docs/en/settings)): it carries + the same version-pinned-path rot, and overriding `PATH` wholesale in settings is its own hazard. + +One candidate is untested rather than rejected. Bash rules accept a wildcard in any position, +including leading, so a rule anchored on the wrapper's own name — `Bash(* *)` — would +syntactically match the bundled-path invocation without naming an interpreter. **Whether that shape +survives auto mode is unverified**: the documented drop list enumerates blanket rules, wildcarded +interpreters, package-manager runners, and `Agent` rules, and says nothing about a leading wildcard +in the command position. Establish that before building on it, and weigh that such a rule matches the +name at *any* path, including an unvetted copy. ## Sources From afa0356ed31a9056ba2a65de51e60cf6d55c422a Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 25 Jul 2026 02:40:33 -0400 Subject: [PATCH 3/3] docs(permission-rule-hygiene): align the known gap with what is actually verified Review found the section asserting more than the evidence carries in three places. The fixed-path `bash ` bullet declared the rule dropped on entering auto mode. The documented drop categories reach the wildcarded-target form but do not clearly reach a fixed-path target, and the audit criteria in this same repo flag that shape on convention grounds rather than as a confirmed drop. Stating it as confirmed left two documents in one repo disagreeing about whether such a grant survives. The practical instruction is unchanged: treat the call as reaching the classifier. The leading-wildcard candidate named a concrete shape that cannot match the invocation it targets: the documented bundled-path form quotes the path, so the character after the wrapper name is a closing quote, not a space. The candidate now records both gates it has to clear - matching the real command string, and surviving auto mode - without asserting a replacement shape that is equally unprobed. Also states that macOS and Linux behavior is unverified, so a non-Windows reader does not read the platform-scoped heading as a clearance, and drops the staging-clone claim about install-cache layout, which was never probed and which the version-pinning argument does not need. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Kb58mL6JnfEos5zrhgn4GD --- .../permission-rule-hygiene/CHANGELOG.md | 2 +- .../permission-rule-hygiene/README.md | 37 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/docs/conventions/permission-rule-hygiene/CHANGELOG.md b/docs/conventions/permission-rule-hygiene/CHANGELOG.md index c3a9368aea..b8b5be8016 100644 --- a/docs/conventions/permission-rule-hygiene/CHANGELOG.md +++ b/docs/conventions/permission-rule-hygiene/CHANGELOG.md @@ -9,7 +9,7 @@ P1/P2/P3), whose detector and criteria version independently of this document. - Added "Known gap — step 1's plugin `bin/` is not delivered on Windows / Git Bash": the measured behavior, its harness-wide scope, the two consequences for helper authors, why a `~/.local/bin` shim and an `env.PATH` settings entry are not substitutes, and the one untested candidate - (leading-wildcard rule) with the specific unknown that gates it. + (leading-wildcard rule) with the two specific unknowns that gate it. ## 1.0 — 2026-07-14 diff --git a/docs/conventions/permission-rule-hygiene/README.md b/docs/conventions/permission-rule-hygiene/README.md index 102b6b0e09..0fb474aec9 100644 --- a/docs/conventions/permission-rule-hygiene/README.md +++ b/docs/conventions/permission-rule-hygiene/README.md @@ -125,9 +125,10 @@ name narrowly: ## Known gap — step 1's plugin `bin/` is not delivered on Windows / Git Bash The plugin `bin/` half of step 1 is documented but does not hold on this platform, so a helper whose -only permission story is bin/-on-PATH has **no** operative allow rule there. Measured on Windows 11 / -Git Bash, Claude Code **v2.1.219**, with the owning plugin installed at user scope and reported -`enabled` by `claude plugin list`: +only permission story is bin/-on-PATH has **no** operative allow rule there. Behavior on macOS and +Linux is unverified — probe there rather than reading a Windows-scoped heading as a clearance. +Measured on Windows 11 / Git Bash, Claude Code **v2.1.219**, with the owning plugin installed at user +scope and reported `enabled` by `claude plugin list`: ```console $ which source-control-babysit-merge ; echo $? @@ -151,26 +152,36 @@ Two consequences for anyone writing a guarded helper today: variables). - **Expect the call to reach the classifier on every invocation.** `bash` is not one of the wrappers Claude Code strips before matching, so a rule for a `bash …` command has to name `bash` — - making it interpreter-led, i.e. anti-pattern 1, dropped on entering auto mode. Do not assume the - operator can pre-approve the helper. + making it interpreter-led, i.e. anti-pattern 1. The documented drop categories clearly reach the + wildcarded-target form (`Bash(bash *)`); whether they reach a fixed-path form + (`Bash(bash :*)`) is not stated, so that shape is an anti-pattern on convention grounds + rather than a confirmed drop. Either way, treat the call as reaching the classifier — do not assume + the operator can pre-approve the helper. Until the gap closes upstream, treat step 1's plugin-`bin/` bullet as the intended end state rather than a capability to build on, on this platform. Two substitutes look attractive and are not: - A **`~/.local/bin` shim**: a static shim pins a version-numbered install path that changes on every - plugin update, and a shim that resolves the newest directory under the install cache can select an - unvetted staging clone. + plugin update. - **`env.PATH` in user settings**, which does reach the Bash tool's shell (`env` is applied "to subprocesses Claude Code spawns", [settings](https://code.claude.com/docs/en/settings)): it carries the same version-pinned-path rot, and overriding `PATH` wholesale in settings is its own hazard. One candidate is untested rather than rejected. Bash rules accept a wildcard in any position, -including leading, so a rule anchored on the wrapper's own name — `Bash(* *)` — would -syntactically match the bundled-path invocation without naming an interpreter. **Whether that shape -survives auto mode is unverified**: the documented drop list enumerates blanket rules, wildcarded -interpreters, package-manager runners, and `Agent` rules, and says nothing about a leading wildcard -in the command position. Establish that before building on it, and weigh that such a rule matches the -name at *any* path, including an unvetted copy. +including leading, so a rule anchored on the wrapper's own name rather than on the interpreter could +reach the bundled-path invocation without naming one. Two things have to be established before +building on it. + +- **It has to match the invocation as actually written.** The documented bundled-path form quotes the + path, so the character following the wrapper name is a closing quote, not a space — a candidate + shaped `Bash(* *)` does not match it, and fails before the auto-mode question is even + reached. Derive the candidate from the exact command string operators are told to run. +- **Whether a leading-wildcard rule survives auto mode is unverified.** The documented drop list + enumerates blanket rules, wildcarded interpreters, package-manager runners, and `Agent` rules, and + says nothing about a leading wildcard in the command position. + +Weigh too that a rule anchored on a bare wrapper name matches that name at *any* path, including an +unvetted copy. ## Sources