[BUG] Allowed write commands (e.g. mkdir) still prompt for permission

Status Open
Reported on v2.1.3
Maintainer reply None cached
Activity 7 comments · opened Jan 10, 2026

Summary

Bash commands in the allow list work for read-only and network commands, but still prompt for filesystem write commands. This creates an inconsistent and counterintuitive security model.

Minimal Reproduction

Repo with repro: https://github.com/jameshfisher/claude-code-permissions-bug

  1. Clone the repo
  2. Run claude in the directory
  3. Ask Claude to run ls -laNo prompt
  4. Ask Claude to run curl -s -I https://example.comNo prompt
  5. Ask Claude to run mkdir -p /tmp/testPrompts for permission

Settings

.claude/settings.json:

{
  "permissions": {
    "allow": [
      "Bash(mkdir:*)",
      "Bash(ls:*)",
      "Bash(curl:*)"
    ]
  }
}

Actual Behavior

| Command | Type | Allow Rule | Result |
|---------|------|------------|--------|
| ls -la | Filesystem read | Bash(ls:*) | ✅ No prompt |
| curl -s -I https://example.com | Network | Bash(curl:*) | ✅ No prompt |
| mkdir -p /tmp/test | Filesystem write | Bash(mkdir:*) | ❌ Prompts |

Things That Don't Help

  • "sandbox": { "enabled": false } - still prompts
  • Restarting Claude Code after settings change - still prompts
  • The pattern appears correctly in /permissions UI - still prompts

Security Model Inconsistency

This is counterintuitive:

  • curl can exfiltrate data, download malware, make arbitrary HTTP requests → allowed
  • mkdir creates an empty directory → blocked

Not a Settings Override Issue

Verified no settings are overriding:

~/.claude/settings.json          → empty allow/deny lists
~/.claude/settings.local.json    → doesn't exist
~/.claude/managed-settings.json  → doesn't exist
.claude/settings.local.json      → doesn't exist

Related Issues

These are similar but not the same bug:

  • #13340 - piped commands, not single commands
  • #5140 - user-level settings not applied (project-level works for them)
  • #15421 - closed as dupe of #13340

Environment

  • macOS (Darwin 24.6.0)
  • Claude Code 2.1.3
  • Node.js v22

View original on GitHub ↗

7 Comments

github-actions[bot] · 7 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/15237
  2. https://github.com/anthropics/claude-code/issues/6527

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

jameshfisher · 7 months ago

I also looked for source code to debug this, or unit tests to add, but could find neither.

lucasmccomb · 7 months ago

Would love a fix for this :)

toppsdown · 7 months ago

I'm also running into this. Claude asks to run mkdir every time even though I have:

"Bash(mkdir *)",
"Bash(mkdir:*)",
StructByLightning · 6 months ago

I'm having the same issue, tried both of these

"Bash(mkdir *)",
"Bash(mkdir:*)",

as well as my exact command

Bash(mkdir -p 2026-02-05-responses)

None worked. My allowlist is configured in ~/.claude/settings.json, and inside CC the /permissions screen shows me the expected allow rule. I've included the full json below. I haven't tested every single command exhaustively, but CC seems to respect the allowlist for other write tools such as Write and all my Linear MCP tools.

I checked to make sure I wasn't being overridden by project-level settings, but deleting the project settings file didn't fix this.

I'm on Ubuntu 22.04, running CC v2.1.32 (native install) with Opus 4.6.

