Audit Claude Code permission allow/deny/ask lists across all settings files. Classify issues by risk, suggest tightening, and interactively apply fixes. Can also discover permissions for new CLI tools.
Mode Selection
Parse the first argument to determine the mode:
global,project,all, or no argument → Audit mode (Phases 1-4 below)discover <tool-name>→ Discover mode (see Discover Mode section at the end)
Permission Model Reference
Claude Code has three permission arrays, evaluated in order: deny → ask → allow. First match wins.
| Array | Behavior |
|---|---|
allow | Auto-approved — no prompt |
ask | Always prompts for confirmation |
deny | Auto-rejected — tool cannot be used at all |
Anything not matching any array falls through to the defaultMode setting. Use the right array for the intent:
- allow — safe, read-only, or frequently-used commands (linters, test runners, git log)
- ask — commands that should succeed but need human review each time (git commit, git push, deployments)
- deny — commands that should never execute, even if explicitly requested (force push, rm -rf /)
Phase 1: Discovery
Read all three settings files and detect the project type.
Settings Files
Read each file. If a file doesn't exist, note it and continue.
- Global:
~/.claude/settings.json - Project shared:
.claude/settings.json(in project root) - Project local:
.claude/settings.local.json(in project root)
Extract permissions.allow, permissions.deny, and permissions.ask arrays from each. Also note the permissions.defaultMode value if set — it affects the overall security posture (see below). Ignore all other fields (env, hooks, model, statusLine, spinnerVerbs, etc.) — they are out of scope and must never be modified.
Default Mode Check
Read permissions.defaultMode from each file. Surface the value in the Phase 4 summary. Flag if set to a permissive mode:
| Mode | Risk | Note |
|---|---|---|
"default" or absent | OK | Standard behavior — prompts on first use |
"plan" | OK | Requires plan approval |
"dontAsk" | OK | Auto-denies unless pre-approved in allow rules |
"bypassPermissions" | CRITICAL | All permission rules are ignored — every tool auto-approved |
"acceptEdits" | HIGH | File edits auto-approved without review |
Do not modify defaultMode — only surface it as informational context.
Project Type Detection
Check for indicator files in the project root to determine project type(s). A project can have multiple types.
| Indicator | Type |
|---|---|
pyproject.toml + uv.lock | Python/uv |
pyproject.toml + poetry.lock | Python/Poetry |
pyproject.toml (no uv.lock or poetry.lock) | Python (generic) |
requirements.txt or setup.py (no pyproject.toml) | Python (pip) |
package.json + package-lock.json | Node/npm |
package.json + yarn.lock | Node/yarn |
package.json + pnpm-lock.yaml | Node/pnpm |
package.json + bun.lock or bun.lockb | Node/bun |
Cargo.toml | Rust |
go.mod | Go |
pom.xml | Java/Maven |
build.gradle or build.gradle.kts | Java/Gradle |
*.csproj or *.sln | C#/.NET |
Gemfile | Ruby |
composer.json | PHP |
*.tf files | Terraform |
mise.toml or .mise.toml | Mise |
docker-compose.yml or docker-compose.yaml or compose.yml | Docker |
.github/ directory | GitHub |
Makefile | Make |
If mise is detected, run mise tasks ls to enumerate available task names. These feed into Phase 3 suggestions.
Scope Filtering
If the argument is one of the audit scopes:
global— only audit~/.claude/settings.jsonproject— only audit.claude/settings.jsonand.claude/settings.local.jsonall(default) — audit all three files
No Project Directory
If run outside a project (no .claude/ directory in the working directory), gracefully skip project settings files and only audit the global settings. Note this in the Phase 4 summary. Project-type detection and project-type-aware suggestions are also skipped in this case.
Phase 2: Audit
Analyze every entry in every allow/deny/ask list. Classify each finding by risk level.
Risk Levels
| Risk | Criteria | Examples |
|---|---|---|
| CRITICAL | Allows arbitrary execution or data destruction (in allow/ask) | Bash(*), Bash(sudo *), Bash(rm -rf *), credentials in patterns |
| HIGH | Broad wildcard on risky command family (in allow/ask) | Bash(docker compose *), Bash(find *), Bash(git *) |
| MEDIUM | Deprecated syntax, broader than necessary, duplicates, built-in overlap | :* patterns, redundant entries, Bash(grep *) |
| LOW | Hygiene/informational | Stale WebFetch domains, subset duplicates, style inconsistencies |
Note: Broad patterns in deny are safety features, not risks — see check 1 for array-context-aware classification.
Checks to Perform
Run every check below against every entry. One entry can trigger multiple findings. When multiple checks flag the same entry, consolidate into a single finding using the highest severity and combining the rationale (e.g., an entry that is both overly permissive and in the wrong array → one finding, not two).
1. Overly Permissive Patterns
Flag entries in allow or ask that grant broad access to command families with known destructive subcommands. Skip this check for deny entries — broad patterns in deny are safety features, not risks.
| Pattern | Risk (in allow) | Risk (in ask) | Why |
|---|---|---|---|
Bash(*) | CRITICAL | HIGH | Allows any command |
Bash(sudo *) | CRITICAL | HIGH | Root access |
Bash(rm -rf *) | CRITICAL | HIGH | Arbitrary deletion |
Bash(docker compose *) or Bash(docker compose:*) | HIGH | MEDIUM | Includes down -v, rm, exec |
Bash(find *) or Bash(find:*) | HIGH | MEDIUM | -exec allows arbitrary execution |
Bash(git *) or Bash(git:*) | HIGH | MEDIUM | Includes destructive ops (reset, clean, push --force) |
Bash(npm *) or Bash(npm:*) | HIGH | MEDIUM | npm exec allows arbitrary execution |
Bash(PGPASSWORD=* psql *) or Bash(PGPASSWORD=* psql:*) | CRITICAL | CRITICAL | Arbitrary SQL execution. If password is a literal (not *), also a credential exposure issue (see check 4) |
Bash(terraform *) or Bash(terraform:*) | HIGH | MEDIUM | terraform apply, terraform destroy can modify/delete infrastructure |
Bash(kubectl *) or Bash(kubectl:*) | HIGH | MEDIUM | kubectl delete, kubectl exec can destroy resources or run arbitrary commands |
Bash(make *) or Bash(make:*) | HIGH | MEDIUM | Make targets are arbitrary shell commands — equivalent to Bash(*) for that target |
Bash(yarn *) or Bash(yarn:*) | HIGH | MEDIUM | yarn dlx allows arbitrary execution; yarn run can execute any package.json script |
Bash(pnpm *) or Bash(pnpm:*) | HIGH | MEDIUM | pnpm dlx/pnpm exec allows arbitrary execution; same risk profile as npm/yarn |
Risk is lower in ask (user still confirms each use) but broad ask patterns still warrant tightening.
2. Deprecated Syntax
The legacy :* suffix syntax is deprecated. The current syntax uses a space: *.
Bash(cmd:*)should beBash(cmd *)- Word boundary semantics:
Bash(ls *)matchesls -labut NOTlsof.Bash(ls*)matches both. - Flag ALL
:*entries as MEDIUM risk
Caution — commands with literal colons: Many tools use colons in their command names: mise tasks (mise run fe:lint), Maven goals (mvn dependency:tree), npm scripts (npm run build:prod), Laravel artisan (php artisan migrate:fresh), Rake tasks (rake db:migrate), Gradle subprojects (gradle :app:build). A pattern like Bash(npm run build:*) looks like it should match npm run build:prod, but Claude Code interprets the trailing :* as the deprecated wildcard suffix — maki