How do I write an allow rule that is not too wide?
Put * after the subcommand and include the inner command. Use Bash(devbox run npm test) rather than Bash(devbox run *), which also matches devbox run rm -rf ..
Is limiting domains with WebFetch(domain:...) enough?
No. The docs say WebFetch alone does not prevent network access: if Bash is allowed, Claude can still reach any URL with curl. Pair it with denying curl and wget plus the Sandbox network allowlist.
A Bash permission rule looks like "allow or forbid this command", but the official permissions docs repeatedly stress that rules match the command text Claude writes, not the program itself. This article collects the places in those docs where people most easily think something is blocked when it is not. Everything comes from the official permissions page (Configure permissions); I did not test each case myself, so I add no testing claims, and anything marked "my judgment" is my own summary.
The docs warn to put * after the subcommand. Bash(git log *) allows only git log, while Bash(git *) allows every git command. More subtle is Bash(git * main): the docs table says it matches git -c core.fsmonitor=<script> diff main, because the * stands in for the subcommand and every option before it, including -c, which makes git run a program you name. Also, a trailing space-star such as Bash(ls *) does not match lsof, while Bash(ls*) does.
The docs table lists what deny or ask rules stop and what they do not:
Bash(rm *) stops rm -rf build/ but not /bin/rm -rf build/ or bash -c 'rm -rf build/'.Bash(curl *) stops curl https://example.com but not /usr/bin/curl or sh -c 'curl ...'.Bash(git push *) stops git push origin main but not git -C . push origin main, git -c push.default=current push origin main or git 'push' origin main.The docs conclude plainly: a deny or ask rule covers the invocation Claude usually produces and is not a security boundary around the program. For filesystem and network enforcement that does not depend on command text, use the Sandbox; to inspect the full command text with your own logic, use a PreToolUse hook. Commands in the "does not stop" column are decided by your other rules and the permission mode.
&&, ||, ;, |, |&, & and newlines. Bash(safe-cmd *) does not approve safe-cmd && other-cmd; each subcommand must match. Deny and ask rules fire if any subcommand matches, including inside a subshell, a command substitution or a for loop.timeout, time, nice, nohup, stdbuf, command, builtin, and zsh's noglob, so Bash(npm test *) also matches timeout 30 npm test. The list is built in and not configurable.Bash(rm *) deny still catches FOO=bar rm -rf tmp/.direnv exec, devbox run, mise exec, npx and docker exec are not stripped, and Bash(devbox run *) matches whatever follows run, including devbox run rm -rf .. The docs suggest a specific rule that includes the inner command, such as Bash(devbox run npm test).watch, setsid, ionice and flock, and find with -exec, -delete or -fprint, cannot be auto-approved by a prefix rule and still prompt in Manual mode.The docs use Bash(curl http://github.com/ *) as the example: meant to limit curl to GitHub, it misses options placed before the URL, a switch to https, -L following a redirect, and a URL stored in a variable. Their alternatives: deny curl and wget and allow domains through WebFetch(domain:github.com); because deny does not catch path spellings or sh -c, pair it with the sandbox network allowlist when the restriction must hold; or use a PreToolUse hook. The docs also stress that WebFetch alone does not prevent network access: if Bash is allowed, Claude can still reach any URL with curl.
The docs say a deny at any scope beats allow: a managed deny cannot be overridden by --allowedTools, and a user-level deny blocks a project-level allow. A PreToolUse hook does not bypass rules: a matching deny rule still blocks, and a matching ask rule still prompts even if the hook returned "allow". In the other direction, a hook that exits with code 2 stops the call before Permission Rules are evaluated, even when an allow rule would let it through.
This section is my summary, not official advice. Treat Bash rules as a convenience layer that reduces prompts, not as a guarantee that something cannot happen. Things that truly must not happen (deletion, pushing, outbound calls) belong to the sandbox or a hook, and allow rules should be specific enough to include the inner command, avoiding wide shapes like Bash(git *) or Bash(npx *).