Appearance
Claude Code Permissions
Configure Claude Code tool permissions in ~/.claude/settings.json.
Permission Model
Three tiers based on whether work can be lost:
Allow — Reads and modifications where no work can be permanently lost. Auto-approved, no prompts. Includes safe mutations where git itself prevents data loss (e.g.,
git mergerefuses with dirty working tree,git rebasepreserves old commits in reflog).Ask — Operations where uncommitted or committed work could be permanently lost. Always prompts for confirmation. The test: "if this runs at the wrong time, can work disappear?" If yes, it belongs here.
Deny — Catastrophic operations that should never run under any circumstances. No prompt, just blocked.
Structure
json
{
"permissions": {
"allow": [
"ToolName",
"Bash(command pattern *)"
],
"ask": ["Bash(git push --force*)"],
"deny": ["Bash(rm -rf *)"]
}
}Rules are evaluated: deny first, then ask, then allow. First match wins.
Tool Names
Built-in tools that can appear in allow/deny/ask arrays:
| Tool | Purpose |
|---|---|
Read | Read file contents |
Edit | Modify existing files |
Write | Create new files |
Grep | Search file contents |
Glob | Search for files by pattern |
Bash | Execute shell commands |
WebFetch | Fetch URL content |
WebSearch | Web search |
MCP | Model Context Protocol servers |
Bash Patterns
Bash entries use glob-style matching inside parentheses. Compound commands (&&, ||, ;, |) are parsed — each part is checked independently.
Bash(git status)-- exact commandBash(git log *)-- command with any argumentsBash(npm run *)-- any npm script
Allow (auto-approve)
Reads and non-destructive modifications. No work can be permanently lost.
Allow: Core tools for reading and editing code
Most of these are read-only (Read, Grep, Glob, WebFetch, WebSearch) and can never cause harm. Edit and Write modify files, but editing code is the core job of Claude Code — and any changes show up in git diff and can be reverted.
Read -- read file contents
Edit -- modify existing files
Write -- create new files
Grep -- search file contents
Glob -- find files by pattern
WebFetch -- fetch URL content
WebSearch -- web searchAllow: Git reads and safe mutations
Most of these are read-only queries (status, log, diff, show, branch listing, rev-parse). The mutations (add, commit, push, pull, merge, rebase, switch, fetch, tag, worktree, stash push/save) are auto-allowed because git itself prevents work loss — it refuses to run when uncommitted changes would be overwritten, and old commits survive in the reflog for 90 days. Destructive variants (push --force, reset --hard, checkout files, clean, stash drop, branch -D) are in the ask list.
Bash(git status) -- working tree status (exact)
Bash(git status *) -- working tree status with flags
Bash(git rev-parse *) -- resolve refs to SHAs, repo metadata
Bash(git log *) -- commit history
Bash(git diff *) -- show changes between commits/working tree
Bash(git branch) -- list branches
Bash(git branch -a*) -- list all branches (local + remote)
Bash(git branch -r*) -- list remote branches
Bash(git branch -v*) -- list branches with last commit
Bash(git branch --list*) -- list branches matching pattern
Bash(git branch --show-current*) -- show current branch name
Bash(git branch --contains*) -- list branches containing a commit
Bash(git show *) -- show commit/object contents
Bash(git remote *) -- manage remote repositories
Bash(git add *) -- stage files for commit
Bash(git commit *) -- create a commit
Bash(git stash list*) -- list stash entries
Bash(git stash show*) -- show stash contents
Bash(git stash push*) -- save changes to stash
Bash(git stash save*) -- save changes to stash (legacy syntax)
Bash(git stash -*) -- stash with flags (e.g. -u for untracked)
Bash(git pull) -- fetch and merge from remote
Bash(git pull *) -- fetch and merge with flags
Bash(git push) -- push commits to remote
Bash(git push *) -- push with flags (force push is in ask)
Bash(git merge *) -- merge branches (refuses with dirty tree)
Bash(git cherry-pick *) -- copy commits (originals stay in reflog)
Bash(git rebase *) -- rebase commits (old commits in reflog)
Bash(git checkout -b *) -- create branch (-b first arg)
Bash(git checkout * -b *) -- create branch (flags before -b)
Bash(git restore --staged *) -- unstage files (--staged first arg)
Bash(git restore * --staged *) -- unstage files (flags before --staged)
Bash(git restore -S *) -- unstage files (-S first arg)
Bash(git restore * -S *) -- unstage files (flags before -S)
Bash(git switch *) -- switch branches (refuses with dirty tree)
Bash(git fetch *) -- download from remote, no local changes
Bash(git tag *) -- create/list tags (delete is in ask)
Bash(git worktree *) -- manage worktrees (remove is in ask)
Bash(git -C *) -- any git command targeting a different dir
Bash(git reset) -- unstage all files
Bash(git reset *) -- any reset that isn't --hard (--hard in ask)Allow: GitHub CLI reads and non-destructive operations
Most gh subcommands are reads (list, view, search, status) or non-destructive modifications (create, edit). Auto-allowed because creating or editing issues and PRs doesn't destroy anything — they can always be re-edited or reopened. Destructive operations (repo delete, release delete) and write API methods (-X POST, -X DELETE, etc.) are in the ask list.
Bash(gh issue *) -- list, view, create, edit issues
Bash(gh pr *) -- list, view, create, edit pull requests
Bash(gh run *) -- list, view workflow runs
Bash(gh repo *) -- view, clone repos (delete is in ask)
Bash(gh search *) -- search repos, issues, PRs, code
Bash(gh auth *) -- check auth status, login
Bash(gh api *) -- GitHub API (GET only; write methods in ask)
Bash(gh release *) -- list, view releases (delete is in ask)Allow: Package managers and regenerable installs
Most of these are reads (ls, outdated, view, list, search, info). npm run, npm test, node --test, and just can execute arbitrary scripts, but they only run commands already defined in package.json or justfile (or test files on disk) — the developer has already approved what those scripts do. Install and upgrade operations modify node_modules or system packages, but are auto-allowed because package state is always regenerable from lockfiles — deleting node_modules and reinstalling gives you the same result. No original work lives here. npx, npm exec, and volta are in the ask list because npx/npm exec download and run arbitrary packages and volta run can execute any command.
Bash(npm run *) -- run scripts defined in package.json
Bash(just *) -- run tasks defined in justfile
Bash(npm test *) -- run test script from package.json
Bash(node --test *) -- Node's built-in test runner (safe subset of `node`)
Bash(npm install *) -- install dependencies
Bash(npm update *) -- update packages to latest allowed version
Bash(npm audit *) -- check for known vulnerabilities
Bash(npm ls *) -- list installed packages
Bash(npm outdated *) -- show outdated packages
Bash(npm view *) -- view package registry info
Bash(npm init *) -- create/update package.json
Bash(npm pkg *) -- read/write package.json fields
Bash(brew install *) -- install Homebrew packages
Bash(brew list *) -- list installed packages
Bash(brew search *) -- search available packages
Bash(brew info *) -- show package details
Bash(brew update) -- update Homebrew itself
Bash(brew upgrade *) -- upgrade installed packages
Bash(brew tap *) -- add third-party repositories
Bash(brew --prefix *) -- show install path for a package
Bash(brew services *) -- manage background servicesAllow: Read-only shell utilities
All of these are read-only — navigation (cd, ls, pwd), file inspection (file, stat, cat, head, tail), text processing (grep, awk, sed, sort, cut, tr), system info (ps, env, uname, date, whoami), and modern CLI tools (bat, jq, duckdb, rg, fd, fzf, eza, delta, tokei, semgrep). Auto-allowed because they only read and transform data, never modify files. The one exception is sed -i (in-place edit) which is in the ask list.
Bash(cd *) -- change directory
Bash(ls *) -- list directory contents with flags
Bash(ls) -- list directory contents
Bash(pwd) -- print working directory
Bash(which *) -- locate a command
Bash(wc *) -- count lines/words/bytes with flags
Bash(wc) -- count lines/words/bytes
Bash(file *) -- detect file type
Bash(stat *) -- file metadata (size, timestamps)
Bash(du *) -- disk usage
Bash(df *) -- filesystem free space
Bash(cat *) -- print file contents
Bash(echo *) -- print text
Bash(find *) -- find files by name/attributes
Bash(comm *) -- compare sorted lists (common/unique lines)
Bash(diff *) -- compare files
Bash(grep *) -- search text with regex
Bash(head *) -- show first lines of file
Bash(tail *) -- show last lines of file
Bash(awk *) -- text processing language
Bash(sed *) -- stream text editor (in-place edit in ask)
Bash(sort *) -- sort lines
Bash(uniq *) -- deduplicate adjacent lines
Bash(cut *) -- extract columns from text
Bash(tr *) -- translate/delete characters
Bash(ps *) -- list running processes
Bash(env) -- print environment variables
Bash(env *) -- print/set env for a command
Bash(printenv *) -- print specific env variable
Bash(uname *) -- system info (OS, architecture)
Bash(date) -- current date/time
Bash(date *) -- date with format flags
Bash(id) -- current user/group IDs
Bash(whoami) -- current username
Bash(basename *) -- strip directory from path
Bash(dirname *) -- strip filename from path
Bash(realpath *) -- resolve to absolute path
Bash(readlink *) -- resolve symlinks
Bash(md5 *) -- compute MD5 checksum
Bash(shasum *) -- compute SHA checksum
Bash(bat *) -- syntax-highlighted file viewer
Bash(jq *) -- JSON query and transform
Bash(duckdb *) -- SQL queries on files (CSV, JSON, Parquet)
Bash(rg *) -- fast recursive text search (ripgrep)
Bash(fd *) -- fast file finder
Bash(fzf *) -- fuzzy finder
Bash(eza *) -- modern ls with tree view and git status
Bash(delta *) -- structured diff viewer
Bash(tokei *) -- code statistics by language
Bash(semgrep *) -- pattern-based static analysis
Bash(pbcopy) -- copy stdin to macOS clipboard
Bash(pbcopy *) -- copy stdin to macOS clipboard with flags
Bash(LANG=en_US.UTF-8 pbcopy *) -- pbcopy with locale setAllow: File creation, copying, and safe tools
File creation (mkdir, touch), copying (cp), moving (mv), linking (ln), archiving (tar, unzip), and tools (claude, curl, xh, export, unset). Auto-allowed because these create or rearrange files but don't destroy existing work — cp preserves the source, mv is a rename, curl/xh only do network I/O. General-purpose scripting (python3, bash, sh) is in the ask list instead, because a script can do literally anything.
Bash(claude *) -- Claude Code CLI
Bash(curl *) -- HTTP requests (read-only network I/O)
Bash(xh *) -- modern HTTP client (structured output, like curl)
Bash(export *) -- set environment variables
Bash(unset *) -- clear environment variables
Bash(mkdir *) -- create directories
Bash(touch *) -- create empty files / update timestamps
Bash(cp *) -- copy files (preserves source)
Bash(mv *) -- move/rename files
Bash(ln *) -- create links (hard or symbolic)
Bash(tar *) -- create/extract archives
Bash(unzip *) -- extract zip archivesAsk (always prompt)
Operations where uncommitted or committed work could be permanently lost, or that can do anything (general-purpose scripting). Ask rules are evaluated before allow rules, so these prompt even when a broader allow pattern would match.
Ask: Git operations that overwrite remote history
Force push rewrites the remote branch. If others have based work on commits that get replaced, their work can be silently lost.
Bash(git push --force*) -- force push (flag first)
Bash(git push -*f*) -- force push (any short flag combo with -f)
Bash(git push * --force*) -- force push (flag after remote/branch)
Bash(git push * -*f*) -- force push (short flag combo after remote/branch)Ask: Git operations that delete branches, tags, and worktrees
Refs and directories are permanently gone. Unmerged branch commits survive in the reflog temporarily, but tags and worktree directories do not.
Bash(git tag -*d *) -- delete tag (any short flag combo with -d)
Bash(git tag --delete *) -- delete tag (long flag)
Bash(git worktree remove *) -- delete a worktree directory
Bash(git branch -*d *) -- delete branch (any combo with -d)
Bash(git branch -*D *) -- force-delete branch (any combo with -D)
Bash(git branch --delete *) -- delete branch (long flag)Ask: Git operations that discard uncommitted changes
Edits that exist nowhere in git history are permanently lost. reset --hard discards both staged and unstaged changes (safe reset variants like unstaging are in the allow list), checkout/restore overwrite working tree files, and clean deletes untracked files that were never committed.
Bash(git reset --hard*) -- --hard as first flag
Bash(git reset * --hard*) -- --hard after other args
Bash(git checkout *) -- restore files or switch branches (can overwrite edits)
Bash(git restore *) -- restore working tree files (same risk as checkout)
Bash(git clean *) -- delete untracked files (never in git, gone forever)Ask: Git operations that destroy stash entries
Stash entries are saved work. drop and clear delete them permanently, and pop deletes the entry after applying — if the apply has conflicts, the original stash is already gone.
Bash(git stash drop*) -- delete a single stash entry
Bash(git stash pop*) -- apply and delete a stash entry
Bash(git stash clear*) -- delete all stash entriesAsk: GitHub CLI destructive and write API operations
Can delete repos and releases, or mutate remote state via raw API calls. Write methods (POST, PUT, PATCH, DELETE) are gated because gh api * in the allow list only covers the default GET method.
Bash(gh repo delete *) -- permanently delete a repository
Bash(gh release delete *) -- delete a release
Bash(gh api -X DELETE *) -- raw API DELETE request
Bash(gh api -X POST *) -- raw API POST request
Bash(gh api -X PUT *) -- raw API PUT request
Bash(gh api -X PATCH *) -- raw API PATCH request
Bash(gh api --method DELETE *) -- raw API DELETE (long flag)
Bash(gh api --method POST *) -- raw API POST (long flag)
Bash(gh api --method PUT *) -- raw API PUT (long flag)
Bash(gh api --method PATCH *) -- raw API PATCH (long flag)Ask: General-purpose scripting and arbitrary code execution
Scripts can do anything (delete files, make network calls, modify system state), and the permission pattern can't inspect what's inside the script. npx/npm exec download and run arbitrary packages, and volta run can execute any command under a specific Node version — same risk.
Note: node * is intentionally NOT in this list. Rules are evaluated deny → ask → allow, first match wins. If node * were here, it would match node --test before the allow list is checked, preventing the allow entry from working. Omitting node * from ask is safe because unlisted commands prompt by default — so node script.js still prompts, while node --test * in the allow list auto-approves.
Bash(python3 *) -- run arbitrary Python
Bash(bash *) -- run arbitrary shell script
Bash(bash) -- bare shell (e.g. piped into: curl | bash)
Bash(sh *) -- run arbitrary shell script
Bash(sh) -- bare shell (e.g. piped into: curl | sh)
Bash(xargs *) -- build and execute arbitrary commands from stdin
Bash(watchexec *) -- file watcher that runs arbitrary commands on change
Bash(npx *) -- download and run arbitrary npm packages
Bash(npm exec *) -- same as npx
Bash(volta *) -- Node version manager (volta run executes commands)Ask: File mutations that bypass git
In-place edits bypass git's safety net, and recursive deletion can wipe directories.
Bash(sed -*i *) -- edit file in place (any combo with -i)
Bash(sed --in-place *) -- edit file in place (long flag)
Bash(rm -rf *) -- recursive forced deletion of directoriesDeny (block entirely)
Catastrophic operations where there is no legitimate use case. No prompt, just blocked. rm -rf / would destroy the entire filesystem — there's no reason Claude should ever run this, even if asked.
Bash(rm -rf /) -- destroy entire filesystemImplicit prompts
Any command not in allow, ask, or deny will prompt by default — no configuration needed. This is the catch-all for commands we haven't categorized yet. Examples: rm (single file deletion), kill (process termination), chmod (permission changes). If you find yourself approving the same command repeatedly, consider adding it to the appropriate list above.
Sync Permissions
Sync the recommended permissions above to the user's machine. Follow this procedure exactly.
1. Compare
Collect every entry from all code blocks under the "Allow" sections into the canonical allow set. Collect every entry from all code blocks under the "Ask" sections into the canonical ask set. Collect every entry from the "Deny" code block into the canonical deny set. Read ~/.claude/settings.json and extract permissions.allow, permissions.ask, and permissions.deny as the local sets. Compute the differences for each:
- missing locally: entries in a canonical set but not in the local set
- extra locally: entries in the local set but not in the canonical set
2. Act based on the comparison
Apply this logic to allow, ask, and deny independently.
Already in sync — Both sets match exactly. Tell the user their permissions are already in sync. Done.
Settings has less than the skill — Entries are missing locally. Patch them in (preserving existing entries and all other settings). Tell the user what was added and that they're now in sync.
Settings has more than the skill — Extra entries exist locally. Present the user with these options using AskUserQuestion:
- (a) Sync extras to the skill — Add the extra entries to the appropriate section above.
- (b) Leave extras in settings only — Keep them in the settings file, don't add them to the skill. No changes made.
- (c) Remove extras from settings — Delete entries from the settings file that aren't in the canonical set.
- (d) Granular decisions — Go through each extra entry one by one and let the user decide.
Recommend whichever option you think is best, with brief reasoning based on what the extra entries actually are.
Both directions — Missing AND extra entries exist. First patch in the missing entries. Then handle the extras using the options above.
3. Preserve other settings
When writing ~/.claude/settings.json, only modify permissions.allow, permissions.ask, and permissions.deny. Preserve all other keys (model, statusLine, skipDangerousModePermissionPrompt, etc.) and use 2-space JSON indentation.
Reduce Approval Prompts
Find which Bash commands trigger the most approval prompts and suggest permission changes to reduce friction without compromising security. Uses a single PermissionRequest hook that logs every prompt with the exact command that caused it — no correlation or cross-referencing needed.
How it works
Claude Code's PermissionRequest hook fires only when a permission prompt is about to be shown. It receives the full tool input, including the exact Bash command. A simple hook script appends each event to a JSONL log. After collecting data, analyze the log to find patterns.
Key properties verified through testing:
- Tool calls are processed sequentially — each completes its full lifecycle (PreToolUse → permission check → execute) before the next starts, even when Claude requests multiple parallel tool calls
PermissionRequestfires only for prompted commands — auto-allowed commands skip it entirelyPermissionRequestincludestool_nameandtool_input.command— the exact command that triggered the prompt- Compound commands (
&&,||,;,|) log as the full string — split them during analysis to identify which part caused the prompt - Denials are logged — the hook fires before the user's decision, so both approved and denied prompts appear in the log
- Each log entry includes
session_id— safe with many concurrent sessions - JSONL appends are atomic for small writes — no corruption under concurrency
Phase 1: Set up the logging hook
1. Create the hook script
Write ~/.claude/hooks/log-permission-prompts.sh:
bash
#!/usr/bin/env bash
# Log every permission prompt with the exact command
# that triggered it. Uses the PermissionRequest hook
# event, which fires only when a prompt is needed and
# includes tool_name and tool_input.
#
# Exits 0 with no stdout (pass-through, no decision).
input=$(cat)
timestamp=$(gdate -u +"%Y-%m-%dT%H:%M:%S.%3NZ" 2>/dev/null \
|| date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "$input" \
| jq -c --arg ts "$timestamp" '{
timestamp: $ts,
session_id: .session_id,
tool_name: .tool_name,
command: .tool_input.command,
cwd: .cwd
}' \
>> ~/.claude/permission-prompts.jsonlMake it executable:
bash
chmod +x ~/.claude/hooks/log-permission-prompts.shThe script uses gdate (GNU date) for millisecond timestamps, falling back to date (second precision) if unavailable. On macOS, install with brew install coreutils. On Linux, date already supports %3N.
2. Register the hook
Add this to ~/.claude/settings.json (merge into existing hooks if present):
json
{
"hooks": {
"PermissionRequest": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/log-permission-prompts.sh"
}
]
}
]
}
}No matcher is needed — the hook only fires when a prompt is about to be shown, so every invocation is relevant.
3. Verify
Use Claude Code normally for a few days. Each time a permission prompt appears, a line is appended to ~/.claude/permission-prompts.jsonl.
Phase 2: Analyze the log
After collecting data (at least a few days), analyze the log to find the most common prompts and suggest permission changes.
1. Read the log
Read ~/.claude/permission-prompts.jsonl. Each line is a JSON object with the exact command that triggered the prompt:
jsonc
{
"timestamp": "2026-03-22T18:30:05.189Z",
"session_id": "abc123",
"tool_name": "Bash",
"command": "node scripts/build.js",
"cwd": "/Volumes/src/my-project"
}2. Identify unexpected prompts
For each logged command, check whether it should have been auto-allowed by an existing Bash(...) pattern in ~/.claude/settings.json. Commands that match an allow pattern but still triggered a prompt indicate a bug in the pattern matching — these are the ones to investigate first.
Commands that don't match any pattern are expected prompts. Group these by frequency to find candidates for new allow rules.
3. Normalize and count
Group expected-prompt commands by their base pattern. Extract the first word (or first two words for git, gh, npm, brew) as the key:
git log --oneline -10→git lognode scripts/build.js→nodenode --test tests/foo.test.js→node --testnpm run build→npm run
For compound commands (&&, ||, ;, |), split and normalize each part independently.
4. Report
Present results in two groups:
Unexpected prompts (match an allow pattern but prompted anyway):
Count Command Matching allow pattern
───── ─────────────────── ──────────────────────
12 git status --short Bash(git status *)
8 npm run build Bash(npm run *)These indicate pattern matching bugs to investigate.
Expected prompts (no matching allow pattern):
Count Command Example
───── ───────── ──────────────────────────────
42 node node scripts/build.js
9 open open http://localhost:3000These are candidates for new permission rules.
5. Suggest permission changes
For expected prompts, suggest whether to add the command to allow, ask, or leave as an implicit prompt. Follow the risk model from this skill:
- Allow if no work can be permanently lost (reads, safe mutations, regenerable state).
- Ask if uncommitted/committed work could be lost, or the command can do anything (general-purpose scripting, destructive operations).
- Leave as implicit prompt if the command is rare enough that adding a rule isn't worth it.
After the user approves, apply the changes to ~/.claude/settings.json following the same rules as "Sync Permissions" step 3 (preserve other settings, 2-space indentation). Also add approved entries to the appropriate section in this skill file so they stay in sync.