Claude Academy
intermediate16 min

Tool Permissions: Allow and Deny Patterns

Learning Objectives

  • Understand the permission glob pattern syntax
  • Configure allow and deny lists for Read, Write, and Bash tools
  • Set up MCP tool permissions
  • Use the Tab cycle for runtime permission modes

How Permissions Work

Every time Claude Code wants to read a file, write a file, or run a shell command, it checks the permission system. If the action is explicitly allowed, it proceeds silently. If it's explicitly denied, it's blocked. If neither, Claude asks you for permission.

The permission system uses glob patterns — the same wildcard syntax you know from .gitignore and shell expansion. Mastering these patterns is the key to a Claude Code setup that's both productive and secure.

Permission Pattern Syntax

Read Patterns

Control which files Claude can read:

{

"permissions": {

"allow": [

"Read",

"Read(src/**)",

"Read(docs/*.md)"

],

"deny": [

"Read(.env*)",

"Read(secrets/**)",

"Read(*/.pem)",

"Read(*/.key)"

]

}

}

| Pattern | Matches |

|---|---|

| Read | All file reads (blanket allow) |

| Read(src/**) | Any file under src/ at any depth |

| Read(docs/*.md) | Markdown files directly in docs/ |

| Read(.env*) | .env, .env.local, .env.production |

| Read(*/.pem) | Any .pem file anywhere in the project |

Write Patterns

Control which files Claude can create or modify:

{

"permissions": {

"allow": [

"Write(src/**)",

"Write(tests/**)",

"Write(docs/*/.md)"

],

"deny": [

"Write(package.json)",

"Write(pnpm-lock.yaml)",

"Write(prisma/schema.prisma)",

"Write(.github/**)",

"Write(Dockerfile)"

]

}

}

| Pattern | Matches |

|---|---|

| Write(src/**) | Any file under src/ |

| Write(tests/**) | Any file under tests/ |

| Write(package.json) | Exactly package.json |

| Write(.github/**) | GitHub Actions, PR templates, etc. |

Bash Patterns

Control which shell commands Claude can run:

{

"permissions": {

"allow": [

"Bash(git status)",

"Bash(git log:*)",

"Bash(git diff:*)",

"Bash(git add:*)",

"Bash(git commit:*)",

"Bash(pnpm *)",

"Bash(npm test)",

"Bash(npx vitest *)",

"Bash(docker compose *)"

],

"deny": [

"Bash(rm -rf *)",

"Bash(sudo *)",

"Bash(curl *)",

"Bash(wget *)",

"Bash(git push --force*)",

"Bash(chmod *)",

"Bash(kill *)"

]

}

}

The colon separator in Bash patterns is important:

  • Bash(git log:*) matches git log with any arguments (the colon acts as argument separator)
  • Bash(git log*) matches any command starting with git log — which could include git logout or other unintended commands

Use the colon form for precision:

"Bash(git log:*)"     // git log --oneline, git log -10, etc.

"Bash(git diff:*)" // git diff HEAD, git diff --staged, etc.

"Bash(git branch:*)" // git branch -a, git branch new-feature, etc.

MCP Tool Patterns

Control which MCP tools Claude can invoke:

{

"permissions": {

"allow": [

"mcp__github",

"mcp__github__list_prs",

"mcp__postgres__query"

],

"deny": [

"mcp__postgres__execute",

"mcp__github__delete_repo"

]

}

}

| Pattern | Matches |

|---|---|

| mcp__github | All tools from the GitHub MCP server |

| mcp__github__list_prs | Only the list_prs tool |

| mcp__postgres__execute | Block destructive SQL execution |

The Deny-Over-Allow Rule

When an action matches both an allow and a deny pattern, deny always wins:

{

"permissions": {

"allow": [

"Write(src/**)"

],

"deny": [

"Write(src/generated/**)"

]

}

}

Claude can write to any file under src/ except files under src/generated/. The deny pattern carves out an exception from the broader allow.

This is a deliberate security design. You can grant broad access and then surgically restrict specific paths, knowing the restrictions are absolute.

The Tab Cycle: Runtime Permissions

When Claude encounters an action that's not covered by your allow or deny lists, it prompts you for permission. Instead of typing, you can press Tab to cycle through options:

Claude wants to run: npm install lodash

[Allow Once] → Tab → [Allow Session] → Tab → [Allow Project] → Tab → [Deny]

  • Allow Once: Permits this specific action right now. Gone after this one use.
  • Allow Session: Permits this action for the rest of the session. Resets when you start a new session.
  • Allow Project: Saves the permission to your project settings (.claude/settings.json). Persists across sessions.
  • Deny: Blocks this action.

This is a fast way to build up your permission list organically. As you work, Claude asks about new actions, and you press Tab to the appropriate level. Over time, your settings.json accumulates a well-fitted permission set.

Building a Permission Set

Start Permissive, Narrow Down

For new projects, start with broad allows and see what Claude needs:

{

"permissions": {

"allow": [

"Read",

"Write(src/**)",

"Write(tests/**)",

"Bash(git *)",

"Bash(pnpm *)"

],

"deny": [

"Read(.env*)",

"Bash(rm -rf *)",

"Bash(sudo *)"

]

}

}

As you discover actions you want to restrict, add deny patterns. As you find yourself repeatedly approving the same action, add it to allow.

Lock Down for Production

For production or security-sensitive projects, start restrictive:

{

"permissions": {

"allow": [

"Read(src/**)",

"Read(tests/**)",

"Read(docs/**)",

"Write(src/**)",

"Write(tests/**)",

"Bash(pnpm test)",

"Bash(pnpm lint)",

"Bash(pnpm typecheck)",

"Bash(git status)",

"Bash(git log:*)",

"Bash(git diff:*)"

],

"deny": [

"Read(.env*)",

"Read(secrets/**)",

"Read(*/.pem)",

