Skip to content

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.

Ընտրեք տեքստը՝ հայերեն թարգմանությունը տեսնելու համար։Highlight text to see Armenian. Machine-assisted translation; English is the source of truth.

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 *) matches ls -la but not lsof; Bash(ls*) matches both. The older trailing form Bash(npm run test:*) still works.
  • Compound commands. Claude Code understands &&, ; and |, so git status && rm -rf x needs both parts allowed.
  • Paths. /path is relative to the settings file’s project, not the filesystem root. Use //abs/path for absolute paths and ~/path for your home directory.
  • Use Edit and Read. Path rules written as Write(...) never match; use Edit(...). A Read deny also blocks editing the same path.

How rules are evaluated

That gives you a simple pattern for infrastructure repos:

  1. deny: secrets and destructive commands that should never happen (Read(./.env), Bash(terraform destroy *)),
  2. ask: state-changing commands that need a human (Bash(terraform apply *), Bash(kubectl apply *)),
  3. 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

How a permission decision is made
matchno matchmatchno matchmatchtool callBash(git push …)deny rule?ask rule?allow rule?blockedClaude Code asks youruns without asking

1/5 Claude wants to run a tool. Before anything runs, Claude Code checks your permission rules.

all steps
  1. Claude wants to run a tool. Before anything runs, Claude Code checks your permission rules.
  2. Deny rules are checked first. A match in any settings file (user, project or managed) counts.
  3. A deny match blocks the call. No allow rule can override it, however specific.
  4. No deny match? Ask rules come next. A match means you're asked, even if a narrower allow rule also matches.
  5. 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

  1. Create .claude/settings.json that allows Bash(terraform plan *), Bash(terraform fmt *) and Bash(helm lint *).
  2. Add Bash(terraform apply *) and Bash(kubectl delete *) to ask, and Read(./.env) plus Read(./**/*.tfvars) to deny.
  3. Start a session and ask Claude to read a .tfvars file. Confirm that it's blocked.
  4. Add your own extra allow rule in .claude/settings.local.json and check in /permissions which file each rule comes from.

Check yourself

3 questions. Original practice questions, not taken from the real exam.

  1. User settings allow Bash(git push *). The project's .claude/settings.json denies Bash(git push *).

    1. What happens when Claude tries git push origin main?

  2. You add Bash(terraform plan *) to allow. Claude then proposes running: terraform plan -out=tf.plan && terraform apply tf.plan

    2. Does the allow rule let this compound command run without a prompt?

  3. You want Claude Code never to read Terraform variable files that may contain secrets, anywhere in the repo, and the rule should be shared with the whole team.

    3. Which file and rule do that?

Answer every question to check.