Claude Code settings.json Example: Allow, Ask and Deny Rules
A real Claude Code settings.json example: which settings file wins, how allow, ask and deny rules match, why deny wins, and how to keep .env out of reach.
By Matthews WongReviewed on Checked against Claude Code v2.1.296
Key takeaway
Claude Code's settings.json is the JSON file where permission rules, hooks and other settings live. Five levels can set the same key and the highest wins, but rule lists merge across files. Every tool call is checked against deny, then ask, then allow, and the first match decides, so a deny rule always wins and no allow rule can override it.
You will be able to
- Choose the right settings file for a rule and predict which value wins.
- Write allow, ask and deny rules that match what you mean, wildcards included.
- Keep secrets out of Claude's reach with Read deny rules, and know where they stop.
Step through the diagram
Press Play, or step with the arrows. Every step is written out below the diagram.
How Claude Code decides whether a tool call runs
Settings come from five levels where the highest wins, rule lists from every file merge into one set, and each tool call is checked against deny, then ask, then allow, whatever the permission mode.
Reviewed on 13 October 2026 · Claude Code v2.1.296
Keys: ← → step · Home / End · K play or pause
Settings come from five levels, highest first. Every tool call is then checked against the merged rules in a fixed order: deny, then ask, then allow.
For one value, such as defaultMode, the highest level that sets it wins. Here the team's plan beats your personal acceptEdits.
"defaultMode": "plan"Predict the next step Can a higher settings file delete a lower file's deny rule? (Yes, the higher level wins · No, rule lists merge into one set)Rule lists work differently. allow, ask and deny entries from every file are combined into one set, so no file can delete another file's rules.
git push origin main matches no deny rule, then matches the ask rule, so Claude Code asks you first, even if an allow rule also matches.
"ask": ["Bash(git push *)"]Predict the next step Claude tries to read .env. Which part stops it? (The Read(./.env) deny rule · The .claudeignore file · Nothing: Claude Code asks you first)Reading .env matches the deny rule first, so the call is blocked. A .claudeignore file has no effect, and no allow rule can override a deny.
"deny": ["Read(./.env)"]npm run lint passes deny and ask, then matches Bash(npm run *), so it runs without a prompt. In Manual mode, npm install matches nothing and still asks.
"allow": ["Bash(npm run *)"]Shift+Tab cycles permission modes. Plan mode lets Claude read and propose, but edits nothing until you approve. Deny rules still block in every mode.
claude --permission-mode plan
Which Claude Code settings file wins?
Claude Code reads settings from four files, plus managed settings from your organisation. ~/.claude/settings.json is yours in every project. .claude/settings.json is the shared project file, committed so teammates get the same permissions and hooks. .claude/settings.local.json is yours in this project only, and Claude Code keeps it out of git when it creates it.
Read more (2)Show less
When the same key is set twice, the highest level wins: managed, then a --settings flag for one session, then project local, then shared project, then user. List keys are the exception. permissions.allow, ask and deny from every file are combined, so each file adds rules without removing another file's. Run /status to see which files loaded, and /permissions to see every rule beside the file it came from.
Choosing "Yes, and don't ask again" on a Bash prompt saves an allow rule to .claude/settings.local.json at the repository root. That approval is personal: a teammate never sees it. In this repo, each contributor's approvals collect there, while the committed file carries only what everyone needs.
How do allow, ask and deny rules match a tool call?
A rule is a tool name, optionally with a specifier in parentheses. Bash(npm run build) matches that exact command, Read(./.env) matches reading that file, and a bare Bash matches every shell command. Every call is checked against deny first, then ask, then allow, and the first match decides. Specificity does not change the order, so a broad deny such as Bash(aws *) blocks aws s3 ls even when Bash(aws s3 ls) is allowed.
Read more (1)Show less
In a Bash rule, * matches any text, spaces included. Everything before the first * is the limit, so put it after the subcommand: Bash(git log *) allows only git log, while Bash(git *) allows every git command, push included. The space matters too, because Bash(ls *) does not match lsof. Commands joined with &&, ||, ; or | are split, and each part must match on its own, so allowing npm run lint does not quietly approve npm run lint && rm -rf dist.
Why doesn't .claudeignore protect your secrets?
Plenty of guides still say to list secrets in a .claudeignore file. The docs say plainly that such a file has no effect. The supported way to keep Claude's file tools away from a file is a Read deny rule, such as Read(./.env) and Read(./.env.*), and a Read deny also blocks Edit and Write on the same path.
Read more (1)Show less
Know where it stops. Read and Edit deny rules cover Claude's built-in file tools, file commands Claude Code recognises in Bash such as cat and head, and redirections. They do not cover a Node or Python script that opens files itself; for that, the docs point to the sandbox, which enforces limits at the operating-system level. On the plus side, deny rules in a committed project file apply before a teammate accepts the workspace trust dialog, because they only restrict, while project allow rules wait for that trust.
What do the Claude Code permission modes change?
Modes set the baseline and rules sit on top. Manual, whose config value is default, runs only reads without asking. acceptEdits also approves file edits and common filesystem commands in your working directory. plan lets Claude read and propose without editing until you approve the plan. auto runs without routine prompts while a classifier checks actions in the background. dontAsk denies anything that would prompt, for locked-down scripts, and bypassPermissions skips prompts and belongs only in an isolated container or VM.
Read more (1)Show less
Press Shift+Tab to cycle modes during a session, or start in one with claude --permission-mode plan. permissions.defaultMode sets the starting mode, but a project file cannot start sessions in auto or bypassPermissions; those values only take effect from user or managed settings. Deny rules block in every mode, bypassPermissions included.
The real config behind this lesson
Excerpts from the repository that builds and deploys this website, quoted as they are.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/scripts/review-blog-figures.mjs\" --hook 2>/dev/null || true",
"timeout": 30,
"statusMessage": "Checking whether the figures fit"
}
]
}
]
}
}This repo's whole shared settings file. It holds one PostToolUse hook, covered in lesson 4, and no permission rules at all. Everyone who clones the repo gets the same figure check after every Write or Edit, while each contributor's own approvals collect in an untracked .claude/settings.local.json that is never committed.
# env files (can opt-in for committing if needed)
.env*
!.env.exampleGit keeps every env file except the example out of commits. That protects the repository, not the session. The @ file picker hides gitignored files by default, but that is a convenience, not a guard: nothing in the shared settings file stops Claude's Read tool from opening .env.
Mistakes that cost real time
Bash(git *) when you meant git log
Everything before the first * is what limits the rule, so Bash(git *) allows every git command, push included. Write the command you mean and replace only the part that varies: Bash(git log *).
Expecting a deny rule to stop a script
Read(./.env) stops Claude's file tools and cat, but not a Node or Python script that opens the file itself. For limits that hold for every process, turn on the sandbox.
Team rules in settings.local.json
Approvals you give at a prompt land in your local file, which only you have. If the whole team needs a rule, add it to .claude/settings.json and commit it.
Committed allow rules that seem ignored
Project allow rules apply only after each person accepts the workspace trust dialog. A claude -p run never shows that dialog, so they do not apply there at all. Deny and ask rules apply straight away.
What I'd change here: no Read deny for env files
This repo keeps API keys in a local env file, and its shared settings has no deny rule for it. Adding Read(./.env) and Read(./.env.*) to permissions.deny in .claude/settings.json would protect every contributor from their first session, before the trust dialog even appears.
| ~/.claude/settings.json | .claude/settings.json | .claude/settings.local.json | Managed settings | |
|---|---|---|---|---|
| Who it affects | You, in every project | Everyone working in the repo | You, in this repo only | Everyone your organisation deploys it to |
| In git? | No | Yes: commit it | No: Claude Code keeps it out | No: IT deploys it |
| Use it for | Personal preferences and your own rules | Team permissions, hooks and plugins | Personal overrides and saved approvals | Security policy nobody can override |
Do it on your own machine
You need Claude Code, Node and an empty folder. Fifteen minutes, with a fake secret, in a scratch repo you can delete afterwards.
Make a scratch repo with a lint script and a fake secret.
mkdir perm-lab && cd perm-lab && git init npm init -y npm pkg set scripts.lint="echo lint ok" echo "DEMO_TOKEN=not-a-real-secret" > .envCreate .claude/settings.json with one rule of each kind.
{ "permissions": { "allow": ["Bash(npm run *)"], "ask": ["Bash(git push *)"], "deny": ["Read(./.env)"] } }Start Claude Code and accept the trust dialog. Ask it to run the lint script, to push, and to show you .env. One runs silently, one prompts (decline it), and one is refused.
claudeRun /permissions to see each rule beside its source file, and /status to read the Setting sources line.
/permissions /statusApprove some other Bash command with "Yes, and don't ask again", then open the local file and find the rule it saved.
cat .claude/settings.local.jsonPrefix one prompt with /plan, read the plan Claude proposes, and notice that no file changed until you approve it.
/plan add a health-check script
Three questions before you move on
Pick an answer to see why it is right or wrong. Nothing is saved or graded, so guess freely.
Answer in your head, then check
1.You allow Bash(aws s3 ls) and deny Bash(aws *). Does aws s3 ls run?Show the answer
Answer:No. Deny is checked first and the first match decides; a more specific allow rule cannot carve an exception out of a deny.
2.A teammate clones the repo with your committed allow rules but still gets prompted. Why?Show the answer
Answer:Project allow rules wait until that person accepts the workspace trust dialog. A claude -p run never shows the dialog, so they do not apply there either.
3.Is adding .env to a .claudeignore file enough?Show the answer
Answer:No. That file has no effect. Use a Read(./.env) deny rule, and the sandbox for scripts that open files themselves.
4.Your team's .claude/settings.json sets defaultMode to plan and your user file sets acceptEdits. Which mode does a session in that repo start in?Show the answer
Answer:Plan. Shared project settings sit above user settings, so the team's value wins. To change it for yourself only, set it in .claude/settings.local.json.
Related deep dives on this site
Sources
Primary sources checked on the research date. Follow them for the current version.
- Claude Code docs: settings files and who they affect
- Claude Code docs: settings precedence
- Claude Code docs: lists merge instead of overriding
- Claude Code docs: manage permissions (deny, ask, allow)
- Claude Code docs: wildcard patterns
- Claude Code docs: Read and Edit rules
- Claude Code docs: project allow rules and workspace trust
- Claude Code docs: available permission modes
- Claude Code docs: respectGitignore and the @ file picker
- Claude Code docs in Indonesian: configure permissions