ClaudeFolio
Tutorials

Claude Code permission rules: how allow and deny actually match

Edward Kwun··4 min read
Claude Code permission rules: how allow and deny actually match

See more of our writing in your Google results.

Key points

  • Rules are checked deny first, then ask, then allow
  • A narrow allow rule can never override a broad deny
  • Bash(ls *) with a space won't match lsof, Bash(ls*) will
  • File rules only work on Read and Edit, not Write or Glob
  • A leading slash means the settings folder, not your drive root
  • Bash deny rules aren't a security boundary, so use the sandbox

People search for the exact syntax of Claude Code permission rules because the obvious rule often doesn't do what they expect. You write a deny rule for a secrets folder, and it doesn't cover the case you were worried about. Or you allow npm run test and it still asks every time.

Here's how the rules actually match, with the cases that trip people up. All of it comes from Anthropic's permissions documentation.

Where rules go and which one wins

Rules live in a permissions block in a settings file, in three lists: allow, ask, and deny. The files are ~/.claude/settings.json for you everywhere, .claude/settings.json in a project for the team, and .claude/settings.local.json for your own project overrides. When you click "Yes, and don't ask again," Claude Code saves the rule to that last one.

{
  "permissions": {
    "allow": ["Bash(npm run *)", "Bash(git commit *)"],
    "deny": ["Bash(git push *)", "Read(./.env)", "Read(./secrets/**)"]
  }
}

The order is fixed. The docs say: "Rules are evaluated in order: deny, then ask, then allow. The first match in that order determines the outcome, and rule specificity doesn't change the order." So a narrow allow can't punch a hole in a broad deny. If you deny Bash(aws *), allowing Bash(aws s3 ls) does nothing. Managed settings from an organization sit above all of it and can't be overridden.


 

Bash rules and the space before the star

A Bash rule matches the command text, with * standing in for anything. Bash(npm run *) matches npm run build and npm run test --watch, and it doesn't match npm install.

The space matters, and the docs are specific about it. Bash(ls *) needs a space after ls, so it won't match lsof. Bash(ls*) has no space, so it does. A trailing * after a space also matches the bare command, so Bash(git log *) covers plain git log.

Claude Code strips a few wrappers before matching, including timeout, nice, and nohup, so Bash(npm test *) also matches timeout 30 npm test. It splits compound commands on && and || and checks each piece.

What a Bash deny rule is not: a security boundary. The docs warn it "doesn't match the same program invoked in a different form," so a deny on Bash(git push *) won't catch git -C . push, and a deny on curl won't catch curl called by its full path or inside sh -c. For a real block, the docs point to the sandbox or a PreToolUse hook.


 

Read and Edit rules use gitignore patterns

File rules only work on Read and Edit. A rule written for Write, Glob, or NotebookEdit is accepted and then never consulted, and Claude Code warns about it at startup. Use Edit(docs/**) instead of Write(docs/**). A Read deny also blocks edits and writes to the same path.

The path prefix is the part everybody gets wrong. There are four kinds:

  • //path is absolute from the filesystem root, like Read(//Users/alice/secrets/**).
  • ~/path starts at your home directory, like Read(~/.aws/**).
  • /path is relative to where the settings file applies. In project settings that's the project folder, not your drive root.
  • path or ./path is relative to the current directory.

That third one causes the classic mistake. A deny like Read(/secrets/**) in your user settings blocks ~/.claude/secrets/**, not a secrets folder in your projects. For a user-level rule that works in every project, use // or ~/.

A bare filename matches at any depth, the way gitignore works, so Read(.env) and Read(**/.env) are the same rule and catch every .env under the current folder. One * matches inside a single folder, and ** crosses folders. On Windows, paths are converted to forward-slash form before matching, so C:\Users\alice becomes /c/Users/alice, and //**/.env covers every drive.

Read denies reach further than people expect, and not as far as they hope. They apply to Claude's file tools and to shell commands Claude Code recognizes as file reads, like cat, head, and tail. They don't apply to grep -r run over a whole folder or to a Python script that opens the file itself. For secrets that really must stay unread, the answer is the sandbox, or keeping them out of the working tree. We covered the second option in the prompt injection piece.


 

A starting point

This is roughly what I'd drop into a project's shared settings. It skips the prompts on routine commands and keeps secrets and pushes behind a wall that covers the normal cases:

{
  "permissions": {
    "allow": ["Bash(npm run *)", "Bash(git status)", "Bash(git diff *)", "Bash(git commit *)"],
    "deny": ["Bash(git push *)", "Read(.env)", "Read(.env.*)", "Read(secrets/**)"]
  }
}

After you change it, run claude doctor. It validates your settings files and lists rules Claude Code skipped or can't match, which is faster than finding out mid-session. There's more on settings in our settings, hooks, and permissions guide.


 

Sources

Claude Code docs: Configure permissions - Allow, ask, and deny lists and their deny-ask-allow evaluation order, where approved rules are saved, Bash wildcard matching including the space before a trailing star, wrapper stripping and compound commands, the limits of what a Bash rule matches, Read and Edit rules using gitignore syntax with the four path prefixes, the user-settings anchor example, Windows path normalization, which tools file rules apply to, and managed settings precedence.

Found this article useful?

Add ClaudeFolio as a preferred source on Google to see our articles first.

FAQ

How do I stop Claude Code from reading my .env file?
Add "Read(.env)" to the deny list in your settings.json. A bare filename matches at any depth, so it covers every .env under the project. Scripts that open files themselves can still read it, so use the sandbox for a hard block.
Why doesn't my Claude Code allow rule work?
Usually a deny or ask rule matches first, since Claude Code checks deny, then ask, then allow, and specificity doesn't change that. Also check spacing: Bash(ls *) requires a space after ls, while Bash(ls*) does not.
What does a leading slash mean in a Claude Code permission rule?
A single leading slash is relative to where that settings file applies, such as the project folder, not the filesystem root. Use two slashes for an absolute path and ~/ for paths in your home directory.

Related posts

Comments