Claude Code Permission Errors: Fix Them Fast
What permission errors look like in Claude Code
Claude Code surfaces permission blocks in two forms. The first is a permission prompt: Claude wants to run a command, pauses, and asks you to approve, deny, or approve for the session. This is expected behaviour for tools in the "ask" category. The second is a hard block: Claude cannot proceed because a tool is in the deny list, and it reports this as a constraint rather than a question.
The exact message you see when a tool is blocked looks like:
I don't have permission to run: npm run build
The tool Bash(npm run build) is not in the allowed list.
Or for MCP tools:
I don't have permission to use: mcp__airtable__create_record
This tool requires explicit permission to use.
These messages point directly at the fix: the tool string that needs to be added to the allow list in your settings.json. Understanding how the allow list matching works is what makes the fix reliable. Permission blocks are one of several common Claude Code failures; fixing Claude Code errors fast covers the rest, from rate limits to MCP issues.
For the conceptual model of how Claude Code's permission system is structured, Claude Code permissions explained covers the three-tier model and the difference between settings.json and settings.local.json. This guide covers the failure modes specifically.
The allow list syntax
The allow list in .claude/settings.json (or .claude/settings.local.json for personal overrides) accepts three types of entries:
Bash glob patterns for shell commands:
{
"permissions": {
"allow": [
"Bash(npm run*)",
"Bash(npx *)",
"Bash(git status)",
"Bash(git diff*)"
]
}
}
Exact tool names for non-Bash tools:
{
"permissions": {
"allow": [
"Read",
"Write",
"Edit",
"Glob",
"Grep"
]
}
}
MCP tool references using the mcp__server__tool pattern:
{
"permissions": {
"allow": [
"mcp__airtable__list_records",
"mcp__airtable__create_record",
"mcp__github__get_pull_request"
]
}
}
The structure of a complete settings.json:
{
"permissions": {
"allow": [
"Bash(npm run*)",
"Bash(npx *)",
"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)",
"Read",
"Write",
"Edit",
"Glob",
"Grep"
],
"deny": [
"Bash(git push --force*)",
"Bash(rm -rf*)",
"Bash(sudo *)"
]
}
}
The four most common allow list mistakes
1. Missing the Bash() wrapper
The allow list distinguishes between Claude Code tools and Bash commands. Read, Write, Edit are tool names entered directly. Shell commands must be wrapped in Bash():
Broken:
"allow": ["npm run build"]
Fixed:
"allow": ["Bash(npm run build)"]
Without the Bash() wrapper, the entry is never matched because Claude Code looks for a tool named npm run build, which does not exist.
2. Glob pattern syntax differences
Claude Code's allow list uses a glob syntax that is similar to, but not identical to, shell glob expansion. The critical difference: * in the allow list matches zero or more characters including spaces, but it does NOT match across path separators in Bash command strings the way shell globs do for filenames.
The practical implication: use Bash(npm run*) to match npm run build, npm run test, npm run lint. Do not use Bash(npm*) expecting it to match all npm commands including npm install and npm ci. The broader pattern does work, but being too broad defeats the purpose of the allow list.
A common failure: the allow list has Bash(npx tsx*) but the command is npx tsx scripts/migrate.ts. The * at the end matches the space and the rest of the string, so this does work. What does NOT work is Bash(npx tsx) with no trailing * when the actual command has arguments. The exact string npx tsx does not match npx tsx scripts/migrate.ts.
Rule: if a command ever has arguments, add * at the end of the allow pattern.
"Bash(npx tsx*)" matches npx tsx, npx tsx scripts/migrate.ts, npx tsx --version
"Bash(npx tsx)" matches ONLY the exact string npx tsx with no arguments
3. Wrong file for the override
Claude Code loads settings from two files:
.claude/settings.json(project-level, checked into git).claude/settings.local.json(personal overrides, git-ignored)
Both files have the same structure. Personal overrides in settings.local.json take precedence over settings.json for conflicting entries.
The failure mode: you add an allow entry to settings.local.json but the project settings.json has a deny entry for the same pattern. The deny entry wins (deny takes precedence over allow). Check both files when an allow entry does not seem to take effect.
# Check both files
cat .claude/settings.json
cat .claude/settings.local.json
4. Not restarting the session after editing settings
Claude Code reads settings.json at session start. Edits to the file during an active session do not take effect until you start a new session. This is the most frequent cause of "I added it to the allow list and it still doesn't work."
After editing any settings file, close the current session and open a new one. The new allow entries will be active immediately.
Fixing common permission errors by tool type
Bash commands timing out on approval prompts
If Claude is constantly pausing to ask for approval on commands you use in every session, add those commands to the allow list in settings.local.json (personal override, not the shared project settings). This keeps your personal workflow smooth without changing the project's security baseline for teammates.
{
"permissions": {
"allow": [
"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(git add*)",
"Bash(git commit*)",
"Bash(npm run dev*)",
"Bash(npm run build*)",
"Bash(npm run test*)",
"Bash(npm run lint*)",
"Bash(npx *)",
"Bash(ls*)",
"Bash(find . *)",
"Bash(grep *)"
]
}
}
This covers the commands most developers run dozens of times per session. Adding them to settings.local.json is safe because the file is git-ignored and only affects your environment.
MCP tool permission errors
MCP tools require explicit allow entries because they can interact with external services. A common pattern when first connecting an MCP server: all its tools require approval.
To allow an entire MCP server's tools, list each one explicitly. Claude Code does not support mcp__server__* wildcard matching in the allow list. Each tool must be listed individually:
{
"permissions": {
"allow": [
"mcp__airtable__list_bases",
"mcp__airtable__list_tables",
"mcp__airtable__list_records",
"mcp__airtable__search_records",
"mcp__airtable__get_record",
"mcp__airtable__create_record",
"mcp__airtable__update_records",
"mcp__airtable__delete_records"
]
}
}
To get the exact tool names for a new MCP server, let Claude attempt to use the server and note the tool names in the permission prompts. They follow the mcp__<server-name>__<tool-name> pattern where both names are lowercase with underscores.
Write and Edit permissions for specific directories
By default, Write and Edit require approval. To allow writes only in specific directories:
{
"permissions": {
"allow": [
"Write(src/**)",
"Write(tests/**)",
"Edit(src/**)",
"Edit(tests/**)"
],
"deny": [
"Write(.env*)",
"Write(*.pem)",
"Write(secrets/**)"
]
}
}
The path glob in Write(src/**) uses ** to match any depth of subdirectory under src/. A single * matches only files directly in src/ with no subdirectory traversal.
Destructive command blocks
Some commands should always be blocked regardless of context. Put these in the deny list in the shared settings.json so they apply to every team member:
{
"permissions": {
"deny": [
"Bash(git push --force*)",
"Bash(git reset --hard*)",
"Bash(rm -rf*)",
"Bash(sudo *)",
"Bash(chmod 777*)",
"Bash(curl * | bash)",
"Bash(wget * | bash)"
]
}
}
These deny entries survive even if someone adds a broad allow entry. The deny list takes precedence.
The debugging workflow for an allow entry that does not match
When you add an allow entry and Claude still prompts for approval, work through this sequence:
1. Check the exact command Claude is trying to run.
When Claude prompts for approval, the prompt shows the exact command string. Copy it. This is what you need to match.
Example prompt: Claude wants to run: npx tsx --tsconfig tsconfig.scripts.json scripts/migrate.ts
2. Write the allow pattern from the exact command.
For the example above: Bash(npx tsx*)
This covers npx tsx followed by any arguments including --tsconfig tsconfig.scripts.json scripts/migrate.ts.
3. Check for conflicts in both settings files.
grep -r "deny" .claude/settings.json .claude/settings.local.json
If the command matches a deny pattern, adding an allow entry will not override it. Remove or narrow the deny pattern first.
4. Validate the settings files are valid JSON.
Claude Code silently ignores settings.json files that are not valid JSON. The file parsing fails, permissions default to the baseline, and your allow entries are never read.
node -e "JSON.parse(require('fs').readFileSync('.claude/settings.json', 'utf8'))" && echo "valid"
If this returns an error, fix the JSON before debugging further. A single trailing comma or missing quote will break the entire file.
5. Start a new session.
After confirming the entry is correct and the file is valid JSON, close the current Claude Code session and start a new one. Check whether the command now runs without prompting.
Building a baseline settings.json for a new project
This is a starting-point settings.json for a Node.js TypeScript project. Adjust the allow list to match your project's actual command set:
{
"permissions": {
"allow": [
"Bash(npm run*)",
"Bash(npm install*)",
"Bash(npm ci)",
"Bash(npx *)",
"Bash(node *)",
"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(git stash*)",
"Bash(ls*)",
"Bash(find . *)",
"Bash(grep *)",
"Bash(cat *)",
"Bash(echo *)",
"Bash(mkdir -p *)",
"Bash(cp *)",
"Bash(mv *)",
"Read",
"Write",
"Edit",
"Glob",
"Grep"
],
"deny": [
"Bash(git push --force*)",
"Bash(git reset --hard*)",
"Bash(rm -rf /)",
"Bash(sudo *)",
"Bash(curl * | bash)",
"Bash(wget * | bash)"
]
}
}
For a Python project, replace the npm/npx/node entries with Python equivalents:
{
"permissions": {
"allow": [
"Bash(python *)",
"Bash(python3 *)",
"Bash(pip install*)",
"Bash(pip3 install*)",
"Bash(pytest*)",
"Bash(flask *)",
"Bash(uvicorn *)",
"Bash(alembic *)"
]
}
}
The minimal allow list philosophy
A narrow allow list is safer than a broad one, but there is a practical tradeoff. An allow list that requires approval for every second command interrupts the workflow to the point where the interruptions become the main experience. Claude Code's value is autonomous task completion. Too much friction defeats that.
The practical balance: allow the commands in your normal development workflow without restriction, and apply approval prompts to commands that have significant side effects (external API calls, database modifications, file deletions). The deny list handles the truly dangerous commands unconditionally.
For the full mental model of Claude Code's security architecture, Claude Code permissions explained covers the three-tier system, the settings.json vs settings.local.json split, and the design philosophy behind the permission model.
Get Claudify. The Claudify settings.json template includes a tuned allow list for TypeScript, Python, and full-stack projects, plus the deny list for destructive commands, ready to paste in and customise.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify