Add a permissions block to .claude/settings.json with allow, ask and deny lists. Rules look like Bash(npm run *) or Read(./.env). Claude Code checks deny first, then ask, then allow, and the first match wins, so an allow rule can never cut an exception out of a deny rule. Run /permissions to see every active rule and the file it came from. Rules are enforced by Claude Code, not by the model, so a line in CLAUDE.md cannot grant or revoke access.
Why does Claude Code keep asking for permission?
Out of the box, Claude Code in Manual mode asks before any action that could change something. The permissions docs sort tools like this:
| Tool type | Asks first? | "Yes, and don't ask again" lasts |
|---|---|---|
| File reads, Grep | No, inside the working directory | n/a |
| Bash commands | Yes, except a built-in read-only set | Permanently, per repository and command |
| File edits and writes | Yes | Until the session ends |
| WebFetch | Yes, except preapproved documentation domains | Permanently, per repository and domain |
| WebSearch | Yes | Permanently, per repository |
Each time you pick "Yes, and don't ask again" for a Bash command, Claude Code writes an allow rule into .claude/settings.local.json. Writing rules yourself is faster and gives you the deny side too.
How do allow, ask and deny rules work together?
- Allow lets Claude Code use the tool without asking.
- Ask prompts every time.
- Deny blocks the tool call.
Evaluation order is deny, then ask, then allow. The first match decides, and specificity does not change that. If you deny Bash(aws *), then Bash(aws s3 ls) in the allow list never runs, because the deny matched first. The same holds between ask and allow: a matching ask rule prompts even when a narrower allow rule exists.
Deny rules also behave differently depending on shape. A bare Bash deny removes the tool from Claude's context entirely. A scoped one such as Bash(rm *) keeps the tool and blocks matching calls.
What is the rule syntax?
A rule is Tool or Tool(specifier). With no parentheses it covers every use of the tool. With a specifier it narrows down: Bash(npm run build) matches that exact command, Read(./.env) matches the .env in the current directory, and WebFetch(domain:example.com) matches requests to that domain.
In Bash rules, * matches any text, including spaces. Put it after the subcommand, because everything before the first * is matched as written:
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(git commit *)"
],
"deny": [
"Bash(git push *)"
]
}
}
With this file Claude runs npm scripts and commits without asking, and refuses commands that begin with git push. Three matching details cause most confusion:
| You write | Matches | Does not match |
|---|---|---|
Bash(npm run build) | npm run build | npm run build --watch |
Bash(ls *) | ls -la, ls | lsof |
Bash(ls*) | ls -la, lsof |
The space before a trailing * is part of the rule, so ls * does not match lsof. A trailing * also matches the bare command. The Bash(ls:*) form means the same as Bash(ls *), but only at the end of a pattern.
Claude Code understands shell operators. Bash(safe-cmd *) does not approve safe-cmd && other-cmd, because every subcommand must be allowed. It also strips a fixed set of wrappers (timeout, time, nice, nohup, stdbuf) before matching, so Bash(npm test *) covers timeout 30 npm test. Runners like npx and docker exec are not stripped, so write rules that name both the runner and the inner command.
How do I stop Claude from reading my .env file?
Add Read deny rules. This is the example from the settings docs, and it also shows an allow list:
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(npm run lint)",
"Bash(npm run test *)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)"
]
}
}
Read and Edit rules use gitignore-style patterns with four anchors:
| Pattern | Meaning | Example |
|---|---|---|
//path | Absolute path from the filesystem root | Read(//Users/alice/secrets/**) |
~/path | From your home directory | Read(~/Documents/*.pdf) |
/path | Relative to the settings source | Edit(/src/**/*.ts) |
path or ./path | Relative to the current directory | Read(*.env) |
The one that trips people up: /Users/alice/file is not an absolute path. A single leading slash anchors at the settings source, so use //Users/alice/file. In user settings, /path resolves under ~/.claude, not in your project, so use // or ~/ there.
Other facts worth knowing:
- A
Readdeny rule also blocks Edit and Write on that path, including creating a new file there. - Rules written as
Write(path),Grep(path)orGlob(path)are accepted but do not match. UseEdit(path)andRead(path)instead. - A
.claudeignorefile has no effect. Move its entries into Read deny rules. - Read and Edit deny rules cover Claude's file tools and file commands Claude Code recognizes in Bash, such as
catandsed. They do not cover a Python or Node script that opens files itself. For OS-level blocking, enable the sandbox.
Where do I put the rules?
| File | Who it affects |
|---|---|
~/.claude/settings.json | You, in every project |
.claude/settings.json | Everyone in the repo, if committed |
.claude/settings.local.json | You, in this project only |
| Managed settings | Everyone your organization deploys to |
Precedence, highest first: managed, command line (--settings), project local, shared project, user. List keys such as permissions.allow merge across files instead of overriding, so each file adds entries. Two catches: allow rules in a committed .claude/settings.json only apply after each teammate trusts the folder, while deny and ask rules apply immediately. Edits to permissions reload without a restart, so you can tune rules mid-session.
Which permission mode should I use?
| Mode | What runs without asking |
|---|---|
default (shown as Manual) | Reads only |
acceptEdits | File edits and common filesystem commands such as mkdir, touch, mv, cp in the working directory |
plan | Reads and read-only exploration; no source edits |
auto | Actions run after a background classifier checks them |
dontAsk | Only pre-approved tools; everything else is denied |
bypassPermissions | Everything except a short list no mode approves |
Switch with Shift+Tab, start with a flag, or set a default:
claude --permission-mode plan
claude --permission-mode acceptEdits
# settings.json
{ "permissions": { "defaultMode": "acceptEdits" } }
dontAsk suits locked-down CI runs, because the session never waits for input. bypassPermissions is for containers and VMs only: allow rules have no effect there, deny rules still block, and the docs warn it offers no protection against prompt injection. Setting defaultMode to auto or bypassPermissions in a project file is ignored. Put it in user settings or pass the flag. To forbid these modes for a whole team, set permissions.disableBypassPermissionsMode or permissions.disableAutoMode to "disable" in managed settings.
What can permission rules not stop?
A Bash rule matches the command text Claude writes. It is not a wall around the program. The docs give these examples of what a deny rule misses:
| Deny rule | Stops | Does not stop |
|---|---|---|
Bash(curl *) | curl https://example.com | /usr/bin/curl ..., sh -c 'curl ...' |
Bash(git push *) | git push origin main | git -C . push origin main |
Treat rules as guardrails for normal behavior. For hard limits use the sandbox, which enforces filesystem and network access at the OS level, or a PreToolUse hook that inspects the full command. A hook's deny holds even in bypassPermissions.
How do I check which rules are active?
/permissionslists every rule and the settings file behind it. You can add or remove rules there, and changes apply from Claude's next tool call./statusshows which settings files loaded (theSetting sourcesline).claude doctorlists entries Claude Code rejected, such as a malformed rule. It is the first place to look when a rule seems ignored.
Settings files are strict JSON. A comment or trailing comma makes Claude Code skip the file, so validate after every edit.
FAQ
Does "Yes, and don't ask again" carry over to other projects?
No. For Bash commands it is saved per repository and command, in that project's local settings file.
Can an allow rule override a deny rule?
No. Deny is checked first and an allow rule cannot carve out an exception.
Can I put permission instructions in CLAUDE.md instead?
Instructions there shape what Claude tries to do, but they do not change what Claude Code allows. Use rules, a mode, or a hook.
How do I set up permissions for unattended runs?
Combine --allowedTools with --permission-mode dontAsk. See headless mode and CI.
Sources
Checked against the official docs on November 10, 2026: Configure permissions, Permission modes, Settings files and precedence and Run Claude Code programmatically.
See every current command, flag, and setting in one reference.