Claude Code's settings.json controls what Claude can and can't do on your system. It uses allow and deny lists to grant or block specific Bash commands, built-in tools, and MCP servers. Without it, Claude prompts you for permission on every single action - which gets old fast when you are running hundreds of commands per session.

This guide covers the exact permission model, copy-paste starter configs for every major stack, and the patterns I use across 160+ sessions. Every JSON example is valid, tested, and ready to drop into your setup.

Quick Answer

Claude Code's settings.json at ~/.claude/settings.json controls what Claude can and can't do on your system. It uses allow and deny lists to grant or block specific Bash commands, tools, and MCP servers. Without it, Claude prompts you for every action. Create the file, add your safe commands to permissions.allow, add dangerous commands to permissions.deny, and everything else gets a manual approval prompt.

Where settings.json Lives

Claude Code looks for settings in two locations, and they serve different purposes:

How They Merge

Project settings merge with user settings. They do not replace them. If your user-level config allows Bash(git *) and your project-level config allows Bash(terraform *), both are active when you work in that project. Project-level deny rules also stack on top of user-level deny rules.

This means you set up your base permissions once at the user level, then add project-specific permissions per repo. You never need to duplicate your core config.

Practical advice: start with user-level only

Most developers only need ~/.claude/settings.json. Add project-level settings when you have a repo with unusual requirements - like a CI/CD repo that needs terraform access or a data pipeline that needs psql.

The Permission Model

Claude Code's permission system has three tiers. Every command falls into exactly one:

  1. Allow list - Commands matching these patterns run immediately, no prompt. Use this for safe, everyday commands you run constantly.
  2. Deny list - Commands matching these patterns are blocked entirely. Claude will not run them even if you ask. Use this for destructive or dangerous commands.
  3. Everything else - Claude asks you before running. You approve or reject in real time. This is the default for any command not in either list.

Deny always wins. If a command matches both an allow pattern and a deny pattern, it gets blocked. This is the right behavior - your safety net should never have holes.

Pattern Matching Syntax

Patterns use simple wildcard matching with *:

The wildcard is greedy - Bash(git *) matches git status, git push --force origin main, and everything in between. Be aware of what you are opening up.

Starter settings.json (Copy-Paste Ready)

This is the general-purpose config that works for most developers. It pre-approves common safe commands and blocks the stuff that can ruin your day.

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(npm *)",
      "Bash(npx *)",
      "Bash(node *)",
      "Bash(python3 *)",
      "Bash(pip *)",
      "Bash(ls *)",
      "Bash(mkdir *)",
      "Bash(cp *)",
      "Bash(mv *)",
      "Bash(cat *)",
      "Bash(which *)",
      "Bash(docker *)",
      "Read",
      "Write",
      "Edit",
      "Glob",
      "Grep"
    ],
    "deny": [
      "Bash(rm -rf /)",
      "Bash(rm -rf /*)",
      "Bash(sudo rm -rf *)",
      "Bash(chmod 777 *)",
      "Bash(curl * | bash)",
      "Bash(curl * | sh)",
      "Bash(wget * | bash)",
      "Bash(> /dev/sda*)",
      "Bash(mkfs *)",
      "Bash(dd if=*)"
    ]
  }
}

Why these allow entries: Git, npm, node, and python3 are the commands you run hundreds of times per session. Approving each one manually adds friction without adding safety. The built-in tools (Read, Write, Edit, Glob, Grep) are filesystem operations that Claude needs to be useful at all.

