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:
~/.claude/settings.json- User-level. Applies to every project on your machine. This is your base config..claude/settings.json- Project-level. Lives inside a specific repo. Only applies when you run Claude Code from that directory.
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:
- Allow list - Commands matching these patterns run immediately, no prompt. Use this for safe, everyday commands you run constantly.
- 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.
- 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 *:
Bash(git *)- Matches any Bash command starting withgitfollowed by any arguments.Bash(npm run *)- Matchesnpm run test,npm run build, etc.Read- Matches the built-in Read tool (no arguments needed).Bash(curl * | bash)- Matches piped commands. Good for blocking remote code execution.
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:
- File location. It must be at
~/.claude/settings.json(user) or.claude/settings.json(project). Not~/.claude.json, notsettings.jsonin the project root. - Valid JSON. A missing comma or extra trailing comma breaks the whole file. Run
python3 -m json.tool ~/.claude/settings.jsonto validate. - 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 →