Bash permission rules vs docker image-tag colons: silent dead rules, and settings.json rejects patterns --allowedTools accepts
Environment
- Claude Code 2.1.206, macOS (darwin 25.5.0)
- Rules placed in
~/.claude/settings.jsonpermissions.allow
Goal
Allow a narrowly-scoped docker command — docker run --rm --entrypoint sh myorg/build-image:<any tag> -c '<diagnostic command>' — without allowing arbitrary docker run. This is a common shape for anyone whose CI toolchain lives in a pinned container image: the image name should be anchored, the tag needs to vary.
Docker image references contain a colon (myorg/build-image:1.2.3), which collides with the permission-rule syntax's use of :* in several ways. We found four distinct behaviors, three of them surprising. All verified empirically via headless claude -p A/B tests (--allowedTools "Bash(<rule>)" for fast iteration, then bare claude -p with the rule loaded from settings.json for end-to-end confirmation), each with a positive control (the target command) and a negative control (same command with a different image, expected to stay denied).
Findings
1. Bash(docker run --rm --entrypoint sh myorg/build-image:*) — silently never matches
Validates and loads with no warning, but can never match docker run --rm --entrypoint sh myorg/build-image:1.2.3 -c '...'. Per the docs, a trailing :* (≡ trailing *) enforces a word boundary — prefix must be followed by a space or end-of-string — and the image tag's own colon occupies exactly that position.
This is documented behavior, but the failure mode is completely silent: the rule sits in the allowlist looking correct while every matching-looking command prompts. A docs callout for the image-tag/URL-port colon case (or a startup lint for a rule whose literal prefix ends mid-word in typical usage) would save a lot of debugging. It cost us several sessions of restarts and support-ticket-level frustration before we isolated it.
2. Bash(docker run --rm --entrypoint sh myorg/build-image*:*) — passes validation, never matches (bug?)
Mid-pattern * is documented to match any characters including spaces, and :* at the end is documented as a trailing wildcard. Composed, this rule should match the target command (the glob can consume :1.2.3). It does not:
- Loads from settings.json with no validation warning
- Never matches, tested both via settings.json and via
--allowedTools
Either the matcher should support this composition (docs suggest it should) or validation should reject it the way it rejects finding 3. Silently accepting a dead rule is the worst of both.
3. Bash(docker run --rm --entrypoint sh myorg/build-image:* *) — settings.json rejects it, --allowedTools accepts it and matches (inconsistency)
Loaded from ~/.claude/settings.json, this produces:
Settings Warning
└ permissions.allow: Invalid permission rule "Bash(docker run --rm --entrypoint sh myorg/build-image:* *)" was skipped:
The :* pattern must be at the end. Move :* to the end for prefix matching, or use * for wildcard matching
But the same rule passed via --allowedTools is accepted and matches correctly (and respects the negative control). Two consequences:
- Rule validation is inconsistent between rule sources, so testing a rule via
--allowedToolsdoes not prove it will load from settings - The matcher evidently can handle a literal colon anchored before a wildcard — the validator just forbids expressing it in settings. If that restriction were lifted (or an escape syntax added), image references could be scoped precisely.
4. Working fallback: Bash(docker run --rm --entrypoint sh myorg/build-image* *)
This validates, loads, and matches. But because the colon can't be anchored (finding 3), the image name can only be prefix-matched — myorg/build-image-other:tag also matches. Fine when the prefix namespace is org-controlled, imprecise otherwise.
Suggestions
- Docs: add an explicit callout that trailing
:*/*cannot match commands where the prefix boundary falls inside a token (docker image tags,host:port,path:line), with the mid-pattern-glob workaround. - Bug: make finding 2 either match (per composed documented semantics) or fail validation loudly.
- Consistency: apply the same rule validation to
--allowedToolsas to settings files (or document that they differ). - Feature: allow anchoring a literal colon adjacent to a wildcard (validation change or an escape like
\:), so image references can be scoped exactly.
Repro harness
# fast A/B per rule variant:
claude -p "Use the Bash tool exactly once to run this command and show its output: docker run --rm --entrypoint sh myorg/build-image:1.2.3 -c 'echo MATCHED' — if the tool call is denied or blocked, reply with just the word DENIED." \
--allowedTools "Bash(<candidate rule>)" --model haiku --max-turns 2
# end-to-end (validation + load + match) after writing the rule to ~/.claude/settings.json:
claude -p "<same prompt>" --model haiku --max-turns 2
4 Comments
Did not have the GitHub integration installed to tag this as generated with the help of Claude. I don't mean to attribute this to myself alone, it was just an oversight.
Still reproduces on 2.1.238 (Windows 11) — flagging since this is
marked
stale.Finding 3's warning text is unchanged. Two differences from the
original report: mine came from a project
.claude/settings.local.jsonrather than
~/.claude/settings.json, and the:*was *literalcontent* rather than an intended pattern:
``
``Bash(printf '**Scope:** XS9 PhantomSprint\n**Scope:** XS1
StaticVerification\n')
That's the
:**inside a markdown**Scope:**.That second difference suggests the gap is wider than the docker-tag
and URL-port cases here: there appears to be no way to express a
literal
:*in a rule at all. So any approved command whose textcontains that sequence — markdown bold,
sed 's/a:*/b/', image tags,URLs with ports — yields an allowlist entry that can never load, plus a
recurring startup warning.
Confirmed — reproduced all four behaviors on 2.1.233 (Linux) with your exact A/B harness, including the positive and negative controls:
…build-image:*— never matches (the documented prefix boundary lands on the image tag's colon), and there is indeed no warning that the rule can't match.…build-image*:*— accepted with no warning, never matches. Confirmed bug: it should either match per the composed documented semantics or fail validation loudly.…build-image:* *— rejected when loaded from settings but accepted and matching via--allowedTools(negative control stays denied). Confirmed inconsistency; in headless mode the settings skip is completely silent, which makes it worse.…build-image* *— works, with the over-broad prefix caveat you noted.Your suggestions (validation parity between rule sources, reject-or-match for the dead composition, a docs callout for colon-bearing tokens like image tags and host:port, and a way to anchor a literal colon next to a wildcard) all look right to us — leaving this open to track the fix.
🤖 Generated with Claude Code
Related or duplicate issue:
https://github.com/anthropics/claude-code/issues/84969