Appearance
Claude Code permissions
Tool-usage permission rules for Claude Code sessions, configured in .claude/settings.json (project-level, version-controlled) alongside the hooks. Distinct from hooks: permissions gate whether a tool call is allowed to run at all — allow / ask / deny — rather than running a command before/after it.
Precedence
deny wins over ask, and ask wins over allow — regardless of which settings file (user, project, or local) a rule came from, and regardless of how specific a competing rule is. A broad allow cannot be carved back open by a narrower rule elsewhere; only an equally-or-more-specific deny/ask rule changes the outcome for a given match.
Settings load in order user → project → local, later sources layering on top, but for permission rules specifically the deny/ask/allow category always decides first — see Claude Code settings docs for the general load order.
Example rules (.claude/settings.json)
json
{
"permissions": {
"allow": ["Read", "Grep", "Glob"],
"ask": ["Bash(vercel *)", "Bash(pnpm run deploy)", "Bash(pnpm deploy*)"],
"deny": [
"Bash(cat .env*)",
"Bash(cat **/.env*)",
"Bash(less .env*)",
"Bash(more .env*)",
"Bash(head .env*)",
"Bash(tail .env*)",
"Bash(printenv)",
"Bash(env)",
"Read(.env)",
"Read(.env.local)",
"Read(.env.production)",
"Read(.env.development)",
"Read(.env.test)"
]
}
}Add nested package env paths (e.g. Read(apps/cms/.env)) to deny when those files hold secrets.
Read-only inspection — allow
Read, Grep, Glob never prompt. Matches a general stance that inspection is safe; Edit/Write/Bash still go through Claude Code's normal interactive approval unless you override that default.
Deploy commands — ask
vercel *, pnpm run deploy, pnpm deploy* always prompt for confirmation, even when a personal .claude/settings.local.json (gitignored) has a broad Bash(pnpm run *) allow rule that would otherwise silently cover pnpm run deploy. Because ask rules win over allow rules from any source, this closes that gap without touching the personal file.
This is Claude Code's counterpart to the Allow/Deny dialog Grok hooks enforce before production deploy. The shell PreToolUse hook still matters so prefix variations and chained commands are caught, not only literal permission-rule prefixes.
Secret exposure — deny
Two independent protections, matched at the level of each tool's own permission-rule syntax (there's no rule that spans both Bash and Read in one pattern):
Bash:cat/less/more/head/tailagainst.env*, plus bareprintenv/env, are hard-blocked — no prompt, just refused. A.env*wildcard also blocks reading.env.examplevia those shell commands (harmless content; use theReadtool for templates instead).Read: explicit secret filenames only —.env,.env.local, production/dev/test variants — deliberately not a.env*wildcard. A wildcard would also catch.env.example(the git-tracked, secret-free template), and sincedenyalways wins overallowwith no specificity override, there'd be no way to carve the example files back open. New env-file variants not in the list aren't automatically covered — add them when introduced.
Troubleshooting
| Symptom | Check |
|---|---|
Agent still ran vercel --prod without asking | Confirm .claude/settings.json is present and hasn't been locally overridden with a deny→allow downgrade in a higher-precedence source |
Agent can't read .env.example | Check it isn't being read via a denied Bash command (cat/less/etc.) — use the Read tool instead |
A new .env.* variant is readable when it shouldn't be | Add its exact filename to permissions.deny for both the Bash(cat ...) family and Read(...) |
Related
- Claude Code hooks — lifecycle automation (format-on-edit, post-agent quality gate)
- Claude Code session management —
/orientand/handoffskills - Grok agent hooks — mirrored deploy Allow/Deny for Grok CLI sessions