C2 · lesson 3 · 9 min read
settings.json and permission rules
Settings file locations and precedence, and how allow, ask and deny rules are written and evaluated.
Հայերեն
CLAUDE.md tells Claude what you want. settings.json controls what it is
allowed to do. Instructions can be ignored or misread; settings are enforced by
the client.
Where settings live
| File | Scope | In git? |
|---|---|---|
Managed settings (/etc/claude-code/managed-settings.json and the macOS/Windows equivalents, or MDM) |
Organisation | n/a |
.claude/settings.json |
Project, shared | Yes |
.claude/settings.local.json |
Project, personal | No (git-excluded when Claude Code creates it) |
~/.claude/settings.json |
You, every project | n/a |
Precedence, highest first: managed → command line (--settings) → project
local → shared project → user. Lists such as permissions.allow are merged
across files rather than replaced, so a user-level allow and a project-level allow
both apply.
A realistic file
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(terraform plan *)",
"Bash(terraform fmt *)",
"Bash(helm lint *)"
],
"ask": ["Bash(terraform apply *)", "Bash(git push *)"],
"deny": ["Read(./.env)", "Read(./**/*.tfvars)", "WebFetch"],
"defaultMode": "plan"
},
"env": { "TF_IN_AUTOMATION": "1" }
}
Other useful keys are model, hooks (next lesson) and, inside permissions,
additionalDirectories and disableBypassPermissionsMode.
Rule syntax
A rule is Tool or Tool(specifier):
| Rule | Matches |
|---|---|
Bash(npm run build) |
Exactly that command |
Bash(npm run *) |
Any npm run … |
Read(./secrets/**) |
Anything under secrets/ (gitignore-style globs) |
Edit(/src/**/*.ts) |
TypeScript files under the project’s src/ |
WebFetch(domain:*.example.com) |
Fetches to any subdomain |
mcp__github__* |
Every tool from the github MCP server |
Details that catch people out:
- Wildcards and spaces.
Bash(ls *)matchesls -labut notlsof;Bash(ls*)matches both. The older trailing formBash(npm run test:*)still works. - Compound commands. Claude Code understands
&&,;and|, sogit status && rm -rf xneeds both parts allowed. - Paths.
/pathis relative to the settings file’s project, not the filesystem root. Use//abs/pathfor absolute paths and~/pathfor your home directory. - Use Edit and Read. Path rules written as
Write(...)never match; useEdit(...). AReaddeny also blocks editing the same path.
How rules are evaluated
That gives you a simple pattern for infrastructure repos:
- deny: secrets and destructive commands that should never happen
(
Read(./.env),Bash(terraform destroy *)), - ask: state-changing commands that need a human
(
Bash(terraform apply *),Bash(kubectl apply *)), - allow: safe, repetitive commands
(
Bash(terraform plan *),Bash(helm template *)).
Because a managed deny also beats --allowedTools on the command line, an
organisation can block something for every user and every CI job in one place.
Recap
- There are four settings locations. Managed beats CLI, which beats local, which beats project, which beats user. Lists merge.
- Rules are
Tool(specifier), with globs for paths and*for commands. - Evaluation is deny, then ask, then allow; the first match wins and specificity doesn’t matter.
- Use deny for secrets, ask for state changes and allow for safe repetition.
See it in action
1/5 Claude wants to run a tool. Before anything runs, Claude Code checks your permission rules.
all steps
- Claude wants to run a tool. Before anything runs, Claude Code checks your permission rules.
- Deny rules are checked first. A match in any settings file (user, project or managed) counts.
- A deny match blocks the call. No allow rule can override it, however specific.
- No deny match? Ask rules come next. A match means you're asked, even if a narrower allow rule also matches.
- Only then are allow rules checked. A match runs the tool without a prompt; with no match at all, the permission mode decides.
$ ./try-it.sh
Try it: a least-privilege settings file for an infra repo
- Create
.claude/settings.jsonthat allowsBash(terraform plan *),Bash(terraform fmt *)andBash(helm lint *). - Add
Bash(terraform apply *)andBash(kubectl delete *)toask, andRead(./.env)plusRead(./**/*.tfvars)todeny. - Start a session and ask Claude to read a
.tfvarsfile. Confirm that it's blocked. - Add your own extra allow rule in
.claude/settings.local.jsonand check in/permissionswhich file each rule comes from.
Check yourself
3 questions. Original practice questions, not taken from the real exam.