Claude Code — permission denied on file or bash operations (2026) — Fix Guide (2026) | AI Error Hub
Home Providers Claude Claude Code permissions
Claude Claude Code · Permissions Severity: Medium HTTP n/a

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.

By Ahmed R. · Senior AI Infrastructure Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: Claude Code uses per-tool allowlists and denylists defined in .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 tool blocked at prompt
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
File write outside allowed roots
Write(/etc/hosts)
❌ Permission denied: /etc/hosts is outside the project root and not in the allowlist.
Settings file rejected — invalid schema
$ 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)

LocationScopeBest for
.claude/settings.local.jsonProject — not checked inPersonal overrides
.claude/settings.jsonProject — checked inTeam-wide project permissions
~/.claude/settings.jsonUser — all projectsPersonal defaults across projects
--dangerously-skip-permissions CLI flagOne-off sessionSandbox / disposable environments only

permissions.allow / permissions.deny pattern syntax

PatternMatchesExample
Bash(cmd)Exact bash commandBash(npm test)
Bash(cmd:*)Command with any argsBash(git status:*)
Bash(cmd arg:*)Prefix matchBash(npm run:*)
Read(./path/**)File pattern for readsRead(./src/**)
Write(./path/**)File pattern for writesWrite(./dist/**)
Edit(./path/**)File pattern for editsEdit(./src/**/*.ts)
WebFetch(domain:example.com)Domain-scoped web fetchWebFetch(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 runs npm 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/deniedTools keys from settings v1 no longer recognised; new schema uses permissions.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. WebFetch requires domain: patterns; without them, every fetch prompts.
  • 7%
    Global permissions accidentally shadow project settings. ~/.claude/settings.json deny wins over project allow.

Fixes — copy-paste solutions

Fix #1

Set up a project-scoped settings.json with the right allowlist

Check this in — every team member gets the same experience.

Create .claude/settings.json at the project root, with a curated allowlist for the commands your workflow actually needs. Deny the destructive patterns explicitly.

.claude/settings.json
{
  "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"
  }
}
Check .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.
Fix #2

Use prefix patterns and command families for flexibility

Prefix matching cuts down "same command, slightly different args" prompts.

Use Bash(cmd:*) to allow any invocation of a command. Group family commands (git, npm, pytest) under prefix patterns rather than listing every variation.

refined_bash_patterns.json
{
  "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.
Fix #3

Approve during the session — auto-persist to settings.local.json

Let the workflow shape your permissions organically.

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.

workflow_example.sh
# 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.
Never use --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.json into 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 WebFetch permission — never allow unrestricted web access.
  • Review settings.local.json weekly 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.
Yes with --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.
Yes. If a pattern matches both allow and deny, deny wins. This is intentional — you can safely widen an allowlist and add specific denies for the dangerous edges of that pattern.
An unscoped WebFetch permission would let the agent hit any URL — a privacy and data-exfil risk. Scoping to specific domains restricts fetches to trusted sources. Add domains one at a time as you need them.
Claude Code writes session transcripts to ~/.claude/projects/<project>/. Every tool call is logged. For agentic runs, tail these files or ship them to your log aggregator for review.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.