Claude Code permission errors: reading the refusal correctly

Four unrelated mechanisms produce something that reads like "permission denied", and the fixes have nothing in common. Identifying which one you are looking at is the whole job.

By the Continuum team. We build a workbench that runs Claude Code, Codex, and their peers, so the model rates quoted here are the ones our own cost analytics ship with.

The short version

A Claude Code permission problem is one of four things. The operating system refused, which shows as EACCES, EPERM, or Permission denied. The Claude Code permission system refused, which comes with an explanation and traces to a deny rule, an ask rule, a permission mode, or a hook. macOS refused, which shows as Operation not permitted in Desktop, Documents, or Downloads. Or the Bash sandbox refused, which happens only when you have turned it on. The message differs in each case, and so does the fix.

What you need to know
  • Four mechanisms: OS, Claude policy, macOS folder protection, and the sandbox.
  • EACCES or EPERM means the filesystem, not Claude.
  • Operation not permitted in Desktop or Documents means macOS.
  • Rules evaluate deny, then ask, then allow. Specificity never changes that order.
  • A path rule on Write or Glob is accepted and never consulted. Use Edit and Read.
  • Never sudo to fix this. It makes the next one worse.

Telling them apart

You seeMechanismFix
EACCES, EPERM, Permission deniedOperating systemFix file ownership
A refusal with a reason, in Claude's own wordsClaude Code policyA deny rule, an ask rule, a mode, or a hook
Operation not permitted in ~/Desktop or ~/DocumentsmacOS folder protectionGrant access, or move the project
A command fails only when the sandbox is onThe Bash sandboxWiden the filesystem or network policy
EACCES during npm install -gnpm prefix owned by rootUser-writable prefix; never sudo
A tool Claude never even mentionsA bare-name deny ruleIt was removed from Claude's context
Read works, write fails, no messageFolder protection, or a read-only mountCheck both

Filesystem permissions

# who owns it?
ls -la path/to/file
ls -ld .

# root-owned files in your project, usually from a sudo you regret
find . -user root 2>/dev/null | head

# take them back
sudo chown -R "$(whoami)" .

# the npm classic: a global prefix owned by root
ls -ld "$(npm root -g)"
sudo chown -R "$(whoami)" ~/.npm

# and the install directory the native installer needs
test -w ~/.local/bin && echo writable || echo "not writable"
sudo mkdir -p ~/.local/bin && sudo chown -R "$(whoami)" ~/.local

macOS protected folders

macOS protects ~/Desktop, ~/Documents, and ~/Downloads. A process working there needs an explicit grant, and without one the failures are intermittent, poorly explained, and look exactly like a broken tool. The tell is Operation not permitted on a file you can open yourself in Finder.

The fix that always works, and takes ten seconds.
mkdir -p ~/code
mv ~/Documents/my-project ~/code/
cd ~/code/my-project
claude

When Claude Code itself refuses

Now the useful part. A policy refusal has exactly four possible sources, and /permissions shows you the first two directly.

SourceHow to confirmNote
A deny rule matched/permissions, or jq .permissions on your settingsDeny always beats allow
An ask rule matchedSamePrompts even when a narrower allow matches
The permission mode/status, or Shift+Tab to see the current modePlan mode is read-only by design
A PreToolUse hook exited 2/hooks, then run the hook by handBlocks before rules are even evaluated
Managed policy/status shows whether managed settings are in effectCannot be overridden locally, by design
Outside the working directoryCompare the path with pwd--add-dir, /add-dir, or /cd
Inspecting the rules, and testing a hook the way Claude Code calls it.
# what is configured, per scope
jq .permissions ~/.claude/settings.json
jq .permissions .claude/settings.json
jq .permissions .claude/settings.local.json

# is a hook doing it?
jq .hooks .claude/settings.json

# run the hook by hand with a realistic payload
echo '{"tool_name":"Bash","tool_input":{"command":"git push --force"}}' \
  | .claude/hooks/guard.sh; echo "exit=$?"

One behaviour surprises people: a deny rule written as a bare tool name, such as Bash, removes the tool from Claude's context entirely, so Claude never knows it existed and never mentions it. A scoped rule such as Bash(rm *) leaves the tool available and blocks only matching calls. If Claude seems oddly unable to do something obvious, check for a bare-name deny before assuming a bug.

Permission modes, and what each one changes.

