Claude Code permissions in settings.json and settings.local.json, allow, deny and ask rules with real examples
The allow / deny / ask rules and pattern syntax, scope precedence, what settings.local.json is for and how it stays out of git, plus recommended setups for solo and team work.
Contents
By default, Claude Code asks before every file edit and every shell command. That is safe, but it kills your momentum. Granting everything, on the other hand, lets irreversible operations such as rm -rf or git push --force run without a second look.
This article shows how to use the permissions block in settings.json to draw the line, and how the shared settings.json and your personal settings.local.json divide the work.
KEY POINT
What you will learn
- Where settings.json lives and how the scopes override each other
- How to write allow / deny / ask rules and the pattern syntax
- What gets written to settings.local.json, and which git mechanism keeps it out of your commits
- Recommended setups for solo projects and for teams
Where settings.json lives and which scope wins
| Scope | Path | Purpose | In git |
|---|---|---|---|
| User | ~/.claude/settings.json | Defaults for all your projects | No |
| Project | .claude/settings.json | Rules shared with the team | Yes |
| Local | .claude/settings.local.json | Personal additions; where "don't ask again" is saved | No |
| Managed | OS-specific policy location | Rules enforced by an organization (Enterprise) | - |
When several files set the same key, the order from strongest to weakest is managed settings, the command line (claude --settings), project local, shared project, user. Permission rules are the exception: they are evaluated deny, then ask, then allow, and the first match decides. Anything the team wants to forbid therefore belongs in the project deny list, where no personal allow can override it.
The shape of the permissions block
{
"permissions": {
"allow": [
"Read",
"Edit",
"Bash(npm run lint)",
"Bash(npm run test:*)",
"Bash(git diff:*)",
"Bash(git log:*)"
],
"ask": [
"Bash(git push:*)",
"Bash(npm install:*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Bash(rm -rf:*)",
"Bash(git push --force:*)"
],
"defaultMode": "acceptEdits"
}
}
| Key | Meaning |
|---|---|
allow | Rules that run without confirmation |
ask | Rules that always prompt, even if an allow rule also matches |
deny | Rules that are blocked. Highest priority |
defaultMode | What happens to operations that match no rule: default (prompt), acceptEdits (edits run, commands prompt), plan (read-only), bypassPermissions (everything runs; isolated environments only) |
Rule syntax
A rule is ToolName(pattern). Omit the pattern to match the whole tool.
| Rule | Matches |
|---|---|
Bash(npm run test:*) | Any command starting with npm run test (:* is a prefix match) |
Bash(git commit *) | git commit followed by space-separated arguments |
Read(./.env) | Reading .env at the project root |
Edit(src/**) | Editing any file under src |
WebFetch(domain:docs.example.com) | Fetching from that domain |
mcp__github__create_issue | The create_issue tool of the MCP server named github |
mcp__github | Every tool of that MCP server |
Bash patterns match the command string
Bash(rm -rf:*) matches commands that begin with rm -rf. It does not match sudo rm -rf or cd /tmp && rm -rf. Treat deny rules as a safety net and protect files that truly matter with filesystem permissions or a container as well.
settings.json versus settings.local.json
Look inside .claude/ and you may find a settings.local.json next to settings.json. Claude Code generates it, and it is your personal settings file, a different job from the settings.json your team shares.
.claude/settings.json | .claude/settings.local.json | |
|---|---|---|
| Role | Settings shared across the project | Your personal additions |
| In git | Yes, commit it | No, Claude Code keeps it out when it creates the file |
| How it appears | You write it | Created automatically when you choose "don't ask again" |
| Precedence | Lower | Higher (but deny is still evaluated first) |
What "don't ask again" does
When you choose "Yes, and don't ask again" at a permission prompt, Claude Code saves that approval as an allow rule in settings.local.json:
{
"permissions": {
"allow": [
"Bash(npm run test:*)",
"Bash(git diff:*)"
]
}
}
The list grows quietly, so review it with /permissions from time to time and delete what you no longer want. When you wonder why some command runs without a prompt, this file is the first place to look. If you are unsure which files are in effect, claude config list and /status show where the current settings were loaded from.
One useful property: while the file stays untracked, its allow rules apply without the workspace-trust step Claude Code requires for the committed project file.
What belongs where
| Setting | Where it goes |
|---|---|
| Deny rules everyone must follow (no reading secrets, no force push) | settings.json |
| Allow rules that are safe for anyone (lint, tests) | settings.json |
| Shared hooks (format after edit) | settings.json |
Allow rules for your own speed (git commit, npm install) | settings.local.json |
Your own defaultMode, such as acceptEdits | settings.local.json |
| Hooks and environment variables specific to your machine | settings.local.json |
Keeping the team's settings.json on the cautious side and letting individuals loosen things locally is the arrangement that holds up best.
How it stays out of git
This is the part that surprises people. Claude Code does not edit your repository's .gitignore. The first time it writes settings.local.json in a git repository that doesn't already ignore it, it adds **/.claude/settings.local.json to your global git excludes file, so the file stays out of your commits in every repository.
That file is core.excludesFile when your global git config sets it to an absolute or ~-prefixed path; otherwise it is $XDG_CONFIG_HOME/git/ignore, or ~/.config/git/ignore when XDG_CONFIG_HOME is unset.
A global exclude doesn't travel with the repository
Because the pattern lives in your own global excludes, a teammate who clones the repository does not inherit it. If you created the file by hand and Claude Code hasn't written to it yet, nothing has been excluded at all. To cover everyone, add the paths to the repository's .gitignore as well:
.claude/settings.local.json
CLAUDE.local.mdEven though the file is untracked, do not put API keys into a hook's command. Read them from environment variables instead.
Recommended setup, solo projects
Edits run automatically; anything that touches the outside world is confirmed.
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Bash(npm run *)",
"Bash(npx tsc *)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git add:*)",
"Bash(git commit:*)"
],
"ask": [
"Bash(git push:*)",
"Bash(npm install:*)",
"Bash(npm uninstall:*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Bash(rm -rf:*)",
"Bash(git push --force:*)",
"Bash(git reset --hard:*)"
]
}
}
Recommended setup, teams
Keep only two things in the shared .claude/settings.json: deny rules everyone must respect, and allow rules that are safe no matter who runs them. Leave personal preferences to settings.local.json.
{
"permissions": {
"allow": [
"Bash(pnpm lint)",
"Bash(pnpm test:*)",
"Bash(pnpm typecheck)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./infra/secrets/**)",
"Edit(./.github/workflows/**)",
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(rm -rf:*)",
"Bash(curl:*)",
"Bash(wget:*)"
]
}
}
curl and wget are denied to stop arbitrary downloads and data exfiltration. Move them to ask if you need them.
Related settings
A complete example that auto-allows npm scripts, confirms git push and denies force pushes in one file is in Claude Code permissions by example. Permissions decide what runs; hooks let you enforce follow-up work such as running the linter after every edit, which makes wider automation safer: Run lint and format automatically with Claude Code hooks. Keeping secrets out of the model's reach is covered in Stop AI coding tools from reading your API keys and secrets.
Summary
- Settings override in the order user, project, local, and permission rules are evaluated deny, then ask, then allow
- Write rules as
Tool(pattern), for exampleBash(npm run test:*) settings.jsonis shared through git;settings.local.jsonis personal, and "don't ask again" appends to it, so review it with/permissions- Claude Code keeps
settings.local.jsonout of git through your global excludes, not the repository's.gitignore, so add it there too if the whole team should ignore it - Start with
defaultMode: "acceptEdits"plus a short allow list, and put dangerous operations in deny
FAQ
- What is the difference between settings.json and settings.local.json?
- settings.json is shared with the team through git; settings.local.json is personal and kept out of git. When both set the same key the local file wins, except that a deny rule is evaluated first wherever it is written.
- What happens when a command matches both allow and deny?
- deny wins. Rules are evaluated deny, then ask, then allow, so dangerous operations in deny never run even if an allow rule is written too broadly.
- Where do the permissions I grant during a session go?
- Choosing "don't ask again" appends the rule to .claude/settings.local.json. Use the /permissions command to review and edit them.
- Will settings.local.json get committed?
- Not if Claude Code created it. The first time it writes the file in a repository that doesn't already ignore it, it adds the pattern to your global git excludes file, not to the repository's .gitignore. If you created the file by hand, add it to .gitignore yourself.
Questions & answers
Q. When I pick "Yes, and don't ask again" on a permission prompt, how much have I actually allowed? Does it also change the session's permission mode?
The scope depends on the tool type, and the documentation's table answers it directly.
| Tool type | What "Yes, and don't ask again" does |
|---|---|
| Read-only (file reads, Grep) | N/A — it doesn't prompt in the first place |
| Bash commands | Permanently per repository and command |
| File modification (Edit / Write) | Until session end |
| WebFetch | Permanently per repository and domain |
| WebSearch | Permanently per repository |
When the approval does save permanently, it goes to .claude/settings.local.json at the root of the git repository, resolved through worktrees to the main checkout. A file-modification approval is not written to the file at all; as the table says, it lasts until the session ends.
On the permission mode: no official documentation says that "don't ask again" changes it. There is a separate option for that, the third one on the prompt, labelled "Yes, and switch to auto mode", and the docs note it does not appear on every prompt.
You can switch modes with Shift+Tab and list your current rules with /permissions. Those two are how to check whether anything changed unexpectedly.
One behaviour worth knowing: approving a compound command saves a separate rule per subcommand that required approval. Approving git status && npm test saves a rule for npm test, so future npm test calls are recognised regardless of what precedes the &&. At most 5 rules are saved for one compound command.
For a file path, gitignore pattern characters such as [, ] and * are escaped, so the generated rule matches only the literal path you approved. Rules you write yourself are not escaped.
A rule saved from a prompt covers only what the prompt showed
Some prompts offer a one-time approval only. The documentation explains that the "don't ask again" option appears only when the prompt can show you everything it would allow. For anything broader, write the rule yourself in /permissions.
For rule syntax and scope precedence, see Claude Code permissions in settings.json.
Q. In acceptEdits, writes under .git/ are refused even with --add-dir and an explicit allow rule. What am I missing?
Nothing is missing from your configuration. .git is a protected path, and permissions.allow cannot pre-approve one by design. The documentation says so directly:
permissions.allowrules in settings files do not pre-approve protected-path writes. The safety check runs before Claude Code evaluates allow rules from settings.
Because the check runs first, an allow rule such as Edit(.git/**) changes nothing. --add-dir only widens the working directories; it has no bearing on the protected-path check.
The per-mode behavior:
| Mode | Protected-path writes |
|---|---|
default, acceptEdits | Prompted |
plan | Allowed in interactive terminal sessions with bypass permissions available; otherwise routed to the classifier when auto mode is available, and prompted when it isn't |
auto | Routed to the classifier |
dontAsk | Denied |
bypassPermissions | Allowed |
In acceptEdits the write is not auto-approved, which is not the same as forbidden: answer the prompt and it goes through. On a path where no prompt can be shown — dontAsk, or an environment that cannot surface one — that reads as a refusal.
.git is not the only one. The protected directories are:
.git .config/git .vscode .idea .husky .cargo
.devcontainer .yarn .mvn .claude (with a few exceptions)
Protected files are listed too, including .gitconfig, .gitmodules, shell configuration (.bashrc, .zshrc, .envrc and friends), package manager configuration (.npmrc, .yarnrc, bunfig.toml), .pre-commit-config.yaml, .mcp.json and .claude.json.
注意
In a session started with --restricted (v2.1.248 or later), the classifier cannot approve protected-path writes either, so auto mode won't let them through.
If you genuinely need these writes unattended, the only documented route is bypassPermissions mode — and since that skips the protected-path check wholesale, the docs limit it to isolated environments like containers or VMs. Rewriting .git is usually better done through git commands than by letting the Edit tool at it.
For how rules are written and in what order they're evaluated, see Configuring permissions in Claude Code's settings.json.
Q. I configured Claude Code to answer in my language, but it keeps reverting to English. How do I make it stick?
The feature for keeping every response in a given voice is an output style. From the documentation:
An output style is a set of instructions that sets Claude's role, tone, and response format for every response in a session.
You can also put "answer in Korean" in CLAUDE.md, but that file is for what Claude should know about your project, and following it is not guaranteed. The docs' own comparison table routes "every response in a certain voice, length, or format" to an output style.
The usual reason it seems to revert is where the choice is saved. The /output-style command and both menus save your selection to .claude/settings.local.json at the local project level. Open a session in a different project and the setting isn't there. To make it your default everywhere, set it in ~/.claude/settings.json:
{
"outputStyle": "Concise"
}
The value is case-sensitive. The built-in names are Proactive, Concise, Explanatory and Learning; a value that doesn't match exactly, such as explanatory, silently gives you the Default style. (The /output-style command itself ignores case.) A project's settings files take precedence over your user-level value.
For a language instruction specifically, write a custom style as a Markdown file in ~/.claude/output-styles (user), .claude/output-styles (project), or the managed policy directory:
---
name: Korean
description: Always respond in Korean
keep-coding-instructions: true
---
Always write your responses to the user in Korean. Leave code, commands, file paths and verbatim error messages in their original form.
Without keep-coding-instructions: true, a custom style drops Claude Code's built-in software engineering instructions. If you only want to change the language of responses, always include it.
注意
The documentation is explicit that an output style "applies to every response in a session. It's an instruction Claude follows, so nothing enforces it." For something that must happen without exception, use a hook instead.
In the terminal, style files are read at startup, so restart Claude Code after creating or editing one mid-session. For how the settings files rank against each other, see Configuring permissions in Claude Code's settings.json.
Q. I told Claude Code to work only inside one folder, but it edited files under ~/.claude/ without asking. How do I actually enforce the boundary?
Saying "only this folder" in a prompt is not a boundary. What defines Claude Code's reach is the directory you launched it in, plus any directories added to it. From the documentation:
By default, Claude has access to files in the directory where you launched it.
Launch inside a project and everything under it is in scope. Naming a subfolder in your prompt does not narrow that. If files under your home directory were edited, they were in scope by some other route: the session was launched there, a directory was added with --add-dir, or additionalDirectories lists one. /status shows the current set.
To actually stop it, use settings rather than prose:
{
"permissions": {
"deny": [
"Edit(~/.claude/**)",
"Edit(//**)"
],
"ask": [
"Edit"
],
"blockReadsOutsideWorkingDirectories": true
}
}
注意
Path rules are only evaluated for Edit(path) and Read(path). The documentation states that Claude Code checks file permissions against Edit(path) and Read(path) rules only: a path rule written for Write, NotebookEdit or Glob is accepted but never consulted, and you get a startup warning. Write Edit(docs/**), not Write(docs/**).
Rules are evaluated deny → ask → allow, and the first match decides. Specificity does not change that order, so a broad deny beats a narrower allow.
Setting permissions.blockReadsOutsideWorkingDirectories to true makes the file tools refuse the paths it fences in every permission mode. In auto mode, Claude Code offers to turn it on the first time Claude reads outside the working directories.
To narrow the reach itself, launch in the folder you mean, or move the session with /cd <path>. For assembling the whole permissions block, see Configuring permissions in Claude Code's settings.json.
Q. Can a deny rule for .env in Claude Code be overridden by an allow rule in settings.local.json?
No. deny wins over allow regardless of the scope it is written in (user, project or local). With this in the project .claude/settings.json, a broad Read allow in someone's settings.local.json still cannot read .env:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./.env.*)"]
}
}
Note that deny rules govern Claude Code's tool calls. Reading through a shell command such as cat .env is a separate rule, and prefix matches like Bash(cat .env:*) are easy to sidestep. For values that truly matter, keep .env outside the workspace or inject secrets at run time from a secrets manager.
Primary sources
This article was drafted by AI from official documentation and reviewed by the site operator before publishing. Found a mistake? Let us know via the contact page.