"Write(package.json)",

"Write(.github/**)",

"Bash(rm *)",

"Bash(sudo *)",

"Bash(curl *)",

"Bash(wget *)",

"Bash(chmod *)",

"Bash(git push *)"

]

}

}

This gives Claude exactly what it needs for development — read source, write source, run tests — while blocking anything that could be dangerous.

Common Permission Recipes

Full-stack TypeScript Project

"allow": [

"Read",

"Write(src/)", "Write(tests/)", "Write(docs/**)",

"Bash(pnpm )", "Bash(npx )", "Bash(git *)",

"Bash(docker compose *)"

],

"deny": [

"Read(.env*)", "Write(package.json)",

"Bash(rm -rf )", "Bash(sudo )", "Bash(curl *)"

]

Python Data Science Project

"allow": [

"Read",

"Write(src/)", "Write(notebooks/)", "Write(tests/**)",

"Bash(python )", "Bash(pip )", "Bash(pytest *)",

"Bash(git )", "Bash(jupyter )"

],

"deny": [

"Read(.env)", "Read(credentials/*)",

"Bash(rm -rf )", "Bash(sudo )", "Bash(curl *)"

]

Monorepo

"allow": [

"Read",

"Write(packages//src/)", "Write(packages//tests/)",

"Bash(pnpm )", "Bash(turbo )", "Bash(git *)"

],

"deny": [

"Read(.env)", "Write(packages//package.json)",

"Bash(rm -rf )", "Bash(sudo )", "Bash(curl *)"

]

Key Takeaway

Tool permissions use glob patterns to control exactly what Claude can read, write, and execute. Deny patterns always override allow patterns — this is a security guarantee. Use the colon separator in Bash patterns (Bash(git log:*)) for precise command matching. Start with broad permissions and narrow down, or start locked-down for sensitive projects. The Tab cycle at permission prompts lets you build your permission set organically as you work.