Claude Code settings.json & Permissions: The Practical Reference

Claude Code’s permission prompts exist so an agent with a shell can’t surprise you. But out of the box you’ll be approving npm test for the hundredth time, and the fix people reach for — blanket bypass — throws away the guardrails entirely. The actual fix is fifteen minutes with settings.json. This is the reference I wanted when I started: which file wins, how rules match, and a setup that’s quiet and safe.

The three files and who wins

Claude Code reads settings from three layers, most specific winning:

  1. ~/.claude/settings.json — your user file. Applies to every project on your machine. Personal defaults live here.
  2. .claude/settings.json — the project file. Committed to the repo, shared with the team: project policy.
  3. .claude/settings.local.json — your personal overrides for one project. Git-ignored; this is where your machine-specific allowances go without becoming team policy.

The layering is the feature. Team policy says what’s forbidden; your local file says what you’re comfortable auto-approving; neither pollutes the other. (If the .claude directories themselves are new to you — what else lives in them, what to commit — see the .claude folder, explained.)

Permission rules: the part worth understanding properly

The permissions block holds allow, deny, and ask lists. Each entry names a tool, optionally with a matcher for its input:

{
  "permissions": {
    "allow": [
      "Bash(npm run test:*)",
      "Bash(git diff:*)",
      "Bash(git log:*)",
      "Read(~/notes/**)"
    ],
    "deny": [
      "Bash(git push --force:*)",
      "Read(.env*)",
      "WebFetch"
    ]
  }
}

The rules that matter in practice:

  • Deny always beats allow. A denied pattern is blocked even if a broader allow matches it.
  • Matchers are prefix-style patterns, not regex. Bash(npm run test:*) covers npm run test and everything after it. Precision matters: Bash(git:*) allows all git — including git push --force — which is almost never what you meant. Allow narrow, specific prefixes.
  • Deny is also your secrets shield. Read(.env*) keeps the agent from ever opening your env files — enforcement, not etiquette.
  • You don’t have to hand-edit JSON. The /permissions command edits these interactively, and the “always allow” option on any prompt writes the rule for you. The JSON view is for reviewing and committing policy.

Permission modes (and the one to be careful with)

Claude Code also has session-level modes: the default (ask for anything not allowlisted), accept-edits (file edits pre-approved, shell still gated), plan mode (read-only — Claude proposes, you approve before anything changes), and bypass (no prompts at all). Plan mode is underrated for unfamiliar codebases; bypass belongs in sandboxes and CI, not on your laptop with your dotfiles. The quiet-but-safe goal is: default mode plus a well-tended allowlist.

What else lives in settings.json

Beyond permissions, the file carries the rest of the tool’s configuration — the highlights you’ll actually touch:

  • env — environment variables for every session (API endpoints, feature flags).
  • model — your default model.
  • hooks — shell commands that fire on lifecycle events, like running your formatter after every file edit. Hooks are enforcement’s big brother: where permissions say no, hooks say and then this happens automatically. They deserve their own post.
  • statusLine — the configurable terminal status line (also set up via /statusline).

The starter setup I’d give a teammate

  1. In the project file: deny the disasters (git push --force, reading .env*, your deploy command), so policy travels with the repo.
  2. In your local file: allow your loop — test runner, linter, git status/diff/log — each as a narrow prefix.
  3. Leave everything else on ask, and use “always allow” prompts to grow the allowlist from real usage rather than speculation.
  4. Revisit monthly with /permissions — allowlists, like CLAUDE.md files, accumulate cruft.

The division of labor across Claude Code’s config, in one line each: settings.json enforces, CLAUDE.md instructs, memory accumulates, and custom commands package procedures. Get each concern in its right file and the tool feels like it was configured by someone who likes you. (And if you’re looking for where all these files live on disk, the Mac guide to Claude’s folders has the map.)

Usual disclosure before the sign-off: we build NorthLab Folders — local folders and search for the claude.ai/ChatGPT/Gemini side of your workflow, where the design conversations happen before the settings get written.

Frequently asked questions

Where is Claude Code’s settings.json file?

Three places, by scope: ~/.claude/settings.json applies to you on every project; .claude/settings.json in a repo applies to that project and should be committed; .claude/settings.local.json is your personal per-project layer and belongs in .gitignore. More specific files override broader ones.

How do Claude Code permissions work?

Through allow, deny, and ask rules matched against tool calls — for example Bash(npm run test:*) allows test commands without prompting, while a deny rule blocks a tool call outright. Deny beats allow. You can edit rules interactively with the /permissions command, and they persist to settings.json.

How do I stop Claude Code from asking for permission constantly?

Allowlist the safe, repetitive commands your workflow actually uses — your test runner, linter, git status/diff — rather than reaching for a blanket bypass. Each allow rule like Bash(make test:*) removes one recurring prompt while keeping genuinely destructive actions behind confirmation.

What’s the difference between settings.json and CLAUDE.md?

CLAUDE.md is advisory — instructions the model reads and follows as intent. settings.json is mechanical — permission rules the harness enforces before anything runs. Rules that must be unbreakable (never touch prod, never force-push) belong in settings.json; context and conventions belong in CLAUDE.md.