ModeBehaviour
defaultPrompts on first use of each tool. Labelled Manual in the CLI.
planReads and read-only commands only. No source edits.
acceptEditsAuto-accepts edits and common filesystem commands inside your directories.
autoAuto-approves with background safety checks against your request.
dontAskAuto-denies anything not pre-approved. A refusal here is the mode, not a rule.
bypassPermissionsSkips prompts. Isolated environments only.

The rule that is accepted and never consulted

This one deserves its own section because it fails silently and looks like a Claude Code bug. File permissions are checked against Edit(path) and Read(path) rules only.

Wrong. Accepted, warned about at startup, and never enforced.
{
  "permissions": {
    "deny": ["Write(docs/**)", "Glob(secrets/**)"]
  }
}
Right. Edit covers every file-editing tool; Read covers file discovery.
{
  "permissions": {
    "deny": ["Edit(docs/**)", "Read(secrets/**)"]
  }
}

Two more details that decide real cases. Deny and ask rules match a directory name at any depth below the current directory, so Read(secrets/**) also covers nested copies. And when Claude touches a symlink, both the link and its target are checked: a symlink pointing at a denied file is itself denied, while an allow rule falls back to prompting rather than silently permitting.

The sandbox changes the shape of this

Claude Code ships an OS-level sandbox for the Bash tool. Turned on, it stops asking about each command and instead enforces a filesystem and network boundary that applies to every command and its child processes. That is a better trade than approving commands one at a time, and it produces a different failure message: a command that works in your shell fails inside the session because it wrote outside the working directory or reached a domain you have not allowed.

Sandbox requirements as of August 2026.

PlatformRequirement
macOSNothing to install. Uses the built-in Seatbelt framework.
Linux and WSL2bubblewrap and socat, plus an optional seccomp filter
Native WindowsNot supported. Run Claude Code inside WSL2.
Linux and WSL2 setup, then check from inside a session.
sudo apt-get install bubblewrap socat     # Ubuntu and Debian
# sudo dnf install bubblewrap socat       # Fedora

# Ubuntu 24.04 and later restrict unprivileged user namespaces
sysctl kernel.apparmor_restrict_unprivileged_userns   # 1 means you need an AppArmor profile for bwrap

# then, inside Claude Code
# /sandbox   opens Mode, Overrides, Network, and a Dependencies tab if anything is missing

Questions people ask

Either the operating system refused, which shows as EACCES or EPERM with no explanation, or the Claude Code permission system refused, which always comes with a reason. Raw errno means fix file ownership. A reasoned refusal means a deny rule, an ask rule, the permission mode, or a PreToolUse hook.

Check ownership with ls -la. Root-owned files in your project or npm tree, usually from an earlier sudo npm install -g, are the common cause. Take ownership back with chown, use a user-writable npm prefix, and make sure ~/.local is writable for the native installer.

macOS protects Desktop, Documents, and Downloads, and the grant belongs to the process that is running. The tell is Operation not permitted on a file you can open yourself. Approve the prompt, grant your terminal access in System Settings, or move the project to somewhere ordinary such as ~/code.

Run /permissions to see the resolved rules and which settings file each came from. Rules evaluate deny, then ask, then allow, so a broad deny beats a narrower allow and you need to narrow the deny rather than add an exception. Also check /hooks: a PreToolUse hook that exits 2 blocks the call before rules are evaluated.

Most likely you wrote it against a tool that file permission checks do not consult. Only Edit(path) and Read(path) rules are checked for file paths; Write, NotebookEdit, MultiEdit, and Glob path rules are accepted, warned about at startup, and never enforced. Use Edit(docs/**) instead of Write(docs/**).

No. It creates root-owned files that break later operations for your user, and the resulting errors give no hint about the cause. If a directory is not writable, change its ownership once with chown rather than running the tool as root.

It is scoped to the directory you launched it from. Use --add-dir at startup or /add-dir in a session to grant access, or /cd to relocate the session, which also loads that directory CLAUDE.md.

Not natively. The Bash sandbox runs on macOS, Linux, and WSL2, and on Windows you run Claude Code inside a WSL2 distribution. On Linux and WSL2 it needs bubblewrap and socat installed; the /sandbox panel lists anything missing.

Sources

Every figure above was read from these pages on August 2026. Vendors reprice without notice; if you find a stale number, tell us.

  1. Claude Code: configure permissions
  2. Claude Code: the sandboxed Bash tool
  3. Claude Code settings reference
Try it

Isolation without
the prompts.

Continuum runs each session in its own worktree, so an agent working freely can only affect one branch.

free app · your subscriptions · local-first