Skip to content

Claude Code Permissions

Configure Claude Code tool permissions in ~/.claude/settings.json.

Permission Model

Three tiers based on whether work can be lost:

  1. 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 merge refuses with dirty working tree, git rebase preserves old commits in reflog).

  2. 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.

  3. 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:

ToolPurpose
ReadRead file contents
EditModify existing files
WriteCreate new files
GrepSearch file contents
GlobSearch for files by pattern
BashExecute shell commands
WebFetchFetch URL content
WebSearchWeb search
MCPModel 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 command
  • Bash(git log *) -- command with any arguments
  • Bash(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 search

Allow: 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 services

Allow: 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 set

Allow: 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 archives

Ask (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 entries

Ask: 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 directories

Deny (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 filesystem

Implicit 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
  • PermissionRequest fires only for prompted commands — auto-allowed commands skip it entirely
  • PermissionRequest includes tool_name and tool_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.jsonl

Make it executable:

bash
chmod +x ~/.claude/hooks/log-permission-prompts.sh

The 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 -10git log
  • node scripts/build.jsnode
  • node --test tests/foo.test.jsnode --test
  • npm run buildnpm 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:3000

These 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.