
Tool permissions and you
Allow, ask, deny. The three-tier model that prevents most of the “wait I didn’t mean that” moments.
Why permissions matter
Claude Code can run shell commands, modify files, make network requests. By default it asks before each one. Safe but slow. I drift toward “allow more” over time as the prompts get tedious. That's fine for cheap reversible operations and dangerous for everything else.
The cost of getting permissions wrong is asymmetric. An over-strict config wastes a few seconds per prompt. An over-loose config can force-push to main, delete the wrong directory, or pipe a curl into shell. Minutes of damage, hours of recovery.
Good news. Permissions live in one JSON file (.claude/settings.json) and the right model is short. Three tiers, around 25 entries total, audit quarterly. Once it's set up right, the prompts are rare enough that I actually read them when they appear.
I made this the capstone of Foundations because it ties everything else together. Model choice (Foundations 09), agent authority (Foundations 16), background runs (Foundations 13), diff review (Foundations 14). They all interact with permissions. The permissions config is the layer that makes the rest of the discipline mechanical.
Allow / ask / deny
Three tiers. Each one has a clear filling rule.
| Tier | What goes here | Example entries |
|---|---|---|
| Allow | Boring, reversible, read-only. Runs without prompt. | ls, git status, git diff, npm test, cat, Read, Edit |
| Ask | Mutates state outside cwd, makes network calls, deletes/moves. Prompts every time. | git commit, git push, rm, mv, curl, npm publish |
| Deny | Irreversible, never-do, even with confirmation. Refuses without prompt. | git push --force to main/master, rm -rf /, curl <url> | sh |
One question per command. What's the cost if this fires when I didn't mean for it to?
- Cost is zero or trivial (re-run the command, undo the file edit) → Allow.
- Cost is minutes to hours of cleanup, recoverable → Ask.
- Cost is data loss, public exposure, money-move, or anything that can't be cleanly reversed → Deny.
Scope matters as much as content. Project-local settings (.claude/settings.json) override global (~/.claude/settings.json). Personal prefs go in .claude/settings.local.json which is gitignored. I've watched people put personal prefs in the wrong file and either leak them to teammates or lose them on a machine swap. Get the scope right.
Three permissions failure patterns
Allow-everything
Ask-everything
Permissions in the wrong scope
Pattern across all three. Permissions decay without an audit. The right config in January is wrong by July from “Always” drift, scope confusion, and reflexive approvals.
My quarterly permissions audit
The check that keeps allow / ask / deny aligned with reality.
Audit my Claude Code permissions.
Read .claude/settings.json (project) and .claude/settings.local.json
(personal) and ~/.claude/settings.json (global).
For each permission entry:
1. **Scope check** — is this in the right file? (Project-shared
in settings.json, personal in settings.local.json, global only
if you genuinely want it in every project.)
2. **Allow list** — for each entry, ask: would I be OK if this
ran with no prompt right now, on the most sensitive file in
this repo? If no, move to ask.
3. **Ask list** — for each entry, ask: do I get prompted for
this so often that I click through reflexively? If yes, the
pattern is too broad — narrow it.
4. **Deny list** — is anything irreversible NOT covered? Specifically:
force-push to main, rm -rf parent dirs, prod deploys, anything
touching credentials.
Output a table of recommended changes (move-to-allow, move-to-ask,
add-to-deny) with reasoning per row.Catches Always-drift, scope errors, missing deny entries.
My first audit on any long-running project usually surfaces 5-10 entries that have drifted into allow via “Always” approvals. Most should move back to ask. About 10 minutes of work for a quarter of protection.
Annotated transcript: settings.json walk-through
Real session of mine. Bootstrap the three-tier permissions for a Next.js project, save the config, test all three tiers with real commands.
Three-tier model, drafted JSON, four tests verifying allow runs invisible, ask prompts, deny refuses outright.
- operator
I want to set up sane permissions for the Website repo. I trust npm + git read commands but want a checkpoint on anything that pushes or deploys. Walk me through settings.json.
- agent
Three-tier model. Allow / Ask / Deny.
Allow (no prompt, just runs):
- Bash(ls), Bash(pwd), Bash(cat), Bash(git status), Bash(git diff)
- Bash(npm install), Bash(npm test), Bash(npm run build)
- Read, Write, Edit (the file tools)Ask (prompts every time):
- Bash(git commit), Bash(git push) — anything mutating remote
- Bash(rm), Bash(mv) — anything deleting/moving
- Bash(curl*) — outbound network
- Bash(npm publish*) — package publicationDeny (never, even with a prompt):
- Bash(git push --force*) to main/master
- Bash(rm -rf /) and similar
- Bash(curl * | sh) and similar pipe-to-shell patternsProject-level at .claude/settings.json or .claude/settings.local.json. settings.local.json is gitignored — use that for personal preferences. settings.json is checked in for team-wide rules.
- operator
Show me the actual JSON.
- agent
Drafted at .claude/settings.json:
{ "permissions": { "allow": [ "Bash(ls*)", "Bash(pwd)", "Bash(cat *)", "Bash(git status)", "Bash(git diff*)", "Bash(git log*)", "Bash(npm install*)", "Bash(npm test*)", "Bash(npm run build)", "Read", "Write", "Edit" ], "ask": [ "Bash(git commit*)", "Bash(git push*)", "Bash(git checkout*)", "Bash(git merge*)", "Bash(rm*)", "Bash(mv*)", "Bash(cp *)", "Bash(curl*)", "Bash(wget*)", "Bash(npm publish*)" ], "deny": [ "Bash(git push --force*main*)", "Bash(git push --force*master*)", "Bash(rm -rf /*)", "Bash(curl *|*sh*)", "Bash(wget *|*sh*)" ] } }⚠️ Glob notes:
- Trailing * on Bash entries matches argument variations
- The deny pattern for force-push specifically guards main/master — feature branch force-push is in 'ask' not deny
- The pipe-to-shell deny catches the most common supply-chain attack patternReview before saving.
- operator
Save it. Now test: try to commit, try to push, try to rm a file.
- agent
Saved.
Test 1 —
git status→ ran without prompt ✓
Test 2 —git commit -m "test"→ prompted: 'Allow Bash(git commit -m "test")?' Y/N/Always. ✓
Test 3 —rm test.txt→ prompted: 'Allow Bash(rm test.txt)?' Y/N/Always. ✓
Test 4 —git push --force origin main→ DENIED outright, no prompt. ✓The three-tier behaves correctly. Allow runs invisible, ask prompts, deny refuses without prompting.
Audit reminder: every quarter, review the allow list. If you've added 'Always' approvals during the quarter, those will have been added to allow automatically — check whether they should stay or move back to ask.
Prompt kit
Three prompts I use: audit, bootstrap, narrow-scope.
Audit my Claude Code permissions.
Read .claude/settings.json (project) and .claude/settings.local.json
(personal) and ~/.claude/settings.json (global).
For each permission entry:
1. **Scope check** — is this in the right file? (Project-shared
in settings.json, personal in settings.local.json, global only
if you genuinely want it in every project.)
2. **Allow list** — for each entry, ask: would I be OK if this
ran with no prompt right now, on the most sensitive file in
this repo? If no, move to ask.
3. **Ask list** — for each entry, ask: do I get prompted for
this so often that I click through reflexively? If yes, the
pattern is too broad — narrow it.
4. **Deny list** — is anything irreversible NOT covered? Specifically:
force-push to main, rm -rf parent dirs, prod deploys, anything
touching credentials.
Output a table of recommended changes (move-to-allow, move-to-ask,
add-to-deny) with reasoning per row.Set up tool permissions for this project. The defaults should be:
- Allow: file reads, package install/test/build, git read-only operations
- Ask: anything that mutates remote state, deletes, makes network calls
- Deny: force-push to main/master, rm -rf parent dirs, pipe-to-shell
Adjust based on this project's specifics:
- <project-specific tools or paths the agent will use>
- <any commands that should be allow despite mutating, e.g.,
"deploy to staging is OK without prompt">
Draft .claude/settings.json. Don't apply yet — show me the file
and I'll review.I have <permission> in my global settings (~/.claude/settings.json).
I think it should be project-local instead.
Walk through:
1. Why this should be project-local (what would go wrong with the
global scope?)
2. Which projects need this permission
3. The diff to remove from global and add to each project's
settings.json
Show diffs; I'll apply.Apply this — install the three-tier config
20-minute exercise. One project, three tiers, test all three. This is the safety layer everything else sits on.
Build sane tool permissions
Each step takes 3-5 minutes. Progress saves automatically.
- 01Open .claude/settings.json in your most-used project. If it doesn't exist, create one.Use the bootstrap prompt to draft sane defaults.
- 02Build the three-tier list: allow (boring + reversible), ask (mutates state), deny (irreversible).Aim for around 10 entries in allow, 8-12 in ask, 3-5 in deny.
- 03Test all three tiers with real commands. Verify allow runs invisible, ask prompts, deny refuses.If anything behaves unexpectedly the glob pattern is wrong. Fix and retest.
- 04Run the permissions audit on any project where settings.json is older than 90 days.'Always' drift accumulates silently. The audit catches it.
- 05Migrate any sensitive permissions from global (~/.claude/settings.json) to project-local.Global is for the truly safe defaults. Anything that could hurt one project shouldn't live globally.