Claude Code permissions in settings.json and settings.local.json, allow, deny and ask rules with real examples

Claude Code Published: Updated:

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.

Verified on Oct 10, 2026 These tools change quickly. Please also check the latest official documentation. How articles are researched and verified
Contents
  1. Where settings.json lives and which scope wins
  2. The shape of the permissions block
  3. Rule syntax
  4. settings.json versus settings.local.json
    1. What "don't ask again" does
    2. What belongs where
    3. How it stays out of git
  5. Recommended setup, solo projects
  6. Recommended setup, teams
  7. Related settings
  8. Summary

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

ScopePathPurposeIn git
User~/.claude/settings.jsonDefaults for all your projectsNo
Project.claude/settings.jsonRules shared with the teamYes
Local.claude/settings.local.jsonPersonal additions; where "don't ask again" is savedNo
ManagedOS-specific policy locationRules 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"
  }
}
KeyMeaning
allowRules that run without confirmation
askRules that always prompt, even if an allow rule also matches
denyRules that are blocked. Highest priority
defaultModeWhat 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.

RuleMatches
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_issueThe create_issue tool of the MCP server named github
mcp__githubEvery 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
RoleSettings shared across the projectYour personal additions
In gitYes, commit itNo, Claude Code keeps it out when it creates the file
How it appearsYou write itCreated automatically when you choose "don't ask again"
PrecedenceLowerHigher (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

SettingWhere 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 acceptEditssettings.local.json
Hooks and environment variables specific to your machinesettings.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.md

Even though the file is untracked, do not put API keys into a hook's command. Read them from environment variables instead.

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:*)"
    ]
  }
}

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.

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 example Bash(npm run test:*)
  • settings.json is shared through git; settings.local.json is personal, and "don't ask again" appends to it, so review it with /permissions
  • Claude Code keeps settings.local.json out 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?

Answer by the editorsSource: GitHub anthropics/claude-code Issue #103615

The scope depends on the tool type, and the documentation's table answers it directly.

Tool typeWhat "Yes, and don't ask again" does
Read-only (file reads, Grep)N/A — it doesn't prompt in the first place
Bash commandsPermanently per repository and command
File modification (Edit / Write)Until session end
WebFetchPermanently per repository and domain
WebSearchPermanently 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?

Answer by the editorsSource: GitHub anthropics/claude-code Issue #99809

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.allow rules 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:

ModeProtected-path writes
default, acceptEditsPrompted
planAllowed in interactive terminal sessions with bypass permissions available; otherwise routed to the classifier when auto mode is available, and prompted when it isn't
autoRouted to the classifier
dontAskDenied
bypassPermissionsAllowed

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?

Answer by the editorsSource: GitHub anthropics/claude-code Issue #99494

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?

Answer by the editorsSource: GitHub anthropics/claude-code Issue #99334

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?

Answer by the editorsFrequently searched question

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.