Configuring Claude Code Permissions: A Practical Guide
Claude Code permissions decide which actions the agent can take without asking you first, which ones need a nod, and which are blocked outright. Get the configuration right and you stop babysitting every file edit while still keeping guardrails around destructive commands. This guide walks through the permission model, the settings files that control it, and the rule syntax you need to write your own allow and deny lists.
Why claude code permissions matter more than you think
An agentic coding tool is only as useful as the trust boundary you draw around it. If you approve every single tool call by hand, you lose the speed benefit of running an agent at all. If you grant blanket access, you risk a bad command running against your filesystem, your git remote, or a production database. Claude Code permissions exist to let you tune that boundary per project, per tool, and even per shell command pattern, so the agent moves fast on safe, repetitive work and stops to ask before anything risky.
The permission system sits between the model's decision to call a tool and the tool actually executing. Every tool call, whether it is Read, Edit, Bash, WebFetch, or an MCP tool, is checked against your configured rules before it runs. Understanding that pipeline is the key to configuring claude code permissions well, because it tells you exactly where to intervene.
How the permission pipeline works
When Claude Code wants to call a tool, it evaluates the request in this order:
- Deny rules are checked first. If a rule in your
denylist matches, the call is blocked immediately, no prompt, no override. - Allow rules are checked next. If a rule in your
allowlist matches, the call runs without interrupting you. - Ask rules are checked after that. A match here always prompts, even if a broader allow rule might otherwise have matched.
- If nothing matches, Claude Code falls back to its default behavior for that tool, which for most write and execute tools is to ask.
This ordering matters. A deny entry always wins over an allow entry, so you can carve out exceptions to a broad allow rule by adding a narrower deny rule. There is no rule priority beyond the deny-allow-ask order, so specificity is enforced by which list a rule lives in, not by pattern length.
Where claude code permissions are configured
Claude Code reads settings from several files, merged in a defined precedence. Knowing the layers lets you decide whether a rule belongs to you personally, to the whole team, or to a one-off local override.
~/.claude/settings.json user-level, applies to every project
<project>/.claude/settings.json project-level, checked into git, shared with the team
<project>/.claude/settings.local.json project-level, personal, gitignored by defaultProject settings override user settings, and local settings override project settings. This gives you a natural workflow: put broadly safe rules (like allowing Read everywhere) in the user file, put project-specific rules (like allowing npm test in a Node repo) in the checked-in project file, and put anything personal or experimental (like a wider Bash allowance you don't want teammates to inherit) in the local file.
A minimal settings.json for permissions looks like this:
{
"permissions": {
"allow": [
"Read(**)",
"Bash(git status)",
"Bash(git diff*)",
"Bash(npm test)"
],
"ask": [
"Bash(git push*)",
"Edit(**/*.env)"
],
"deny": [
"Bash(rm -rf*)",
"Bash(git push --force*)"
]
}
}Save that at the appropriate level, restart or reload the session, and the new rules take effect immediately for subsequent tool calls.
Rule syntax: tool name plus pattern
Every permission rule has the shape ToolName(pattern). The tool name matches one of Claude Code's built-in tools (Bash, Read, Edit, Write, WebFetch, WebSearch, Glob, Grep, and others) or an MCP tool name in the form mcp__server__tool. The pattern in parentheses depends on the tool.
For file-oriented tools like Read, Edit, and Write, the pattern is a glob matched against the file path:
Read(**) any file, anywhere
Edit(src/**) any file under src/
Edit(**/*.test.ts) any test file, any directory
Write(dist/**) any file under dist/For Bash, the pattern matches against the command string, and a trailing * acts as a prefix wildcard:
Bash(git status) exact command only
Bash(git diff*) any command starting with "git diff"
Bash(npm run *) any npm run script
Bash(docker compose *) any docker compose subcommandBecause Bash patterns are prefix matches rather than full regular expressions, a rule like Bash(git*) is broader than most teams want: it would match git push --force just as readily as git status. Write narrower prefixes and use deny rules to explicitly block the dangerous subcommands you know about.
For network tools like WebFetch, the pattern matches against the domain:
WebFetch(domain:docs.anthropic.com)
WebFetch(domain:*.internal.example.com)For MCP tools, you generally allow or deny the whole tool rather than pattern-matching arguments, since MCP tool inputs are structured JSON rather than a single string:
mcp__github__create_pull_request
mcp__postgres__run_queryManaging claude code permissions from inside a session
You do not have to hand-edit JSON for every change. The /permissions slash command opens an interactive view of your current allow, ask, and deny rules, and lets you add or remove entries without leaving the session. This is the fastest way to fix a permission mistake mid-task: if Claude Code stops to ask about a command you know is safe, you can add a rule on the spot and continue.
There is also a per-prompt shortcut. When Claude Code asks for permission on a specific tool call, the prompt offers an option to remember the decision, which writes a matching rule into your local settings file automatically. Over the first few sessions in a new project, this naturally builds up a working rule set without you writing any JSON by hand.
To inspect what is currently active, including rules inherited from user, project, and local settings combined, run:
claude config listThis prints the merged view, which is useful for debugging why a call was allowed or blocked when you expected the opposite.
A practical starter rule set
Most teams converge on a similar shape: read operations are unrestricted, common build and test commands are allowed, anything that touches git history or leaves the local machine requires a prompt or is denied outright. Here is a set that works well as a starting point for a typical web project and can be checked into .claude/settings.json for the whole team:
{
"permissions": {
"allow": [
"Read(**)",
"Glob(**)",
"Grep(**)",
"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(npm run lint*)",
"Bash(npm test*)",
"Bash(npm run build*)"
],
"ask": [
"Bash(git commit*)",
"Bash(git push*)",
"Bash(npm install*)",
"Edit(**/*.env*)",
"Write(**/*.env*)"
],
"deny": [
"Bash(rm -rf /*)",
"Bash(git push --force*)",
"Bash(git reset --hard*)",
"Bash(curl * | sh*)",
"Read(**/.env*)",
"Read(**/secrets/**)"
]
}
}Notice the deny rules on Read for .env files and a secrets directory. Denying read access to files with credentials is worth doing even though it feels counterintuitive for an assistant whose whole job is reading your code: it stops an accidental prompt-injection scenario, where content the agent reads from a webpage or dependency tries to trick it into exfiltrating secrets, from having anything sensitive to leak in the first place.
Handling destructive commands explicitly
Broad deny patterns for rm, force pushes, and hard resets are worth writing by hand rather than trusting default behavior, because defaults change between versions and because "ask" is not the same as "deny." An ask rule still lets the action through if you approve it, including approving it reflexively while multitasking. For anything that is unrecoverable, a deny entry removes the decision from the moment entirely:
{
"permissions": {
"deny": [
"Bash(rm -rf*)",
"Bash(git push --force*)",
"Bash(git branch -D*)",
"Bash(*DROP TABLE*)",
"Bash(*DELETE FROM*)"
]
}
}If you genuinely need to run one of these commands during a session, run it yourself in a separate terminal rather than loosening the rule for the whole project. The point of a deny list is that it does not bend under time pressure.
Bypassing permissions when you mean to
There are two escape hatches, and they exist for different situations. The first is --dangerously-skip-permissions, a CLI flag that disables the permission system for the whole session. This is appropriate only in an already-isolated environment, such as a throwaway container or a CI job with no access to anything you care about, because it removes every guardrail at once, deny rules included.
The second is scoping a session to a sandboxed working copy, such as a git worktree or a container mount, so that even a fully-permissive session cannot reach outside that boundary. This is the safer pattern when you want an agent to move fast without constant prompts: instead of loosening the rules, shrink the blast radius. Combine a permissive allow list with a filesystem boundary rather than skipping permissions on your primary checkout.
Avoid running --dangerously-skip-permissions against a repository that has your real git remote, real credentials, or a live database connection string in an .env file. The flag does what it says.
Team-shared vs personal permission rules
Split your rules by who benefits from them. Put rules in the checked-in .claude/settings.json when the whole team should get the same behavior on day one of cloning the repo: the standard test and build commands, the deny rules for destructive git operations, the deny rules for secrets files. Put rules in .claude/settings.local.json when they are about your personal workflow: maybe you use a package manager teammates don't, or you want a wider WebFetch allowance for a research task you're doing solo.
This split also matters for onboarding. A new contributor who clones the repo and starts a Claude Code session immediately inherits the project-level guardrails, so they cannot accidentally force-push or read a secrets file on their first afternoon, even before they have opened the settings file themselves.
Debugging a permission rule that isn't matching
The most common mistake is pattern mismatch: a Bash rule written as Bash(npm run test) will not match Bash(npm test), because these are different literal command strings even though they do the same thing. Check the exact command Claude Code is sending by looking at the tool call it attempted, then write the rule against that exact string or a prefix of it.
A second common mistake is forgetting that deny always wins. If you add an allow rule and a tool call is still being blocked or still prompting, check your deny list first, since a broad deny pattern like Bash(git*) in an example config will silently override any narrower allow rule you add later.
A third mistake is scope confusion: editing ~/.claude/settings.json when you meant to edit the project's .claude/settings.json, or vice versa. Run claude config list after any change to confirm the merged rule set looks the way you expect before you trust it in a real session.
Permission patterns by project type
The right claude code permissions setup differs by stack, mostly because the safe-to-allow command list differs. A few starting points:
Node or TypeScript projects typically allow the package manager's read-only and test commands while asking before install or publish steps:
{
"permissions": {
"allow": [
"Bash(npm test*)",
"Bash(npm run lint*)",
"Bash(npm run build*)",
"Bash(npx tsc*)"
],
"ask": [
"Bash(npm install*)",
"Bash(npm publish*)",
"Bash(npm uninstall*)"
]
}
}Python projects usually add the test runner and formatter, and keep package installs on the ask list since they can pull arbitrary code from PyPI:
{
"permissions": {
"allow": [
"Bash(pytest*)",
"Bash(ruff check*)",
"Bash(python -m mypy*)"
],
"ask": [
"Bash(pip install*)",
"Bash(uv add*)"
]
}
}Infrastructure or Terraform repos benefit from allowing read-only plan commands while denying apply and destroy outright, since those commands change real infrastructure and a mistaken approval is expensive:
{
"permissions": {
"allow": [
"Bash(terraform plan*)",
"Bash(terraform validate*)",
"Bash(terraform fmt*)"
],
"deny": [
"Bash(terraform apply*)",
"Bash(terraform destroy*)"
]
}
}Database-adjacent repos, whether through a raw CLI or an MCP database tool, should treat any write-capable query as at minimum an ask, and any destructive statement as a deny:
{
"permissions": {
"ask": [
"Bash(psql*)",
"mcp__postgres__run_query"
],
"deny": [
"Bash(*DROP TABLE*)",
"Bash(*TRUNCATE*)"
]
}
}None of these lists are complete on their own. Treat them as a template you tighten or loosen after watching a few real sessions: if Claude Code keeps stopping to ask about a command you always approve, promote it to allow; if it ran something you wish it had asked about first, add a deny or ask rule the same day rather than making a mental note to do it later.
Testing your permission rules before you trust them
Before relying on a new rule set during real work, run a short session against a disposable branch or a scratch directory and deliberately try to trigger the boundary cases. Ask Claude Code to run the exact commands your deny rules target and confirm they are actually blocked, since a typo in a glob pattern silently produces a rule that matches nothing. A pattern like Bash(git push --force) without a trailing wildcard, for instance, will not catch git push --force-with-lease, which is a real command with real consequences.
It also helps to check that your allow rules are not wider than intended by asking the agent to run something adjacent to what you meant to permit. If you allowed Bash(git diff*) intending to cover diff inspection, confirm it does not also match a command you did not think about, since prefix matching in Bash rules is purely textual and has no understanding of shell syntax, quoting, or command chaining with && or ;.
Once the rule set behaves the way you expect on the scratch branch, commit the project-level file and move on. Revisit it periodically, especially after adding a new build tool, a new deployment script, or a new MCP server to the project, since each of those introduces new tool calls that fall through to the default ask behavior until you write a rule for them.
FAQ
What is the difference between allow, ask, and deny in claude code permissions? allow lets a matching tool call run with no prompt. ask always prompts you before the call runs, even if it would otherwise be permitted. deny blocks the call outright with no way to approve it in the moment. Deny is checked first and always wins over allow or ask.
Where should I put project-wide claude code permissions so my team gets them automatically? Put them in <project>/.claude/settings.json and commit that file to git. It is checked before user-level settings are overridden and after them in precedence, but the key point is that anyone who clones the repo and runs Claude Code inherits these rules without any setup step.
Can I write a Bash permission rule that matches a whole family of commands? Yes, using a trailing wildcard: Bash(npm run *) matches any npm run script. Be careful with broad prefixes like Bash(git*), since a prefix match is not aware of subcommand boundaries and can accidentally include destructive subcommands you did not intend to allow.
Does `--dangerously-skip-permissions` also bypass my deny rules? Yes. The flag disables the entire permission pipeline for the session, including deny rules, so it should only be used in an isolated environment like a disposable container where nothing sensitive is reachable.
How do I stop Claude Code from reading my `.env` file even though it can read everything else in the repo? Add an explicit deny rule ahead of your broad Read allow: Read(**/.env*) in the deny list. Because deny is evaluated before allow, this carves out an exception even under a permissive Read(**) rule.
Do MCP tools use the same permission syntax as built-in tools? Yes, the same allow, ask, and deny lists apply, using the tool's full name in the form mcp__server__tool. Argument-level pattern matching is generally not available for MCP tools, so rules typically allow or deny the tool as a whole rather than matching against specific input values.
Will changing `settings.json` affect a session that is already running? Reload or restart the session after editing settings files on disk so the new rules are picked up. Changes made through the /permissions command inside an active session take effect immediately without a restart, since they are applied to the running session's rule set directly.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
Claude CodeGo from zero to confident with Claude Code, the terminal agent that reads, edits, runs, and verifies real code.