{
  "permissions": {
    "allow": [
      "mcp__playwright__*",
      "mcp__linear__*",
      "Bash(cd:*)",
      "Bash(echo:*)",
      "Bash(ls:*)",
      "Bash(pwd)",
      "Bash(cat:*)",
      "Bash(head:*)",
      "Bash(tail:*)",
      "Bash(wc:*)",
      "Bash(tr:*)",
      "Bash(cut:*)",
      "Bash(cmp:*)",
      "Bash(which:*)",
      "Bash(basename:*)",
      "Bash(dirname:*)",
      "Bash(realpath:*)",
      "Bash(readlink:*)",
      "Bash(stat:*)",
      "Bash(file:*)",
      "Bash(du:*)",
      "Bash(df:*)",
      "Bash(sleep:*)",
      "Bash(nl:*)",
      "Bash(grep:*)",
      "Bash(column:*)",
      "Bash(date)",
      "Bash(date +*)",
      "Bash(tree:*)",
      "Bash(rg:*)",
      "Bash(sort:*)",
      "Bash(find:*)",
      "Bash(jq:*)",
      "Bash(sed:*)",
      "Bash(git status:*)",
      "Bash(git log:*)",
      "Bash(git show:*)",
      "Bash(git diff:*)",
      "Bash(git ls-files:*)",
      "Bash(git ls-tree:*)",
      "Bash(git grep:*)",
      "Bash(git --no-pager status:*)",
      "Bash(git --no-pager log:*)",
      "Bash(git --no-pager show:*)",
      "Bash(git --no-pager diff:*)",
      "Bash(git --no-pager branch)",
      "Bash(npx tsc:*)",
      "Bash(npx eslint:*)",
      "Bash(npm run lint:*)",
      "Bash(npm run test:*)",
      "Edit",
      "Write",
      "Bash(xargs grep*)",
      "Bash(mkdir *)"
    ],
    "ask": [
      "Edit(./.env)",
      "Edit(./.env.*)",
      "Edit(./secrets/**)",
      "Edit(./.claude/**)",
      "Edit(./package.json)",
      "Edit(./package-lock.json)",
      "Edit(./yarn.lock)",
      "Edit(./pnpm-lock.yaml)",
      "Edit(./.gitignore)",
      "Edit(./Dockerfile)",
      "Edit(./docker-compose.*)",
      "Edit(./.github/**)",
      "Edit(./tsconfig.json)",
      "Edit(./next.config.*)",
      "Edit(./.eslintrc*)",
      "Edit(./tailwind.config.*)",
      "Edit(./postcss.config.*)",
      "Edit(./vite.config.*)",
      "Edit(./webpack.config.*)",
      "Write(./.env)",
      "Write(./.env.*)",
      "Write(./secrets/**)",
      "Write(./.claude/**)",
      "Write(./.github/**)",
      "Bash(rm:*)",
      "Bash(rmdir:*)",
      "Bash(kill:*)",
      "Bash(ps:*)",
      "Bash(top:*)",
      "Bash(curl:*)",
      "Bash(wget:*)",
      "Bash(git branch)",
      "Bash(git branch -a)",
      "Bash(git branch -r)",
      "Bash(git branch -v)",
      "Bash(git branch --list:*)",
      "Bash(chmod:*)",
      "Bash(chown:*)",
      "Bash(xargs:*)",
      "Bash(git branch -d:*)",
      "Bash(git branch -D:*)",
      "Bash(git branch -m:*)",
      "Bash(git branch -M:*)",
      "Bash(git branch --delete:*)",
      "Bash(date -s:*)",
      "Bash(date --set:*)"
    ],
    "deny": [
      "Bash(eval:*)",
      "Bash(dd:*)"
    ]
  },
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/activate-linear-skill.sh"
          }
        ]
      }
    ]
  }
}

Happy to provide additional info as needed.

masonelmore · 6 months ago

I bashed my head against this for over an hour. Wish I would have found this first. Very annoying.

yurukusa · 5 months ago

A PreToolUse hook auto-approves mkdir regardless of the permission system's behavior:

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
BASE=$(echo "$COMMAND" | sed 's/^\s*//' | awk '{print $1}')
case "$BASE" in
    mkdir|touch|ln)
        jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Safe write command auto-approved"}}'
        ;;
esac
exit 0
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/allow-mkdir.sh" }]
    }]
  }
}

mkdir, touch, and ln are auto-approved by the hook regardless of what the permission system does or doesn't match.