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 2This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