[BUG] fewer-permission-prompts drops commands prefixed with environment-variable assignments
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
The fewer-permission-prompts skill silently drops commands that are prefixed with environment-variable assignments (e.g. FOO=bar cmd ...), claiming they're "already covered" by an existing allowlist rule for the bare command. But Claude Code's Bash permission matcher is a literal prefix matcher and does not strip env-var prefixes — so the user still gets prompted every time. Over months, this accumulates dozens of redundant exact-match rules in settings.local.json that never consolidate.
Reproduction
- Allowlist
Bash(uv run:*)in settings. - Run
TEST_DATABASE_URL="postgresql://host/db" uv run pytest tests/foo.py. - You get a permission prompt despite
Bash(uv run:*)being allowlisted. - Click "allow" — an exact-match rule is added for that full invocation only.
- Run a second, near-identical pytest command with a different test path. Prompted again.
- Run
/fewer-permission-prompts. The skill will ignore these commands because its extraction logic strips env prefixes and seesuv run pytest— already covered.
Root cause
Step 2 of the skill (Extract tool-call frequencies) says:
For Bash calls: parseinput.command, take the leading command token (handlingsudo,timeout, pipes,&&, env-var prefixes). Record the command + first subcommand pair (e.g.git status,gh pr view,ls,cat).
Stripping env prefixes is correct for grouping counts, but the skill then uses the stripped form to check "already covered" against the raw allowlist strings. The matcher's semantics differ from the extraction normalization, so "covered" in the skill means something the real matcher never sees.
Concrete evidence
Scanned 20 recent transcripts in a project that had Bash(uv run:*) allowlisted. Of 13 distinct TEST_DATABASE_URL=... invocations hitting the prompt:
| Variant | Count | Would-be rule |
|---|---|---|
| TEST_DATABASE_URL="postgresql://postgres:test@localhost:55432/test" uv run… | 6 | Bash(TEST_DATABASE_URL="postgresql://…" uv run:*) |
| TEST_DATABASE_URL="postgres://postgres:test@localhost:55432/test" uv run… | 5 | same pattern, different scheme |
| TEST_DATABASE_URL="postgresql://postgres:test@localhost:55432/postgres" uv run… | 1 | same pattern, different db |
| TEST_DATABASE_URL="postgres://postgres:postgres@localhost:55432/postgres" uv run… | 1 | same pattern, different creds |
All 13 were dedup'd away by the skill as "already covered by Bash(uv run:*)" — but none actually matched.
Suggested fix
In the dedup step, test each candidate command against the existing allowlist using the same semantics as the real Bash matcher (literal string with :* / * prefix-match) rather than the normalized-for-grouping form. Concretely: after grouping by stripped-command-token, before dropping a group as "already covered", re-check a sample of the raw commands in that group against the raw allowlist patterns. If none match literally, propose a rule for the group using the common literal prefix (e.g. Bash(TEST_DATABASE_URL="postgresql://…" uv run:*)), not the stripped form.
Alternatively: warn when the skill detects this mismatch (N commands grouped under uv run, but none match Bash(uv run:*) literally) so the user can decide.
Impact
Low-severity but persistent UX issue: users who run env-var-prefixed commands never get consolidated rules, only exact-match accretion. A production settings.local.json in my setup has ~90 entries that could collapse into 4 prefix rules.
Environment
- Claude Code version: 2.1.114
- macOS 25.2.0 (darwin arm64)
What Should Happen?
When the skill decides whether a group of commands is "already covered" by the existing allowlist, it should test against the raw commands using the same semantics as Claude Code's actual Bash matcher — literal prefix match (Bash(prefix:*) / Bash(prefix *)) and exact match, with no env-var stripping. If no raw command in the group literally matches any rule, the skill should propose a prefix rule built from the common literal prefix (e.g. Bash(TEST_DATABASE_URL="postgresql://…" uv run:*)) rather than dropping the group as covered.
Concretely: in step 2 of the skill (frequency extraction), stripping env-var prefixes for grouping is fine, but the subsequent dedup check against existing rules must use the unstripped form, because that's what the matcher sees.
Error Messages/Logs
Scanned 20 recent transcripts in a project that had `Bash(uv run:*)` allowlisted. Of 13 distinct `TEST_DATABASE_URL=…` invocations that hit the permission prompt, none was suggested by the skill — all were mis-classified as "already covered":
| Variant | Count | Rule that would actually cover it |
|---|---|---|
| `TEST_DATABASE_URL="postgresql://postgres:test@localhost:55432/test" uv run…` | 6 | `Bash(TEST_DATABASE_URL="postgresql://…katlas_test" uv run:*)` |
| `TEST_DATABASE_URL="postgres://postgres:test@localhost:55432/test" uv run…` | 5 | same pattern, `postgres://` scheme |
| `TEST_DATABASE_URL="postgresql://postgres:test@localhost:55432/postgres" uv run…` | 1 | same pattern, different db |
| `TEST_DATABASE_URL="postgres://postgres:postgres@localhost:55432/postgres" uv run…` | 1 | same pattern, different creds |
The production `settings.local.json` in this project has ~90 exact-match entries that could collapse into 4 prefix rules using the proposed fix.
Steps to Reproduce
- In a project's
.claude/settings.local.json, addBash(uv run:*)underpermissions.allow. - Run a command like
TEST_DATABASE_URL="postgresql://host/db" uv run pytest tests/foo.py— you get a permission prompt even thoughBash(uv run:*)is allowlisted. - Click "Allow" — Claude Code appends an exact-match rule for the full invocation, not a prefix rule.
- Run a near-identical pytest command with a different test path. Prompted again.
- Invoke the
fewer-permission-promptsskill. It scans recent transcripts and reports "nothing to add" for these commands, because its extraction step strips the env-var prefix and seesuv run pytest— then marks the group "already covered" byBash(uv run:*).
Repeating steps 2–4 over weeks produces dozens of exact-match entries in settings.local.json for every slight variation (different DB URL scheme, creds, test path, pytest flags). The skill never consolidates them.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.114
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
_No response_
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