Claude Code asks before it runs a command or edits a file. That is the right default, and after a week it is also the thing you click through without reading. Every “Yes, and don’t ask again” you answer is written into a settings file as an allow rule, and once you know the shape of those rules you can write them yourself, add deny rules for the things you never want it to do, and stop being asked about git status.
This post covers the 3 lists, the rule syntax including the one character that catches everyone, the Windows and PowerShell specifics, which file to put a rule in, and the permission modes behind Shift+Tab. Everything is from the current docs, linked at the end. The basics of settings.json itself are in Install Claude Code on Windows, Native and in WSL.
Allow, Ask and Deny
Permissions are a permissions object with up to 3 arrays. Claude Code checks them in a fixed order: deny, then ask, then allow, and the first match wins. A broad deny beats a narrow allow every time, so an allow rule cannot carve an exception out of a deny.
{
"permissions": {
"allow": [
"Bash(git status)",
"Bash(git diff *)",
"Bash(git commit *)",
"PowerShell(Get-ChildItem *)"
],
"ask": [
"Bash(git push *)"
],
"deny": [
"Read(./.env)",
"Read(./secrets/**)",
"Bash(rm -rf *)",
"PowerShell(Remove-Item *)"
]
}
}
Type /permissions in a session to see every rule in force and which file each one came from. You can add and remove rules there while Claude is working, and the change applies from its next tool call.
The Rule Syntax
A rule is Tool or Tool(specifier). A bare tool name matches every use of that tool; as a deny rule it removes the tool from Claude altogether, so "deny": ["WebSearch"] means Claude never sees that it has a search tool.
For Bash the specifier is the command text, with * standing in for any text. The space before a trailing * is part of the rule. Bash(ls *) matches ls and ls -la; Bash(ls*) also matches lsof. The docs table:
Bash(npm run build)matches exactlynpm run build, notnpm run build --watchBash(npm run *)matchesnpm run build,npm run test --watchand barenpm run, notnpm installBash(git log * main)matchesgit log --oneline main, notgit push origin mainBash(git * main)matches every git subcommand, which is why the docs warn against a*before the subcommand
Put the * after the subcommand, because everything before the first * is what limits the rule. Compound commands are split on &&, ||, ; and |, and every part has to be allowed for the whole line to run without a prompt, so Bash(safe-cmd *) does not approve safe-cmd && other-cmd. A deny rule, on the other hand, fires if any part matches, including inside a subshell.
Other tools have their own specifiers. Read(./.env) and Edit(./src/**) take paths. WebFetch(domain:example.com) takes a domain. mcp__github__* allows every tool from an MCP server called github. Agent(Explore) names a subagent and Skill(deploy) a skill.
PowerShell Rules on Windows
With Git for Windows installed, Claude Code’s shell tool is Bash through Git Bash. It also has a PowerShell tool, and rules for that use the same shape with a cmdlet name in place of the command. Aliases are canonicalised before matching, so PowerShell(Get-ChildItem *) also matches gci, ls and dir, and matching is not case sensitive:
{
"permissions": {
"allow": [
"PowerShell(Get-ChildItem *)",
"PowerShell(Get-Content *)",
"PowerShell(git status)"
],
"deny": [
"PowerShell(Remove-Item *)",
"PowerShell(Stop-Service *)"
]
}
}
Claude Code parses the PowerShell AST, so ;, | and on PowerShell 7 the && and || operators split a line into subcommands, each checked on its own. One Windows-specific prompt you will see regardless of rules: any command whose arguments include a UNC path such as \\server\share asks first, because reaching a network path can send your Windows credentials to that host.
Protect Secrets With Read Deny
A Read deny rule stops the file tools, and on a best-effort basis Grep, Glob and @file mentions, from opening a path. The docs’ paste-ready example for a project:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(./config/credentials.json)"
]
}
}
If the project has a .claudeignore file from an older setup, it does nothing; move its entries into Read deny rules. Path patterns use gitignore syntax with 4 forms: ./path or a bare path is relative to the current directory, /path is relative to the settings file that defines it (the project for a project file, ~\.claude for the user file), //path is absolute from the filesystem root, and ~/path is from your home directory. On Windows, paths are normalised to /c/Users/... form before matching, so //c/**/.env matches every .env on C:.
Which File the Rule Goes In
There are 3 settings files, and the same permissions block works in all of them. Project local beats shared project, which beats user:
~\.claude\settings.json: your rules for every project. The git read-only commands you always allow belong here..claude\settings.jsonin the repo: the team’s rules, committed. Allow rules in this file only apply once you have trusted the folder, so a cloned repo cannot pre-approve its own commands..claude\settings.local.json: yours for this project. This is where “Yes, and don’t ask again” writes, and Claude Code adds it to your global git excludes the first time it creates the file.
A quick way to see what you have accumulated:
# every allow rule in your user file (Get-Content "$env:USERPROFILE\.claude\settings.json" -Raw | ConvertFrom-Json).permissions.allow # and in the current project (Get-Content .\.claude\settings.local.json -Raw | ConvertFrom-Json).permissions.allow
Permission Modes
Rules decide individual calls. The mode decides the baseline. Shift+Tab cycles it in a session and the status bar shows which one is on:
- Manual (config value
default): asks for everything except reads and a built-in read-only command set. Shown asmanual mode on. - acceptEdits: file edits and common filesystem commands run without asking; other shell commands still prompt.
- plan: reads and explores, writes nothing until you approve a plan. Plan Mode in Claude Code has the detail.
- auto: no routine prompts; a separate classifier reviews each action before it runs. From version 2.1.283 this is the starting mode for interactive terminal sessions.
- dontAsk: anything that would prompt is denied. For scripts, set with
--permission-mode dontAsk. - bypassPermissions: no checks at all. The docs say containers and VMs only, and it is not in the
Shift+Tabcycle unless you started with the flag.
To start every session in a mode, set permissions.defaultMode in a settings file; to start one session that way, pass claude --permission-mode plan. Note that auto and bypassPermissions only take effect from the user file, not from a project file.
The built-in read-only set runs without a prompt in every mode: ls, cat, pwd, head, tail, grep, find, wc, which, diff, stat, du, cd and the read-only forms of git. If you want to be asked about one of those anyway, an ask rule for it does that.
Other Considerations
A Bash rule matches the text Claude typed, not the program. Bash(rm *) in deny stops rm -rf build and does not stop /bin/rm -rf build or bash -c 'rm -rf build'. The docs are explicit that a deny rule is not a security boundary around a program; for that, use sandboxing (WSL only on Windows) or a PreToolUse hook that inspects the whole command. Claude Code hooks covers the hook.
Redirects are checked as file writes. Bash(git commit *) allows the command, but git log > out.txt is also checked against your Edit rules and the working directory, so a redirect into ~ still prompts.
Rules are enforced by Claude Code, not by the model. An instruction in CLAUDE.md shapes what Claude tries; it does not change what Claude Code allows. CLAUDE.md is for guidance, this file is for the line it cannot cross.
Strict JSON. No comments, no trailing commas. A broken settings file shows a Settings Error at the next start, and in claude -p runs it is silently ignored, which is worse. claude doctor from the shell checks the files without starting a session.
Frequently Asked Questions
Where did my “don’t ask again” answers go?
Into .claude\settings.local.json in the project (or at the repo root if you started in a subfolder), as entries in permissions.allow. For a compound command Claude Code saves one rule per part that needed approval, up to 5.
Why does Bash(git *) feel too broad?
Because it is. It allows git push --force and git -c core.fsmonitor=<script> diff, which runs a program. Allow the subcommands you mean, Bash(git status), Bash(git diff *), Bash(git commit *), and put Bash(git push *) in ask.
Does a deny rule in CLAUDE.md work?
No. CLAUDE.md is context. Only permissions.deny, a permission mode, or a hook is enforced by Claude Code regardless of what the model decides.
How do I block every MCP tool at once?
A deny rule with a tool-name glob: "deny": ["mcp__*"]. Allow rules only accept a glob after a literal server prefix, such as mcp__github__get_*.
Can my team’s committed settings.json approve commands on my clone?
Not until you trust the folder. Allow rules in a repository’s .claude\settings.json wait for the workspace trust dialog, which is what stops a cloned repo from pre-approving its own commands.
Related
- Claude Code on Windows: The Complete Guide
- Install Claude Code on Windows, Native and in WSL
- CLAUDE.md: Project Memory for Claude Code
- Claude Code Hooks: Run a Check After Every Edit
- Plan Mode in Claude Code: Read First, Edit After
- Add an MCP Server to Claude Code
- Run Claude Code From a PowerShell Script With claude -p
- Claude Code docs: configure permissions (rule syntax, Bash and PowerShell rules, Read deny)
- Claude Code docs: permission modes
- Claude Code docs: settings files and precedence
If SQL Server is part of your day job, my current work lives over at sqldba.blog: production DBA scripts, an error library and the SSMS guide.
Leave a Reply