Help us improve
Share bugs, ideas, or general feedback.
From kookr-toolkit
Claude Code permission system — modes, allow/deny/ask rules, pattern syntax, settings precedence, and optimal configuration for minimal prompts with safety guardrails
npx claudepluginhub kookr-ai/kookr --plugin kookr-toolkitHow this skill is triggered — by the user, by Claude, or both
Slash command
/kookr-toolkit:claude-code-permissionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Configuring Claude Code permissions to reduce or eliminate prompts
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
--settings, --allowedTools, --disallowedTools interact| Mode | defaultMode value | Auto-approves | Notes |
|---|---|---|---|
| Ask Permissions | default | Read only | Most restrictive |
| Auto Accept Edits | acceptEdits | Read + Edit | Recommended baseline |
| Plan Mode | plan | Read only, blocks all writes/execution | Exploration only |
| Don't Ask | dontAsk | Only tools in allow list | Fully non-interactive; unlisted tools silently skipped |
| Bypass Permissions | bypassPermissions | Everything | No safety net, not recommended |
First match wins, checked in this order:
1. deny → BLOCKED unconditionally
2. ask → user PROMPTED for confirmation
3. allow → APPROVED without prompting
4. mode → fallback to defaultMode behavior
deny always beats allow. Safe to broadly allow + specifically deny.
"Bash" // ALL bash commands (bare tool name)
"Write" // ALL file writes
"Read" // ALL file reads
"Agent" // ALL agent spawns
"WebFetch" // ALL web fetches
"Bash(npm run build)" // exact command only
"Bash(npm run *)" // npm run + any args
"Bash(git push *)" // git push with any args
"Bash(git push --force *)" // force push specifically
"Bash(git push * --force*)" // --force anywhere in args
"Bash(* --version)" // any command with --version
Word boundary: Space before * enforces word boundary.
Bash(ls *) matches ls -la but NOT lsofBash(ls*) matches both ls -la AND lsof"Read(./.env)" // relative to current dir
"Read(./.env.*)" // glob pattern
"Read(~/.ssh/**)" // home directory recursive
"Read(//usr/local/secret)" // absolute path (// prefix)
"Read(/src/**/*.ts)" // relative to project root
"WebFetch(domain:github.com)" // specific domain
"mcp__playwright__browser_navigate" // specific tool
"mcp__playwright__*" // all tools from server
--settings, --allowedTools, --disallowedTools, --permission-mode.claude/settings.local.json (gitignored).claude/settings.json (version-controlled)~/.claude/settings.json--settings merges with loaded settings (does not replace them).
--setting-sources "" skips all file-based settings.
In --print mode (non-interactive), permissions differ from interactive:
| Rule type | Behavior in --print |
|---|---|
deny | Enforced (tool blocked) |
ask | Effectively blocks (no way to prompt) |
allow | Works as expected |
| Permission mode | Mostly irrelevant — tools auto-approved regardless of mode |
plan mode | Still blocks writes/execution |
Implication: --print mode tests validate deny/allow rules but NOT permission mode enforcement. Interactive mode (tmux, direct CLI) is where modes matter.
Wrong approach — whack-a-mole, prompts for every new command:
{
"allow": [
"Bash(git status*)",
"Bash(git log *)",
"Bash(npm *)",
"Bash(ls *)",
// ... 60+ patterns and growing
]
}
Correct approach — bare tool name + targeted deny:
{
"allow": ["Bash"],
"deny": [
"Bash(rm -rf /)",
"Bash(git push --force *)",
"Bash(git reset --hard*)"
]
}
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Bash",
"Read",
"Edit",
"Write",
"WebFetch",
"WebSearch",
"Agent",
"NotebookEdit",
"EnterPlanMode",
"ExitPlanMode",
"EnterWorktree",
"ExitWorktree",
"TaskCreate",
"TaskUpdate",
"TaskOutput",
"TaskStop"
],
"deny": [
"Bash(rm -rf /)",
"Bash(rm -rf /*)",
"Bash(rm -rf ~)",
"Bash(rm -rf ~/*)",
"Bash(rm -rf .)",
"Bash(rm -rf ./*)",
"Bash(git push --force *)",
"Bash(git push * --force*)",
"Bash(git push -f *)",
"Bash(git push * -f)",
"Bash(git push * -f *)",
"Bash(git reset --hard*)",
"Bash(git clean -f*)",
"Bash(git checkout -- .)",
"Bash(git restore .)",
"Bash(shutdown *)",
"Bash(reboot *)",
"Bash(mkfs *)",
"Bash(dd if=*)",
"Read(./.env)",
"Read(./.env.*)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)"
],
"ask": []
}
}
A supervisor that spawns Claude Code children (e.g. via claude --settings <path> ...) typically passes hook definitions only via --settings and lets permission rules come from the standard cascade. Workers then inherit permissions from:
~/.claude/settings.json (user global).claude/settings.json (project shared).claude/settings.local.json (project local)To change permissions for all spawned workers, edit ~/.claude/settings.json. The --settings flag merges additively — permissions in the per-launch file are unioned with the cascade above, not replaced.
# Spawn isolated Claude instance with specific settings
env -u ANTHROPIC_API_KEY claude \
--print \
--setting-sources "" \
--settings '{"permissions":{"defaultMode":"acceptEdits","allow":["Bash"],"deny":["Bash(rm -rf /)"]}}' \
--model haiku \
--output-format json \
"Run: echo test"
# Key flags:
# --setting-sources "" → skip all settings files
# --settings <json> → inject test config
# --bare → skip hooks/LSP/OAuth (needs API key)
# --output-format json → structured output with metadata
# env -u ANTHROPIC_API_KEY → force OAuth when API key has low credits
"Bash" (bare) in allow list auto-approves ALL bash commandsdeny rules override allow — safe to broadly allow + specifically denydeny rules enforced in all modes including --printask rules effectively block in --print mode (no way to prompt)--settings merges with file-based settings (does not replace)plan mode restricts available tools even in --print moderm -rf / independently of permissions)bypassPermissions mode does NOT respect deny rules reliably