[BUG] fewer-permission-prompts drops commands prefixed with environment-variable assignments

Resolved 💬 1 comment Opened Apr 20, 2026 by byliu-labs Closed May 26, 2026

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

  1. Allowlist Bash(uv run:*) in settings.
  2. Run TEST_DATABASE_URL="postgresql://host/db" uv run pytest tests/foo.py.
  3. You get a permission prompt despite Bash(uv run:*) being allowlisted.
  4. Click "allow" — an exact-match rule is added for that full invocation only.
  5. Run a second, near-identical pytest command with a different test path. Prompted again.
  6. Run /fewer-permission-prompts. The skill will ignore these commands because its extraction logic strips env prefixes and sees uv run pytest — already covered.

Root cause

Step 2 of the skill (Extract tool-call frequencies) says:

For Bash calls: parse input.command, take the leading command token (handling sudo, 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

  1. In a project's .claude/settings.local.json, add Bash(uv run:*) under permissions.allow.
  2. Run a command like TEST_DATABASE_URL="postgresql://host/db" uv run pytest tests/foo.py — you get a permission prompt even though Bash(uv run:*) is allowlisted.
  3. Click "Allow" — Claude Code appends an exact-match rule for the full invocation, not a prefix rule.
  4. Run a near-identical pytest command with a different test path. Prompted again.
  5. Invoke the fewer-permission-prompts skill. It scans recent transcripts and reports "nothing to add" for these commands, because its extraction step strips the env-var prefix and sees uv run pytest — then marks the group "already covered" by Bash(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_

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