Claude Code Permissions: I Have No Idea What I'm Doing
January 2026 update: The latest findings and a practical configuration guide are in “Claude Code Settings: Expectations vs. Reality.”
I started messing with Claude Code’s settings files to stop the constant permission prompts, and ended up in a rabbit hole.
Conclusion: when the official docs say permissions “can be bypassed” and call them “tricky,” perfect control is probably not on the table.
What I Wanted
- Stop being asked every time Claude Code reads a project file
- Stop being asked every time it runs a Docker command
- But still block access to production environments
First Attempt
Added allow rules to .claude/settings.json:
{
"permissions": {
"allow": [
"Read(*)",
"Bash(docker *)",
"Bash(docker-compose *)"
]
}
}
Should’ve worked.
Where It Got Ugly
After working for a while, I noticed settings.local.json had ballooned:
{
"permissions": {
"allow": [
"Bash(echo:*)",
"Bash(npx playwright install chromium)",
"Bash(curl:*)",
"Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\")",
"Bash(npx playwright test:*)",
"Bash(node test-login.js:*)",
"Bash(npm install:*)",
"Bash(docker exec:*)",
"Bash(docker-compose down:*)",
"Bash(__NEW_LINE__ echo \"=== POST /reports ===\")",
"Bash(__NEW_LINE__ echo -e \"\\n\\n=== POST /messages ===\")"
]
}
}
The Problems
- A JWT token is recorded in plain text (expired, but still not a good pattern)
- Full paths are in there (
c:\Users\username\...) - Echo statements and one-liners — meaningless entries piling up
- Internal representation
__NEW_LINE__showing up raw
The Pattern Matching Mystery
Bash(npm *) is in my settings, but Bash(npm run build:*) still gets appended.
Things I Tried
Does spacing matter?
// Style seen in articles
"Bash(npm install*)"
// My style
"Bash(npm install *)"
Trying Both Forms
"Bash(npx *)",
"Bash(npx:*)"
Still didn’t work.
Dealing with __NEW_LINE__
Multi-line commands store their internal representation literally, so I tried:
"Bash(__NEW_LINE__*)"
That didn’t help much either.
Where I Ended Up
After piling on entries, this is what the config looked like:
{
"permissions": {
"allow": [
"Read(*)",
"Write(*)",
"Bash(docker *)",
"Bash(docker-compose *)",
"Bash(docker compose *)",
"Bash(git *)",
"Bash(npm *)",
"Bash(npx *)",
"Bash(node *)",
"Bash(php *)",
"Bash(composer *)",
"Bash(cat *)",
"Bash(ls *)",
"Bash(dir *)",
"Bash(find *)",
"Bash(grep *)",
"Bash(tar *)",
"Bash(powershell *)",
"Bash(powershell.exe *)",
"Bash(npx playwright test:*)",
"Bash(npm run build:*)",
"Bash(tar:*)",
"Bash(docker exec:*)"
],
"deny": [
"Bash(*https://example.com*)",
"Bash(*http://example.com*)",
"Bash(*curl*example.com*)",
"Bash(*scp*)",
"Bash(*ssh*example.com*)",
"Bash(*rsync*)"
]
}
}
Got to a state where it “mostly doesn’t ask anymore.”
But the list kept growing:
"Bash(__NEW_LINE__ echo \"*\")",
"Bash(__NEW_LINE__ echo \"=== * ===\")",
"Bash(__NEW_LINE__ echo \"\")",
"Bash(docker cp:*)",
"Bash(docker-compose restart:*)",
"Bash(__NEW_LINE__ echo \"=== Some API ===\")"
Reddit Confirmed I’m Not Alone
Everyone is dealing with the same thing.
What the Official Docs Say
Settings File Priority
Per the official docs, settings files have a hierarchy:
~/.claude/settings.json— user-level (applies to all projects).claude/settings.json— project settings (shared with team, tracked in git).claude/settings.local.json— personal overrides (gitignored)- Managed settings (Enterprise) — highest priority
The reason settings.local.json was growing: every time I clicked “Allow always” on a prompt, it appended to this file.
Official Example
{
"permissions": {
"allow": [
"Bash(npm run lint)",
"Bash(npm run test:*)",
"Read(~/.zshrc)"
],
"deny": [
"Bash(curl:*)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
}
}
And the key note:
Bash rules use prefix matching, not regex
Bash patterns are prefix matches and can be bypassed
From the FAQ:
Wildcard syntax can be tricky - test permissions thoroughly.
The official docs admit it can be bypassed and call it tricky.
Realistic Options
1. Allow everything, deny what matters
{
"permissions": {
"allow": [
"Read(*)",
"Write(*)",
"Bash(*)"
],
"deny": [
"Bash(*production-domain*)",
"Bash(*scp *)",
"Bash(*rsync *)"
]
}
}
Though deny can also be bypassed.
2. Launch with --dangerously-skip-permissions
claude --dangerously-skip-permissions
Skips all confirmations. The name is terrifying, but apparently realistic for local development.
3. Stop auto-appending
claude config set --global autoAddPermissions false
Stops the automatic additions. Handle prompts case-by-case with “Allow once.”
4. Network-level restrictions
Block production access via the hosts file, a firewall, or Docker network settings — not through Claude Code at all. This is the most reliable approach.
5. Use the sandbox feature
Launch with claude --sandbox to restrict filesystem and network access at the OS level (Linux bubblewrap / macOS seatbelt).
{
"sandbox": {
"enabled": true,
"excludedCommands": ["docker"],
"network": {
"allowUnixSockets": ["~/.ssh/agent-socket"]
}
}
}
Pre-define allowed file paths and domains. More reliable than pattern matching on permission rules.
Summary
Claude Code’s permission system:
- Pattern matching behavior isn’t well-documented
- Internal representations like
__NEW_LINE__get recorded literally - Wildcard behavior is inconsistent
- Rules in
allowmay still trigger prompts - The official docs admit it can be bypassed
Forget perfect control through settings files. Using --dangerously-skip-permissions, network-level restrictions, or --sandbox is the practical path.
Honest Take
I have no idea what I’m doing. It just doesn’t listen.
Follow-up: WebFetch Wildcard Issue
I hit a separate problem where WebFetch(domain:*) wildcards don’t work — that’s also apparently a known bug.
I built a custom skill to work around it, detailed in a separate post.
→ I Built a WebFetch Alternative Skill for Claude Code