Claude Code — permission denied on file write or bash execution
Claude Code refuses destructive operations by default and prompts for approval on most tool calls. Configuring the permission system correctly is the difference between a smooth workflow and a wall of prompts.
Quick fix (TL;DR)
.claude/settings.json (project) and ~/.claude/settings.json (user). Fix "permission denied" errors by (a) adding specific bash patterns to permissions.allow, (b) using project-scoped settings for shared teams, and (c) never disabling the permission system in production repos — instead, allowlist the exact commands you need.Real error messages you'll see
These are the exact strings returned by the Claude API service and its SDKs when this error occurs. Copy-paste-searching any of them should land on this page.
Bash(rm -rf ./build) ⚠ This command requires approval. Choose: 1. Allow once 2. Allow always for "rm -rf ./build" 3. Allow always for "rm -rf *" 4. Reject
Write(/etc/hosts) ❌ Permission denied: /etc/hosts is outside the project root and not in the allowlist.
$ claude Error loading .claude/settings.json: 'allowedTools' should be 'permissions.allow' (settings v2 schema). See https://docs.claude.com/claude-code/settings for migration.
Reference
Where Claude Code reads permissions from (in precedence order)
| Location | Scope | Best for |
|---|---|---|
.claude/settings.local.json | Project — not checked in | Personal overrides |
.claude/settings.json | Project — checked in | Team-wide project permissions |
~/.claude/settings.json | User — all projects | Personal defaults across projects |
--dangerously-skip-permissions CLI flag | One-off session | Sandbox / disposable environments only |
permissions.allow / permissions.deny pattern syntax
| Pattern | Matches | Example |
|---|---|---|
Bash(cmd) | Exact bash command | Bash(npm test) |
Bash(cmd:*) | Command with any args | Bash(git status:*) |
Bash(cmd arg:*) | Prefix match | Bash(npm run:*) |
Read(./path/**) | File pattern for reads | Read(./src/**) |
Write(./path/**) | File pattern for writes | Write(./dist/**) |
Edit(./path/**) | File pattern for edits | Edit(./src/**/*.ts) |
WebFetch(domain:example.com) | Domain-scoped web fetch | WebFetch(domain:github.com) |
Root causes, ranked by frequency
Based on developer reports across Claude API forums, GitHub issues, and Anthropic community during 2025–2026.
- 28%No allowlist in place. Fresh project or new checkout; every tool call prompts because nothing is pre-approved.
- 18%Command varies slightly from allowed pattern.
Bash(npm test)allowed but the model runsnpm test -- --watch; different string, prompt appears again. - 14%File operation outside project root. Claude Code refuses writes to
~/.zshrc,/etc/*, and other paths outside the project unless explicitly allowed. - 10%Settings schema mismatch. Old
allowedTools/deniedToolskeys from settings v1 no longer recognised; new schema usespermissions.allow/permissions.deny. - 8%Deny list overriding allow list. Same pattern in both places; deny always wins.
- 7%Local settings not checked in. Team member has
settings.local.json; new joiner does not; joiner sees prompts everyone else avoided. - 8%WebFetch not domain-scoped.
WebFetchrequiresdomain:patterns; without them, every fetch prompts. - 7%Global permissions accidentally shadow project settings.
~/.claude/settings.jsondeny wins over project allow.
Fixes — copy-paste solutions
Set up a project-scoped settings.json with the right allowlist
Create .claude/settings.json at the project root, with a curated allowlist for the commands your workflow actually needs. Deny the destructive patterns explicitly.
{
"permissions": {
"allow": [
// Read anything in the project
"Read(./**)",
// Write and edit source but not lockfiles or infra secrets
"Write(./src/**)",
"Write(./tests/**)",
"Edit(./src/**)",
"Edit(./tests/**)",
// Common bash commands used in this repo
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(npm test:*)",
"Bash(npm run:*)",
"Bash(npm install:*)",
"Bash(pnpm:*)",
"Bash(pytest:*)",
"Bash(ruff:*)",
"Bash(black:*)",
// Docs — read-only
"WebFetch(domain:docs.python.org)",
"WebFetch(domain:developer.mozilla.org)",
"WebFetch(domain:github.com)"
],
"deny": [
// Never let the agent touch these, even by accident
"Write(./.env)",
"Write(./.env.local)",
"Write(./secrets/**)",
"Edit(./.env)",
"Edit(./.env.local)",
"Edit(./secrets/**)",
// No destructive git or shell
"Bash(git push --force:*)",
"Bash(git reset --hard:*)",
"Bash(rm -rf:*)",
"Bash(sudo:*)"
]
},
// (Optional) Additional dev settings
"hooks": {},
"env": {
"NODE_ENV": "development"
}
}.claude/settings.json into version control so every team member (and CI) gets the same permission model. Do not check in settings.local.json — that is for personal overrides.Use prefix patterns and command families for flexibility
Use Bash(cmd:*) to allow any invocation of a command. Group family commands (git, npm, pytest) under prefix patterns rather than listing every variation.
{
"permissions": {
"allow": [
// BAD — will prompt whenever args differ
// "Bash(git status)",
// "Bash(git diff)",
// "Bash(git log)",
// GOOD — prefix match on the git subcommand
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
// Whole-family allow — trust all git except explicit deny patterns
// Use with care; combine with deny for destructive subcommands
"Bash(git:*)",
// Specific script always OK
"Bash(./scripts/lint.sh:*)",
"Bash(make test:*)",
"Bash(make build:*)"
],
"deny": [
// Even under "Bash(git:*)", deny wins for these
"Bash(git push --force:*)",
"Bash(git push --force-with-lease:*)",
"Bash(git reset --hard:*)",
"Bash(git filter-branch:*)",
"Bash(git rebase --root:*)"
]
}
}Bash(cmd:*) matches any args including empty; Bash(cmd arg:*) requires that arg. When in doubt, start narrow and widen based on prompts you see in the wild.Approve during the session — auto-persist to settings.local.json
When a prompt appears, choose "Allow always" for the specific pattern you trust. Claude Code writes it to .claude/settings.local.json (not checked in). Review that file periodically and promote entries to the shared settings.json.
# 1) Start Claude Code and observe prompts claude # In a fresh project you will get prompts like: # Bash(npm test) → Approve? [1] Allow once [2] Allow always for "npm test" [3] ... # 2) Choose "Allow always" for trusted patterns. # Claude writes to .claude/settings.local.json: cat .claude/settings.local.json # { # "permissions": { # "allow": ["Bash(npm test:*)", "Read(./src/**)"] # } # } # 3) Periodically review and promote to the shared config git diff .claude/settings.local.json # what have I approved this week? # Promote to the shared file so the team benefits: jq -s ".[0].permissions.allow + .[1].permissions.allow | unique" \ .claude/settings.json .claude/settings.local.json # Move the deduplicated allow list to .claude/settings.json, # then clear settings.local.json to just personal overrides.
--dangerously-skip-permissions outside a throwaway container. It disables the entire guardrail; a hallucinated rm -rf from Claude will run without asking.Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Check
.claude/settings.jsoninto version control — same permission model for everyone. - Use prefix patterns (
Bash(cmd:*)) for command families to reduce prompts. - Always deny destructive patterns explicitly:
rm -rf,git push --force,git reset --hard,sudo. - Never allow writes to
.env,secrets/**, or any credentials file. - Domain-scope every
WebFetchpermission — never allow unrestricted web access. - Review
settings.local.jsonweekly and promote proven-safe patterns to the shared file. - For CI/agentic runs, use a dedicated container image with narrow permissions — do not reuse dev config.
Frequently asked questions
settings.json is checked into version control and applies to everyone on the team. settings.local.json is per-user, not checked in — meant for personal overrides. Both are project-scoped. There is also ~/.claude/settings.json for user-wide defaults across all projects.--dangerously-skip-permissions, but do not. That flag is intended for disposable containers where damage is impossible. In any real environment, use a wide allowlist instead — you get most of the same experience with the guardrails intact.~/.claude/projects/<project>/. Every tool call is logged. For agentic runs, tail these files or ship them to your log aggregator for review.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.