Permission system
Claude Code uses a tiered permission system to balance power and safety. The table shows, for each tool type, whether Manual mode asks before the action runs. The other permission modes change which of these ask you; in auto mode a classifier reviews actions instead of you, and how the classifier evaluates actions lists which ones it sees.
When you choose “Yes, and don’t ask again” and the approval saves permanently, such as for a Bash command or a WebFetch domain, Claude Code saves the rule to
.claude/settings.local.json at the root of the git repository, resolved through worktrees to the main checkout. The rule applies to future sessions anywhere in that repository, including sessions started in subdirectories and in worktrees. A file-modification approval isn’t saved to the file: as the table shows, it lasts until the session ends. Outside a git repository, and when the repository root is your home directory, Claude Code saves the rule in the directory you started it from.
Before v2.1.211, Claude Code always saved the rule in the starting directory, so an approval granted in a worktree or subdirectory didn’t apply to the rest of the repository. Rules that earlier versions saved in a subdirectory or worktree still apply to sessions started there.
On a Bash or PowerShell permission prompt, press Ctrl+E to show an explanation of the command: what it does, why Claude is running it, and what could go wrong, labeled Low risk, Med risk, or High risk. Claude Code sends the command and Claude’s own description of the call to the model to generate the explanation only when you press Ctrl+E, not on every prompt. Showing the explanation doesn’t run the command; press Ctrl+E again to hide it.
To turn the shortcut off, set permissionExplainerEnabled to false in ~/.claude.json.
Manage permissions
You can view and manage Claude Code’s tool permissions with/permissions. The dialog lists all permission rules and the settings.json file each rule comes from. You can open the dialog while Claude is working: when you add or remove a rule, Claude Code applies the change starting with Claude’s next tool call in the same turn. Before v2.1.234, Claude Code queued the command until the turn finished.
- Allow rules let Claude Code use the specified tool without manual approval.
- Ask rules prompt for confirmation whenever Claude Code tries to use the specified tool.
- Deny rules prevent Claude Code from using the specified tool.
Bash(aws *) blocks every matching call, including calls that also match a narrower allow rule like Bash(aws s3 ls), so a deny rule can’t carry allowlist exceptions. The same precedence applies between ask and allow: a matching ask rule prompts even when a more specific allow rule also matches the same call.
Deny rules behave differently depending on whether they name a tool or scope a pattern within one. A bare tool name like Bash removes the tool from Claude’s context entirely, so Claude never sees it. Bare-name removal applies to every tool except EndConversation: a deny rule can’t remove it while any other tool remains, and an ask rule never prompts for it. A scoped rule like Bash(rm *) leaves the tool available and blocks matching calls when Claude attempts them.
Permission rules are enforced by Claude Code, not by the model. Instructions in your prompt or
CLAUDE.md shape what Claude tries to do, but they don’t change what Claude Code allows. To grant or revoke access, use /permissions, the rules described here, a permission mode, or a PreToolUse hook.Permission modes
Claude Code supports several permission modes that control how it approves tool calls. See Permission modes for when to use each one. To change the mode sessions start in, setdefaultMode in your settings files. Which mode a session starts in covers the built-in default for each plan and what the VS Code extension reads.
To prevent
bypassPermissions or auto mode from being used, set permissions.disableBypassPermissionsMode or permissions.disableAutoMode to "disable" in any settings file. These are most useful in managed settings where they can’t be overridden.
Permission rule syntax
Permission rules follow the formatTool or Tool(specifier).
Match all uses of a tool
To match all uses of a tool, use only the tool name without parentheses:Bash(*) is equivalent to Bash and matches all Bash commands. As a deny rule, both forms remove the tool from Claude’s context.
Use specifiers for fine-grained control
Add a specifier in parentheses to match specific tool uses:Match by input parameter
Deny and ask rules can match a top-level input parameter on any tool withTool(param:value). The rule matches when Claude calls the tool with that parameter set to that exact value. An allow rule for one parameter value wouldn’t establish that the call is safe overall, so allow rules continue to use each tool’s own specifier syntax. This works for any scalar parameter the tool accepts:
Parameter matching follows these rules:
- The parameter name must be a direct field of the tool’s input, such as
modelon the Agent tool. Fields nested inside an object or array are not matchable - Each rule names one parameter. To gate on both
modelandisolation, write two rules,Agent(model:opus)andAgent(isolation:worktree), rather than combining them in one rule - The value supports
*as a wildcard that matches any sequence of characters, soAgent(isolation:*)matches any explicit isolation value. Without*the match is exact - A parameter the model omits is never matched, so
Agent(model:*)doesn’t match a call that leavesmodelunset - The value is compared against the literal input Claude sends, before any normalization.
Agent(model:opus)matches the aliasopusbut not a full model ID. Run with--verboseto see the exact parameter names and values in each tool call - Whitespace around the colon is ignored
command for Bash and PowerShell, file_path for Read, Edit, and Write, path for Grep and Glob, notebook_path for NotebookEdit, and url for WebFetch. A rule like Bash(command:rm *) would be bypassable by a compound command, so Claude Code ignores it and emits a startup warning. Use Bash(rm *), Read(./path), or WebFetch(domain:host) instead.
Wildcard patterns
Bash rules support glob patterns with*. This configuration allows npm and git commit commands while blocking git push:
:* suffix is an equivalent way to write a trailing wildcard, so Bash(ls:*) matches the same commands as Bash(ls *).
The permission dialog writes the space-separated form when you select “Yes, and don’t ask again” for a command prefix. The :* form is only recognized at the end of a pattern. In a pattern like Bash(git:* push), the colon is treated as a literal character and won’t match git commands.
Tool name wildcards
Deny and ask rules also accept glob patterns in the tool-name position. The pattern must match the full tool name:"*" matches every tool, and "mcp__*" matches every MCP tool across all servers. A tool matched by a bare-name glob deny rule is removed from Claude’s context, the same as a bare tool name, including the EndConversation exception: a glob deny can’t remove it while any other tool remains, and a glob ask never prompts for it. This configuration denies every MCP tool:
mcp__<server>__ prefix. The server segment must be glob-free so the rule names a specific server you configured. mcp__puppeteer__* matches every tool from the puppeteer server, and mcp__github__get_* matches its get_ tools. An unanchored allow glob such as "*", "B*", or "mcp__*" is skipped with a warning and doesn’t auto-approve anything.
A deny or ask rule whose tool name matches no known tool produces a startup warning to catch typos. Tool names containing _ or * are exempt from the check.
The label shown for a tool in the transcript and permission dialog can differ from its canonical name. For example, the tool labeled Stop Task in the transcript has the canonical name TaskStop. Permission rules and hook matchers match the canonical name only, so a rule written as Stop Task doesn’t match. For deny and ask rules, the startup warning above catches the mismatch. Use the canonical names listed in the tools reference.
Tool-specific permission rules
Bash
Bash permission rules support wildcard matching with*. Wildcards can appear at any position in the command, including at the beginning, middle, or end:
Bash(npm run build)matches the exact Bash commandnpm run buildBash(npm run test *)matches Bash commands starting withnpm run testBash(npm *)matches any command starting withnpmBash(* install)matches any command ending withinstallBash(git * main)matches commands likegit checkout mainandgit log --oneline main
* matches any sequence of characters including spaces, so one wildcard can span multiple arguments. Bash(git *) matches git log --oneline --all, and Bash(git * main) matches git push origin main as well as git merge main.
When * appears at the end with a space before it (like Bash(ls *)), it enforces a word boundary, requiring the prefix to be followed by a space or end-of-string. For example, Bash(ls *) matches ls -la but not lsof. In contrast, Bash(ls*) without a space matches both ls -la and lsof because there’s no word boundary constraint.
Compound commands
When you approve a compound command with “Yes, and don’t ask again”, Claude Code saves a separate rule for each subcommand that requires approval, rather than a single rule for the full compound string. For example, approvinggit status && npm test saves a rule for npm test, so future npm test invocations are recognized regardless of what precedes the &&. Subcommands like cd into a subdirectory generate their own Read rule for that path. Up to 5 rules may be saved for a single compound command.
Wrappers
Before matching Bash rules, Claude Code strips a fixed set of wrappers, so a rule likeBash(npm test *) also matches timeout 30 npm test. The stripped wrappers are timeout, time, nice, nohup, and stdbuf, plus the shell builtins command and builtin, and zsh’s noglob. Each runs its argument as the actual command. Two related forms aren’t stripped: the query form command -v, which looks up a command rather than running one, and zsh’s nocorrect.
Claude Code also strips a leading assignment of certain known-safe environment variables, so Bash(npm test *) matches NODE_ENV=test npm test. An allow rule won’t match past an assignment of any other variable. A deny or ask rule matches past any leading assignment, so Bash(rm *) in deny still matches FOO=bar rm -rf tmp/.
Bare xargs is also stripped, so Bash(grep *) matches xargs grep pattern. Stripping applies only when xargs has no flags: an invocation like xargs -n1 grep pattern is matched as an xargs command, so rules written for the inner command do not cover it.
This wrapper list is built in and is not configurable. Development environment runners such as direnv exec, devbox run, mise exec, npx, and docker exec are not in the list. Because these tools execute their arguments as a command, a rule like Bash(devbox run *) matches whatever comes after run, including devbox run rm -rf .. To approve work inside an environment runner, write a specific rule that includes both the runner and the inner command, such as Bash(devbox run npm test). Add one rule per inner command you want to allow.
Exec wrappers such as watch, setsid, ionice, and flock can’t be auto-approved by a prefix rule like Bash(watch *), so in Manual mode they always prompt. The same applies to find with -exec or -delete: a Bash(find *) rule doesn’t cover these forms. To approve a specific invocation, write an exact-match rule for the full command string.
Read-only commands
Claude Code recognizes a built-in set of Bash commands as read-only and runs them without a permission prompt in every mode. These includels, cat, echo, pwd, head, tail, grep, find, wc, which, diff, stat, du, cd, and read-only forms of git. The set is not configurable; to require a prompt for one of these commands, add an ask or deny rule for it.
Unquoted glob patterns are permitted for commands whose every flag is read-only, so ls *.ts and wc -l src/*.py run without a prompt.
In Manual mode, commands from this set still prompt in these cases:
- Unquoted globs for commands with write-capable flags: commands with write-capable or exec-capable flags, such as
find,sort,sed, andgit, prompt when an unquoted glob is present, because the glob could expand to a flag like-delete. dockerpointed at another daemon: read-only forms ofdockerprompt when the command carries a flag that selects a different daemon, such as-H,--context, or Podman’s--urland--connection.filewith path-opening flags:fileprompts when it passes-m/--magic-fileor-f/--files-from, because those flags makefileopen the paths named in the flag’s value.- Network paths on Windows: a command whose arguments include a network (UNC) path, such as
\\server\share\file, prompts because accessing a network path can send your Windows credentials to the host it names. The same check applies to PowerShell tool commands. - Commands the analysis can’t parse: when Claude Code can’t fully parse a command, it asks for approval instead of treating the command as read-only. Commands longer than 10,000 characters always prompt because they exceed what the analysis parses.
cd into a path inside your working directory or an additional directory is also read-only, and a compound command like cd packages/api && ls runs without a prompt when each part qualifies on its own. Two combinations prompt even when each part is read-only:
cdwithgit: prompts when thecdchanges into a different directory, since runninggitin a new directory can execute that directory’s hooks. Acdwhose target resolves to the current working directory is a no-op and doesn’t trigger the prompt.cdwith an output redirect: prompts when Claude Code can’t determine which directory the redirect target resolves against after thecdruns. A command whose only redirect target is/dev/null, such ascd app; grep -r pattern . 2>/dev/null, doesn’t prompt, because/dev/nulldoesn’t depend on the working directory.
PowerShell
PowerShell permission rules use the same shape as Bash rules. Wildcards with* match at any position, the :* suffix is equivalent to a trailing *, and a bare PowerShell or PowerShell(*) matches every command. This configuration allows Get-ChildItem and git commit commands while blocking Remove-Item:
PowerShell(Get-ChildItem *) matches gci, ls, and dir as well. Matching is case-insensitive.
Claude Code parses the PowerShell AST and checks each command in a compound command independently. Pipeline operators |, statement separators ;, and on PowerShell 7+ the chain operators && and || split a compound command into subcommands. A rule must match every subcommand for the compound command to be allowed.
Read and Edit
To block Claude’s file tools from reading a file or directory, add aRead deny rule for its path, such as Read(./.env) or Read(./secrets/**); Exclude sensitive files has a paste-ready example.
Edit rules apply to all built-in tools that edit files. Claude makes a best-effort attempt to apply Read rules to all built-in tools that read files like Grep and Glob, to @file mentions in your prompts, and to the selection and open-file context that a connected IDE shares with Claude.
A Read deny rule also blocks the Edit and Write tools on the same path, including creating a new file there. NotebookEdit isn’t covered, so add an Edit deny rule for paths no tool may change. The check requires Claude Code v2.1.208 or later on edits, and v2.1.228 or later on writes.
Claude Code checks file permissions against Edit(path) and Read(path) rules only. If you write a path rule for Write, NotebookEdit, Glob, or the legacy MultiEdit tool instead, Claude Code accepts the rule but never consults it, and warns at startup, except for a Glob rule passed in --allowedTools. Use Edit(docs/**) in place of Write(docs/**), NotebookEdit(docs/**), or MultiEdit(docs/**), and Read(docs/**) in place of Glob(docs/**). Claude Code doesn’t warn about a tool-name rule with no path, such as a deny rule for Write; it matches that rule at the tool level everywhere. Requires Claude Code v2.1.210 or later.
Read and Edit rules both use gitignore pattern syntax with four distinct pattern types; for single-segment directory patterns, the matching depth also depends on the rule type, described later in this section:
A
/path pattern anchors at a directory associated with the settings source that defines it, so the same rule matches different locations depending on where you put it:
Local settings rules anchor at the directory you started Claude Code from, not at the repository root where Claude Code stores the file in v2.1.211 and later. In a session started at the repository root, the two directories are the same; in a worktree session, a shared rule such as
Edit(/src/**) matches that worktree’s own src/ directory.
A deny rule such as Read(/secrets/**) in user settings blocks ~/.claude/secrets/**, not a secrets directory in your project. To write a rule in user settings that applies inside every project, use a // absolute path or a ~/ home-relative path instead.
On Windows, paths are normalized to POSIX form before matching. C:\Users\alice becomes /c/Users/alice, so use //c/**/.env to match .env files anywhere on that drive. To match across all drives, use //**/.env.
Examples:
Edit(/docs/**): edits in<project>/docs/, not/docs/or<project>/.claude/docs/Read(~/.zshrc): reads your home directory’s.zshrcEdit(//tmp/scratch.txt): edits the absolute path/tmp/scratch.txtRead(src/**): as an allow rule, reads from<current-directory>/src/only; as a deny or ask rule, matches asrcdirectory at any depth under the current directory
Read(.env) and Read(**/.env) are equivalent:
A relative pattern with a single directory segment, such as
src/**, matches at different depths depending on the rule type:
- Allow rules:
Edit(src/**)matches only<cwd>/srcand the files under it. To allow a directory name at any depth, writeEdit(**/src/**). - Deny and ask rules:
Read(secrets/**)matches a directory namedsecretsat any depth under the current directory, so the rule also applies to nested copies.
Edit(/src/**) and Edit(src/components/**) match only at their anchored location, while Edit(**/src/**) matches at any depth.
The following example shows each pattern shape against a project with a top-level src/ directory and a nested copy under vendor/:
In gitignore patterns,
* matches within a single path segment and can appear at any position in the pattern, while ** matches across directories.[, ], and *, so the generated rule matches only the literal path you approved. Rules you write yourself aren’t escaped. Before v2.1.202, Claude Code saved the path unescaped, so a generated rule for a directory named [2024-06] Reports could fail to match its own path or match unintended sibling directories.
When Claude accesses a symlink, permission rules check two paths: the symlink itself and the file it resolves to. Allow and deny rules treat that pair differently: allow rules fall back to prompting you, while deny rules block outright.
- Allow rules: apply only when both the symlink path and its target match. A symlink inside an allowed directory that points outside it still prompts you.
- Deny rules: apply when either the symlink path or its target matches. A symlink that points to a denied file is itself denied.
Read(./project/**) allowed and Read(~/.ssh/**) denied, a symlink at ./project/key pointing to ~/.ssh/id_rsa is blocked: the target fails the allow rule and matches the deny rule.
WebFetch
WebFetch rules use adomain: prefix and match against the hostname of the requested URL. Matching is case-insensitive, supports * wildcards, and strips a trailing . from both the rule and the hostname so example.com. and example.com are treated the same.
WebFetch(domain:example.com)matches requests toexample.comWebFetch(domain:*.example.com)matches any subdomain at any depth, such asapi.example.comora.b.example.com, but notexample.comitselfWebFetch(domain:*)matches every domain and is equivalent to a bareWebFetchrule
*. or a bare *, the wildcard matches only the text between two dots. WebFetch(domain:example.*) matches example.org, where * becomes org, but not example.evil.com, where * would have to become evil.com and cross a dot. This keeps a trailing wildcard from matching domains an attacker could register.
MCP
MCP rules use the server name as configured in Claude Code, optionally followed by the name of a tool from that server.mcp__puppeteermatches any tool provided by thepuppeteerservermcp__puppeteer__*uses wildcard syntax and also matches all tools from thepuppeteerserver