Why these deny entries: Every deny rule prevents a specific catastrophic outcome. rm -rf / and rm -rf /* protect your entire filesystem. curl | bash blocks remote code execution. dd if=* and mkfs * prevent disk destruction. chmod 777 * blocks permission blowouts. These are not theoretical - one wrong command and you are rebuilding from backups.

Stack-Specific Configurations

Node.js / TypeScript Developer

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(npm *)",
      "Bash(npx *)",
      "Bash(node *)",
      "Bash(tsc *)",
      "Bash(vitest *)",
      "Bash(playwright *)",
      "Bash(eslint *)",
      "Bash(prettier *)",
      "Bash(pnpm *)",
      "Bash(ls *)",
      "Bash(mkdir *)",
      "Bash(cp *)",
      "Bash(mv *)",
      "Bash(cat *)",
      "Bash(which *)",
      "Bash(echo *)",
      "Bash(pwd)",
      "Bash(head *)",
      "Bash(tail *)",
      "Bash(wc *)",
      "Bash(gh *)",
      "Read",
      "Write",
      "Edit",
      "Glob",
      "Grep"
    ],
    "deny": [
      "Bash(rm -rf /)",
      "Bash(rm -rf /*)",
      "Bash(sudo rm -rf *)",
      "Bash(chmod 777 *)",
      "Bash(curl * | bash)",
      "Bash(curl * | sh)",
      "Bash(npm publish *)",
      "Bash(git push --force origin main)"
    ]
  }
}

Note the deny on npm publish - you probably don't want Claude accidentally publishing a package. Same idea with force-pushing to main.

Python Developer

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(python3 *)",
      "Bash(python *)",
      "Bash(pip *)",
      "Bash(pip3 *)",
      "Bash(uv *)",
      "Bash(pytest *)",
      "Bash(ruff *)",
      "Bash(mypy *)",
      "Bash(black *)",
      "Bash(isort *)",
      "Bash(ls *)",
      "Bash(mkdir *)",
      "Bash(cp *)",
      "Bash(mv *)",
      "Bash(cat *)",
      "Bash(which *)",
      "Bash(echo *)",
      "Bash(pwd)",
      "Bash(head *)",
      "Bash(tail *)",
      "Bash(wc *)",
      "Bash(gh *)",
      "Read",
      "Write",
      "Edit",
      "Glob",
      "Grep"
    ],
    "deny": [
      "Bash(rm -rf /)",
      "Bash(rm -rf /*)",
      "Bash(sudo rm -rf *)",
      "Bash(chmod 777 *)",
      "Bash(curl * | bash)",
      "Bash(curl * | sh)",
      "Bash(pip install --break-system-packages *)",
      "Bash(dd if=*)"
    ]
  }
}

Rust Developer

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(cargo *)",
      "Bash(rustc *)",
      "Bash(rustup *)",
      "Bash(clippy *)",
      "Bash(rustfmt *)",
      "Bash(ls *)",
      "Bash(mkdir *)",
      "Bash(cp *)",
      "Bash(mv *)",
      "Bash(cat *)",
      "Bash(which *)",
      "Bash(echo *)",
      "Bash(pwd)",
      "Bash(head *)",
      "Bash(tail *)",
      "Bash(gh *)",
      "Read",
      "Write",
      "Edit",
      "Glob",
      "Grep"
    ],
    "deny": [
      "Bash(rm -rf /)",
      "Bash(rm -rf /*)",
      "Bash(sudo rm -rf *)",
      "Bash(chmod 777 *)",
      "Bash(curl * | bash)",
      "Bash(cargo publish *)",
      "Bash(dd if=*)"
    ]
  }
}

Full-Stack Developer

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(npm *)",
      "Bash(npx *)",
      "Bash(pnpm *)",
      "Bash(node *)",
      "Bash(tsc *)",
      "Bash(python3 *)",
      "Bash(pip *)",
      "Bash(uv *)",
      "Bash(cargo *)",
      "Bash(rustc *)",
      "Bash(docker *)",
      "Bash(docker-compose *)",
      "Bash(docker compose *)",
      "Bash(make *)",
      "Bash(vitest *)",
      "Bash(pytest *)",
      "Bash(playwright *)",
      "Bash(eslint *)",
      "Bash(ruff *)",
      "Bash(ls *)",
      "Bash(mkdir *)",
      "Bash(cp *)",
      "Bash(mv *)",
      "Bash(rm *)",
      "Bash(cat *)",
      "Bash(which *)",
      "Bash(echo *)",
      "Bash(pwd)",
      "Bash(head *)",
      "Bash(tail *)",
      "Bash(sort *)",
      "Bash(wc *)",
      "Bash(grep *)",
      "Bash(find *)",
      "Bash(gh *)",
      "Read",
      "Write",
      "Edit",
      "Glob",
      "Grep"
    ],
    "deny": [
      "Bash(rm -rf /)",
      "Bash(rm -rf /*)",
      "Bash(sudo rm -rf *)",
      "Bash(chmod 777 *)",
      "Bash(curl * | bash)",
      "Bash(curl * | sh)",
      "Bash(wget * | bash)",
      "Bash(> /dev/sda*)",
      "Bash(mkfs *)",
      "Bash(dd if=*)",
      "Bash(npm publish *)",
      "Bash(cargo publish *)",
      "Bash(git push --force origin main)"
    ]
  }
}

DevOps / Infrastructure

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(terraform plan *)",
      "Bash(terraform validate *)",
      "Bash(terraform fmt *)",
      "Bash(terraform show *)",
      "Bash(terraform state list *)",
      "Bash(kubectl get *)",
      "Bash(kubectl describe *)",
      "Bash(kubectl logs *)",
      "Bash(kubectl top *)",
      "Bash(helm list *)",
      "Bash(helm status *)",
      "Bash(helm template *)",
      "Bash(aws sts get-caller-identity)",
      "Bash(aws s3 ls *)",
      "Bash(aws ec2 describe-*)",
      "Bash(gcloud config list *)",
      "Bash(gcloud projects list *)",
      "Bash(docker *)",
      "Bash(ls *)",
      "Bash(cat *)",
      "Bash(which *)",
      "Bash(echo *)",
      "Bash(pwd)",
      "Bash(gh *)",
      "Read",
      "Write",
      "Edit",
      "Glob",
      "Grep"
    ],
    "deny": [
      "Bash(terraform apply *)",
      "Bash(terraform destroy *)",
      "Bash(kubectl delete *)",
      "Bash(kubectl apply *)",
      "Bash(helm install *)",
      "Bash(helm upgrade *)",
      "Bash(helm uninstall *)",
      "Bash(aws s3 rm *)",
      "Bash(aws ec2 terminate-*)",
      "Bash(rm -rf /)",
      "Bash(rm -rf /*)",
      "Bash(sudo rm -rf *)",
      "Bash(chmod 777 *)",
      "Bash(curl * | bash)",
      "Bash(dd if=*)"
    ]
  }
}

Why the DevOps config is different

Notice that terraform apply, kubectl delete, and helm install are on the deny list - not the allow list. Infrastructure changes should always require explicit human approval. Let Claude plan and validate freely, but keep the apply button in your hands.

Common Patterns and Tips

Always Allow Read-Only Tools

The built-in tools Read, Glob, and Grep are safe to always allow. They only read your filesystem - they never modify anything. Blocking them just means Claude asks you "Can I read this file?" fifty times per session.

"Read",
"Write",
"Edit",
"Glob",
"Grep"

Write and Edit do modify files, but that is literally Claude's job. If you block those, Claude can not help you code.

Block Destructive Commands Explicitly

Don't rely on "I just won't ask Claude to do that." Claude generates commands based on context, and sometimes the shortest path to solving a problem involves a dangerous command. Your deny list is your insurance policy.

Use Wildcards Wisely

Bash(git *) allows all git commands - including git push --force, git reset --hard, and git clean -fd. If that makes you uncomfortable, be more specific:

"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(git add *)",
"Bash(git commit*)",
"Bash(git push)",
"Bash(git pull*)",
"Bash(git branch*)",
"Bash(git checkout*)",
"Bash(git stash*)"

More lines, but you know exactly what is allowed. I personally use the wildcard approach and add specific deny entries for the dangerous git commands. Less config, same safety.

Project-Level Overrides for Special Repos

Got a repo that needs psql access? A data pipeline that runs dbt? Add a .claude/settings.json inside that project:

{
  "permissions": {
    "allow": [
      "Bash(psql *)",
      "Bash(dbt *)"
    ]
  }
}

This stacks on top of your user-level config. You get your base permissions plus the project-specific ones.

MCP Server Permissions

If you use MCP (Model Context Protocol) servers - like GitHub, Slack, or database connectors - you can control their permissions the same way. MCP tools follow the same allow/deny pattern.

{
  "permissions": {
    "allow": [
      "mcp__github__get_issue",
      "mcp__github__list_issues",
      "mcp__github__create_pull_request",
      "mcp__github__get_pull_request",
      "mcp__github__list_pull_requests",
      "mcp__github__search_code"
    ],
    "deny": [
      "mcp__slack__post_message",
      "mcp__slack__send_message"
    ]
  }
}

This lets Claude read GitHub issues and create PRs freely, but blocks it from sending Slack messages on your behalf. The principle is the same: allow read operations generously, gate write operations carefully.

You can also use wildcards with MCP tools. mcp__github__* would allow all GitHub MCP operations. I recommend being specific with MCP permissions since these tools interact with external services - not just your local filesystem.

Troubleshooting

"Claude keeps asking me to approve commands"

The command is not in your allow list. Check your ~/.claude/settings.json and add it. Pay attention to the exact format - it is Bash(command *), not just command *. The Bash() wrapper is required.

"A command I need is blocked"

Check your deny patterns. A broad deny like Bash(rm *) would block rm temp.txt along with rm -rf /. Make your deny patterns specific to the dangerous variants, not the entire command.

"Settings aren't loading"

Three things to check:

  1. File location. It must be at ~/.claude/settings.json (user) or .claude/settings.json (project). Not ~/.claude.json, not settings.json in the project root.
  2. Valid JSON. A missing comma or extra trailing comma breaks the whole file. Run python3 -m json.tool ~/.claude/settings.json to validate.
  3. New session. Settings load at session start. If you changed the file mid-session, start a new one.

"Project settings override my user settings"

That is expected behavior. Project settings merge with user settings - they add to your allow and deny lists. If a project deny rule blocks something your user config allows, the deny wins. This is intentional. A project can tighten security, and that is a feature.

Frequently Asked Questions

Can I allow all commands?

You could add Bash(*) to your allow list. But you should not. The whole point of settings.json is controlled trust. One hallucinated command with broad permissions can delete your repo, overwrite config files, or worse. The minor inconvenience of approving an occasional unusual command is worth the safety net. I have run 160+ sessions and the specific allow lists in this guide cover 99% of what you actually need.

What is the difference between user and project settings?

Scope. User settings at ~/.claude/settings.json apply to every project on your machine - they are your baseline. Project settings at .claude/settings.json inside a repo apply only when working in that directory. They merge together, so project configs add to your base - they do not replace it.

Do I need to restart Claude Code after changing settings?

No. Claude Code loads settings.json fresh at the start of each session. Save the file, start a new session, and your changes are active. There is no restart command, no reload button. It just works.

Can I see what permissions are active?

Yes. Run /permissions inside Claude Code to see the current allow and deny lists. You can also ask Claude directly to describe its active permissions, or just read the file yourself with cat ~/.claude/settings.json.

Get the Complete Config Pack

The starter settings.json and all five stack-specific configs from this guide are included in the AI Setup pack at drewsky.ai/setup. Free CLAUDE.md template to get started, or $99 for the complete config pack with production-tested settings.json, 5 skills, and the exact patterns behind 9 live tools.

Get the Config Pack →
← All posts Claude Code Setup Guide →