Skip to content

Permissions

How Claude Code decides what it may do without asking, how to write allow, ask and deny rules for every tool, and how trust, hooks and the sandbox fit in.

Permissions decide which actions Claude Code takes on its own, which ones it stops and asks you about, and which ones it refuses outright. You can keep rules in your personal settings, commit them for your team, or have your organisation enforce them. This page covers the rule syntax for each tool, how rules combine, and the places where a rule is weaker than it looks.

What asks by default

Out of the box, in Manual mode, Claude Code treats tools differently depending on how much harm they could do:

Kind of actionAsks first?What "Yes, and don't ask again" saves
Reading files, Grep, GlobNo, inside your working directoriesNot applicable
Shell commands (Bash)Yes, except a built-in set of read-only commandsA permanent rule for that command in this repository
Editing or writing filesYesApproval for the rest of the session only
WebFetchYes, except some pre-approved documentation domainsA permanent rule for that domain in this repository
WebSearchYesA permanent rule for this repository

Other permission modes shift these defaults. In auto mode a classifier reviews actions instead of you.

A prompt shows what Claude wants to do and then your choices, typically Yes, Yes, and don't ask again for ..., sometimes Yes, and switch to auto mode, and No.

Where saved approvals go

When an approval saves permanently (a Bash command or a fetch domain), Claude Code writes an allow rule to .claude/settings.local.json at the root of the git repository, resolving worktrees back to the main checkout. That means approving pnpm test in apps/web/ also covers sessions started in apps/api/ or in a worktree. The exceptions (outside git, on Windows, and a few ownership cases) are in settings. Before v2.1.211 the rule was saved in the starting directory instead; those older rules still apply where they were saved.

Sometimes a prompt offers only a one-off Yes. Claude Code only offers "don't ask again" when it can show you exactly what the saved rule would cover. If you want a broader rule, approve once and add it yourself in /permissions.

Leaving a note with your answer

On most prompts (Bash, PowerShell, file edits, MCP tools) you can highlight Yes or No and press Tab to type a comment. WebFetch and browser prompts do not offer this, nor do the options that save a rule or approve for the session.

Inside the comment field:

  • Enter submits the answer with the comment (an empty field submits without one).
  • Tab closes the field but keeps the text, which is still sent if you then pick that option.
  • Shift+Tab on a file prompt closes the field just like Tab.

A comment on Yes reaches Claude after the action's result. A comment on No is passed to Claude as the reason, and Claude keeps going with that in mind. Choosing No with no comment on a main-conversation prompt ends the turn. I use the "No, because..." route a lot: it is the fastest way to redirect Claude without losing the thread.

The /permissions dialog

/permissions lists every rule in force and the settings file each came from. You can add and remove rules there, even mid-turn: changes apply from Claude's next tool call (v2.1.234 and later).

There are three kinds of rule:

  • allow: run without asking
  • ask: always ask, even if something else would allow it
  • deny: never run

Claude Code checks them in a fixed order: deny, then ask, then allow. The first category that matches decides, regardless of how specific each rule is. So Bash(docker *) in deny blocks docker ps even if Bash(docker ps) sits in allow, and an ask rule beats an overlapping allow. You cannot punch a hole in a deny with a narrower allow.

How a deny rule behaves depends on its shape. A bare tool name such as WebSearch removes the tool from Claude's context altogether, so Claude never even sees it. A scoped rule such as Bash(rm *) leaves the tool available and blocks matching calls when Claude tries them. The only tool a deny cannot remove is EndConversation, while any other tool remains, and an ask rule never prompts for it.

Note: Rules are enforced by Claude Code itself, not by the model. Telling Claude "never touch the migrations folder" in CLAUDE.md influences what it tries; an Edit deny rule is what actually stops it.

When auto mode is available, the dialog also has an Auto mode tab showing the classifier's rules (see auto mode configuration).

Permission modes at a glance

The mode sets the baseline before any rules apply. Set the starting mode with permissions.defaultMode in a settings file; the full guide is permission modes.

ModeIn short
defaultAsk on first use of each tool. Shown as Manual; manual is accepted as an alias
acceptEditsFile edits and common filesystem commands (mkdir, touch, mv, cp) inside your working directories run without asking
planClaude explores with reads and read-only commands but does not edit source files
autoNo routine prompts; a background classifier checks risky actions against your request
dontAskAnything that would prompt is denied instead. Pre-allowed tools and actions that need no approval still run
bypassPermissionsNo prompts at all, apart from a small set of actions no mode auto-approves

Warning: bypassPermissions skips prompts even for writes to protected paths such as .git and .claude. Only use it in a throwaway container or VM.

To take bypassPermissions or auto off the table, set permissions.disableBypassPermissionsMode or permissions.disableAutoMode to "disable". They work in any file, but they really earn their keep in managed settings.

Rule syntax

Every rule is either Tool or Tool(specifier). Parentheses inside the specifier are taken literally, so you never need to escape them.

Whole-tool rules

A bare name covers every use of the tool: Bash, WebFetch, Read. Bash(*) means the same as Bash, and in deny both remove the tool from Claude's context.

Scoped rules

Add a specifier to narrow it down:

RuleCovers
Bash(make lint)Exactly the command make lint
Edit(./CHANGELOG.md)Edits to CHANGELOG.md in the current directory
WebFetch(domain:docs.python.org)Fetches from that host

Matching a tool parameter

Deny and ask rules can also match a top-level input parameter on any built-in tool, written Tool(param:value):

RuleCatches
Agent(model:opus)Subagent calls that ask for the Opus tier
Agent(isolation:worktree)Subagent calls that ask for a git worktree
Bash(run_in_background:true)Background shell commands

The rules for parameter matching:

  • Only direct fields of the tool's input count; nested fields cannot be matched.
  • One parameter per rule. To gate two, write two rules.
  • * matches any run of characters, so Agent(isolation:*) matches any explicit value. Without * the match is exact.
  • A parameter the model leaves out never matches, so Agent(model:*) misses calls with no model.
  • Values are compared exactly as sent: Agent(model:opus) matches the alias opus, not a full model ID.
  • A Skill(skill:<name>) deny is the exception: it matches the skill under any of its names.
  • Spaces around the colon are ignored. Run with --verbose to see real parameter names and values.

This is deliberately deny and ask only. Knowing one parameter is safe does not prove a call is safe, so allow rules keep each tool's normal syntax.

You cannot match a tool's main content field this way: command on Bash and PowerShell, file_path on Read, Edit and Write, path on Grep and Glob, notebook_path on NotebookEdit, url on WebFetch. A rule like Bash(command:rm *) is ignored with a startup warning, because compound commands would slip past it. Use Bash(rm *), Read(./path) or WebFetch(domain:host).

For MCP tools, parameter rules only work as a deny passed with --disallowedTools. In a settings file, any mcp__ rule with parentheses is skipped and reported in the invalid-settings dialog and claude doctor.

Wildcards in Bash rules

In a Bash rule, * matches any text, spaces included. Write the command as you would type it and replace the parts that vary.

Warning: Put the * after the subcommand. Everything before the first * is matched literally, and that is what limits the rule. Bash(docker compose *) allows only docker compose commands; Bash(docker *) allows every docker command. Claude Code warns at startup about an allow rule with * before the subcommand, such as Bash(git * main).

Here is how various shapes behave:

RuleMatchesDoes not match
Bash(cargo build)cargo buildcargo build --release
Bash(cargo build *)cargo build, cargo build --releasecargo test
Bash(kubectl get * -n staging)kubectl get pods -n stagingkubectl get pods, kubectl delete pod x -n staging
Bash(git * main)git merge main, git push origin main, even git -c core.pager=x diff maingit status
Bash(* --version)python --version, any program with that flagpython -V
Bash(cat *)cat README.md, catcatalog
Bash(cat*)cat README.md, catalog
Bash(* --dry-run *)terraform --dry-run planterraform --dry-run

Three things explain that table:

  • * replaces whatever is in its position, which might be a subcommand, a program name or a run of options, including ones like git's -c that execute code.
  • A trailing * also matches the bare command, but only when it is the rule's sole wildcard.
  • The space before a trailing * matters. cat * needs a space after cat; cat* does not, so it also matches catalog.

:* at the very end is an alternative spelling of a trailing *, so Bash(cat:*) equals Bash(cat *). Anywhere else the colon is literal. Rules saved from a prompt use the space form.

A typical project block, allowing Make targets and commits but refusing deploys:

{
  "permissions": {
    "allow": [
      "Bash(make *)",
      "Bash(git commit *)"
    ],
    "deny": [
      "Bash(make deploy *)"
    ]
  }
}

Because deny is checked first, make deploy prod is refused even though Bash(make *) would allow it.

Wildcards in the tool name

Deny and ask rules may use a glob as the tool name, matched against the full name. "*" matches every tool; "mcp__*" matches every MCP tool on every server. A bare-name glob deny removes the matched tools from context, with the same EndConversation exception as above.

Allow rules accept a tool-name glob only after a literal mcp__<server>__ prefix, with no glob in the server part. mcp__sentry__* allows every Sentry tool; mcp__sentry__list_* allows its list tools. An unanchored allow glob like "*" or "mcp__*" is skipped with a warning.

If a deny or ask rule names a tool that does not exist, you get a startup warning, which catches typos. Names containing _ or *, and names of tools Claude Code has retired (such as TaskOutput), are exempt.

Rules use canonical tool names, not the labels in the transcript. The tool shown as "Stop Task" is TaskStop, for instance. The tools reference has the canonical list.

Rules for specific tools

Bash

Bash rules match the full command text. The finer points:

Compound commands

Claude Code understands shell operators: &&, ||, ;, |, |&, & and newlines. A command is split on them and every part must be allowed for the whole thing to run without asking. Bash(npm test *) does not approve npm test && rm -rf dist.

Deny and ask rules fire if any part matches, including commands buried in a subshell, $(...) substitution or a loop body. With Bash(git reset *) in ask, cd repo && git reset --hard and echo "$(git reset --hard)" both prompt, even in auto mode.

A dangling && or || (as in npm test &&) makes the command unparseable, so it is not split and allow rules do not approve it.

When you approve a compound command with "don't ask again", Claude Code saves separate rules for each part that needed approval, up to five. Approving git status && pnpm lint saves pnpm lint, so it is recognised later whatever precedes it. A cd to somewhere outside your working directories generates its own Read rule for that path.

Wrappers

Before matching, Claude Code strips a fixed set of wrappers that just run their argument: timeout, time, nice, nohup, stdbuf, the builtins command and builtin, and zsh's noglob. So Bash(pytest *) also matches timeout 120 pytest -x. command -v (a lookup) and zsh's nocorrect are not stripped.

A leading assignment of certain known-safe environment variables is stripped too, so Bash(npm test *) matches NODE_ENV=test npm test. Allow rules stop at any other assignment. Deny and ask rules look past every leading assignment, so a Bash(rm *) deny still catches TMPDIR=x rm -rf out/.

Bare xargs is stripped (Bash(grep *) matches xargs grep TODO), but xargs with flags is treated as an xargs command.

The list is fixed. Environment runners such as direnv exec, devbox run, mise exec, npx and docker exec are not stripped, which means Bash(mise exec *) approves anything after exec. Write one rule per inner command instead: Bash(mise exec -- pytest).

Exec-style wrappers such as watch, setsid, ionice and flock cannot be approved by a prefix rule and always prompt in Manual mode. Likewise find with -exec or -delete is not covered by Bash(find *). Use an exact rule for the full command if you need one.

What a Bash rule does not match

A rule matches the text Claude writes, after splitting and wrapper stripping. It does not recognise the same program invoked another way, so a deny or ask rule is a guard on Claude's usual phrasing, not a security boundary around the program.

Rule in deny or askStopsDoes not stop
Bash(wget *)wget https://example.org/file/usr/bin/wget ..., bash -c 'wget ...'
Bash(rm *)rm -rf node_modules/bin/rm -rf node_modules, sh -c 'rm -rf node_modules'
Bash(git push *)git push origin featuregit -C . push, git -c push.default=current push, git 'push'

What happens to the forms in the right-hand column is down to your other rules and the mode. For enforcement that does not depend on wording, use the sandbox; for custom inspection of the full command, use a PreToolUse hook.

Trying to restrict arguments with patterns is especially brittle. A rule like Bash(curl https://api.example.com/ *) misses options before the URL, other schemes, redirects and variables. Better options:

  • Deny curl, wget and friends, and allow WebFetch(domain:api.example.com) instead, backed by the sandbox network allowlist.
  • Validate URLs in a PreToolUse hook.
  • Describe allowed patterns in CLAUDE.md, as guidance rather than enforcement.

And remember that allowing WebFetch for a domain does nothing to stop curl if Bash is allowed.

Read-only commands

A built-in, non-configurable set of commands is treated as read-only and runs without a prompt in every mode: ls, cat, echo, pwd, head, tail, grep, find, wc, which, diff, stat, du, cd and the read-only forms of git, among others. To force a prompt for one, add an ask or deny rule. permissions.blockReadsOutsideWorkingDirectories changes this for paths outside your working directories, and in auto mode these can still go to the classifier.

Unquoted globs are fine when every flag the command has is read-only, so wc -l lib/*.rb runs straight away. In Manual mode, read-only commands still prompt when:

  • an unquoted glob appears in a command with write- or exec-capable flags (find, sort, sed, git), since the glob could expand to something like -delete
  • docker is pointed at another daemon with -H, --context, or Podman's --url or --connection
  • file is given -m/--magic-file or -f/--files-from
  • an argument is a Windows network (UNC) path, which could leak credentials (this applies to PowerShell too)
  • the command sets, unsets or loops over special shell variables such as PATH or IFS
  • the command cannot be parsed, or is longer than 10,000 characters

A cd into a working directory is read-only too, so cd services/auth && ls runs without a prompt. Two combinations still prompt: cd plus git when the cd moves to a different directory (git hooks in the new folder could run), and cd plus a redirect whose target Claude Code cannot resolve. A redirect to /dev/null is always fine.

Redirections

Redirect targets are checked as if Claude were reading or writing the file directly:

  • Output (>, >>, 2>): checked against your Edit rules, protected paths and working directories. Bash(git commit *) allows the command but not an arbitrary output file. Targets starting with ~ or containing a glob need approval.
  • Input (<): checked against Read rules and working directories (v2.1.257+). A glob target, or a relative target after a cd in the same command, always needs approval.

/dev/null, descriptor forms like 2>&1, here-docs and here-strings are not checked. Files written by tee, including in a pipeline such as pytest | tee results.txt, get the same output checks (v2.1.269+); Bash(tee *) does not approve a destination outside your working directories.

PowerShell

PowerShell rules look just like Bash ones: * anywhere, :* as a trailing alternative, and bare PowerShell or PowerShell(*) for everything. Common aliases are resolved first and matching is case-insensitive, so PowerShell(Get-Content *) also catches gc and cat.

{
  "permissions": {
    "allow": [
      "PowerShell(Get-Content *)",
      "PowerShell(dotnet build *)"
    ],
    "deny": [
      "PowerShell(Remove-Item *)"
    ]
  }
}

Claude Code parses the PowerShell syntax tree and splits on |, ; and, on PowerShell 7 or later, && and ||. Every part must match an allow rule.

Read and Edit

To keep Claude's file tools away from something, add a Read deny for its path, such as Read(./.env) or Read(./certs/**). A .claudeignore file does nothing; move its entries into deny rules.

  • Edit rules cover every built-in tool that edits files.
  • Read rules are applied, best effort, to every built-in tool that reads (Grep and Glob included), to @file mentions and to the open-file context an IDE shares.
  • A Read deny also blocks Edit and Write on the same path, including creating a file there (v2.1.208+ for edits, v2.1.228+ for writes). NotebookEdit is not covered, so add an Edit deny where nothing should change.
  • Path rules are only consulted for Edit(...) and Read(...). A path rule written for Write, NotebookEdit, Glob or the legacy MultiEdit is accepted but never checked, and you get a startup warning (v2.1.210+). Use Edit(...) for writes and Read(...) for globbing. A bare tool name like Write with no path still works at tool level.

Warning: Read and Edit denies apply to the built-in file tools, to file commands Claude Code recognises in Bash (such as cat, head, tail, sed, tee) and to redirect targets. They do not stop a command that reads files without naming them, like grep -r secret ., or a script that opens files itself. For a real OS-level block, turn on the sandbox.

Path anchors

Read and Edit rules use gitignore syntax with four kinds of anchor:

PrefixAnchored atExample
//Filesystem rootRead(//etc/ssl/private/**)
~/Your home directoryRead(~/.kube/config)
/The settings source (see below)Edit(/migrations/**)
none or ./The current directoryRead(*.pem)

Warning: /Users/sam/notes.txt is not an absolute path in a rule. A single leading slash anchors to the settings source. Write //Users/sam/notes.txt for an absolute path.

Where a /path rule anchors depends on the file it is in:

Rule lives in/path means
.claude/settings.json or .claude/settings.local.json<primary working directory>/path
~/.claude/settings.json~/.claude/path
A --settings <file> file<that file's directory>/path
CLI flags or session rules<primary working directory>/path

Rules added via /permissions follow the row for the file they are saved to. Local-file rules anchor at the primary working directory, not at the repo root where the file is stored, so in a worktree Edit(/src/**) means that worktree's src/.

The gotcha I see most: putting Read(/secrets/**) in your user file blocks ~/.claude/secrets/**, not your project's secrets/. For a user-level rule that bites in every project, use // or ~/.

On Windows, paths become POSIX-style before matching (C:\Users\sam becomes /c/Users/sam), so //c/**/.env covers the C drive and //**/.env covers all drives.

Matching depth

A rule never matches outside its anchor. Within that, a bare filename matches at any depth, gitignore style, so Read(.env) equals Read(**/.env). To block a name everywhere on disk, use Read(//**/.env).

A relative pattern with one directory segment, such as dist/**, depends on the rule type:

  • allow: Edit(dist/**) matches only <cwd>/dist. Use Edit(**/dist/**) for any depth.
  • deny or ask: Read(dist/**) matches a dist directory at any depth.

Other shapes behave the same in all rule types: Edit(/dist/**) and Edit(packages/ui/**) match only where anchored; Edit(**/dist/**) matches everywhere. Take a repo with dist/main.js and packages/ui/dist/index.js:

Ruledist/main.jspackages/ui/dist/index.js
Edit(dist/**) as allowYesNo
Edit(dist/**) as deny or askYesYes
Edit(/dist/**)YesNo
Edit(**/dist/**)YesYes

In these patterns * stays within one path segment and ** crosses directories.

Escaping, negation and odd paths

Paths approved through "don't ask again" have gitignore special characters ([, ], *) escaped so the saved rule matches only that literal path (v2.1.202+). Rules you write yourself are not escaped. Parentheses never need escaping: Edit(./Q3 (draft)/**) works as written.

A deny or ask path that is not a valid gitignore pattern still protects that exact path; an allow with an invalid pattern approves nothing.

A deny or ask entry starting with ! is a negation that carves paths out of earlier relative rules in the same list. In one file, Read(*.key) followed by Read(!test-fixture.key) blocks every .key file except ones named test-fixture.key. A ! listed first does nothing, and a negation only affects rules from the same source, so a project ! cannot reopen a managed deny. It is always read relative to the current directory, so it cannot carve into /, ~/ or // rules, and it cannot reopen a file inside a directory that is denied as a whole.

When a requested path passes through a symlink (or a Windows junction), both the requested path and the resolved target are checked:

  • allow needs both to match. A link inside an allowed folder that points outside it is not allowed.
  • deny needs either to match. A link to a denied file is denied.

On macOS and Linux, a deny or ask written with //, ~/ or / through a symlinked directory also applies at the real location (v2.1.268+): Read(//tmp/**) covers /private/tmp on macOS. Grep and Glob search the resolved directory and Read denies apply to it.

The Edit and Write tools refuse to write to a path that is itself a symlink and point Claude at the target. Writes that pass through a symlinked parent folder, or happen via a shell command, are judged by where they resolve: if the target lands outside your working directories it is not auto-approved in acceptEdits, and in auto mode you are prompted rather than the classifier. Targets that resolve to a protected path follow the protected-path rules, again prompting instead of going to the classifier. Paths that cannot be resolved (a symlink loop, say) are refused, and tools re-check the resolution when they actually open the file.

WebFetch

WebFetch rules use domain: and match the hostname, case-insensitively, ignoring a trailing dot.

  • WebFetch(domain:pypi.org) matches pypi.org
  • WebFetch(domain:*.pypi.org) matches any subdomain at any depth, but not pypi.org itself
  • WebFetch(domain:*) matches every host

Apart from a leading *. or a lone *, a wildcard covers only one label: WebFetch(domain:mycorp.*) matches mycorp.io but not mycorp.attacker.net. Wildcards need v2.1.172 or later.

Bare WebFetch versus domain:*

Both cover every URL, but they do different jobs:

RuleIn allowIn deny
WebFetchFetches run without asking; sandbox network list unchangedThe tool is removed; sandbox network list unchanged
WebFetch(domain:*)Fetches run without asking, and sandboxed commands may reach any hostThe tool stays but every fetch is refused, and sandboxed commands can reach no host

They also differ for reads of claude.ai artifacts. A bare WebFetch deny or ask does not touch those reads; a domain: rule covering claude.ai or *.claudeusercontent.com (or domain:*) does, as does an Artifact rule.

If you want free fetching without widening the sandbox, allow the bare form:

{
  "permissions": {
    "allow": ["WebFetch"]
  }
}

A sandboxed curl to an unlisted host will still prompt you for that host.

MCP

MCP rules use the server name you configured:

  • mcp__linear covers every tool from the linear server
  • mcp__linear__* is the same thing written as a wildcard
  • mcp__linear__create_issue covers one tool

If your organisation has set a claude.ai connector tool to ask and that reaches your session, allow rules for it are ignored and every call prompts, even in auto and bypassPermissions; dontAsk denies it. Connectors that Claude Code fetches itself appear as mcp__claude_ai_<server>__<tool>. See MCP.

In Cowork sessions inside the Claude Desktop app, shell commands and fetches go through mcp__workspace__bash and mcp__workspace__web_fetch. A deny for the whole Bash or WebFetch tool also applies to those, but allow rules do not carry over.

Agent (subagents)

Agent(Name) rules control which subagents Claude can launch: Agent(Explore), Agent(Plan), or a custom one such as Agent(release-notes). Put them in deny or pass --disallowedTools:

{
  "permissions": {
    "deny": ["Agent(Plan)"]
  }
}

Cd

Cd rules control where /cd may move the session. Claude cannot call Cd; the rules apply only when you run /cd yourself.

  • A bare Cd deny disables /cd. A Cd(<pattern>) deny blocks matching targets, checked against every spelling of the path including each symlink hop.
  • Any Cd allow rule switches to allowlist mode: the target must match an allow, or /cd refuses. With no Cd rules, /cd asks you to trust unfamiliar folders as normal.

Patterns use the same //, ~/ and / anchors as Read and Edit, but match the whole directory path: * is exactly one segment, ** crosses segments, and a trailing /** includes the root.

RuleMatchesDoes not match
Cd(~/clients/*)~/clients/acme~/clients, ~/clients/acme/web
Cd(~/clients/**)~/clients and everything under itanything outside ~/clients
Cd(**/.venv)any .venv below the current directory.venv/lib

Adding logic with hooks

Hooks let you run your own code during permission checks. A PreToolUse hook runs before the prompt for every tool except EndConversation, and can deny the call, force a prompt, or allow it through.

A hook cannot override your rules: deny and ask rules are still evaluated whatever it returns, so a matching deny blocks and a matching ask still prompts even if the hook says "allow". MCP tools marked requiresUserInteraction, and connector tools your organisation set to ask, also still prompt.

A hook that exits with code 2 blocks the call before rules are even looked at, so it beats allow rules. That gives you a neat pattern: allow Bash wholesale, then use a hook to reject the handful of commands you never want. Full details are in the hooks reference.

Mods that handle tool.check are different. They answer after rules and hooks, and can override them: approving an ask-rule call, overruling a non-managed hook's block, or skipping the auto mode classifier. Deny rules hold over a mod by default on machines with managed settings or on Team and Enterprise plans; elsewhere a mod can approve something a deny refuses. Decide carefully which mods you trust.

Working directories

Claude can read files in the directory you started in, which is the session's primary working directory. To widen that:

  • at launch: --add-dir <path>
  • mid-session: /add-dir
  • permanently: permissions.additionalDirectories in a settings file

Added directories follow the same rules: reads without prompts, edits according to the current mode. Most network paths (UNC shares such as \\server\share) cannot be added; on Windows, map a drive letter and pass that with --add-dir.

permissions.blockReadsOutsideWorkingDirectories makes the file tools refuse paths outside these directories in every mode. In auto mode, Claude Code offers to switch it on the first time Claude reads outside them.

On macOS, background sessions ask separately for access to ~/Desktop, ~/Documents and ~/Downloads. If reads there fail with Operation not permitted, grant the session host access (see agent view).

Moving with /cd

/cd <path> changes the primary working directory instead of adding one. The conversation carries on, the new folder's CLAUDE.md loads, and you are asked to trust the folder if it is new to you. --resume from the new folder then finds the session.

From v2.1.246, the move immediately applies the new folder's project settings (rules and hooks), its .mcp.json and local-scope MCP servers (subject to the usual approval), its enabled plugins, skills and subagents, and its env values layered over the old ones. The previous folder's project and local MCP servers disconnect, as do servers from plugins no longer enabled. Additional directories come from the new folder's settings, while ones you added with --add-dir or /add-dir stay. Hooks still see ${CLAUDE_PROJECT_DIR} as the original project root. If the new folder is untrusted, the trust prompt lists the allow rules, directories, hooks and helpers it would activate; decline and you stay put.

Added directories give file access, not configuration

Adding a directory lets Claude read and edit there; it does not turn that folder into a full config root. A few config types load from directories added with --add-dir or /add-dir (including SDK additionalDirectories and add_dirs, which pass --add-dir), but not from permissions.additionalDirectories:

ConfigurationLoaded from an added directory?
Skills in .claude/skills/Yes, with live reload
Commands in .claude/commands/Yes, no live reload; your project's command wins a name clash
Subagents in .claude/agents/Yes, no live reload
.claude/settings.json and .claude/settings.local.jsonOnly enabledPlugins and extraKnownMarketplaces
CLAUDE.md, .claude/rules/, CLAUDE.local.mdOnly with CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1; CLAUDE.local.md also needs the local setting source

These load via the project setting source, so --setting-sources without it skips them, and bare mode skips the commands and subagents. Running /add-dir on a subfolder of your current project (v2.1.257+) loads its skills, commands and subagents without adding a new working directory.

Hooks and other project settings load only from the current directory's .claude/, with no parent fallback. Output styles are found in the current directory and its parents, ~/.claude/ and managed settings. To share config across projects, put it in ~/.claude/, package it as a plugin, or start Claude Code from the folder that holds it.

Permissions and the sandbox

The two layers complement each other:

  • Permissions decide which tools Claude may use and which paths and domains it may touch, across every tool.
  • Sandboxing enforces filesystem and network limits at OS level on Bash, PowerShell and Monitor commands and their child processes.

The sandbox still holds if a prompt injection talks Claude into something, so I run both. Paths and domains from your permission rules are merged into the sandbox configuration.

With the sandbox on and autoAllowBashIfSandboxed left at its default of true, sandboxed Bash commands run without prompting even if you have a bare Bash ask rule; the sandbox stands in for that blanket prompt. Plan mode is the exception (v2.1.212+): there the substitution is skipped, so built-in read-only commands still run, other commands follow the normal flow, and a bare Bash ask makes everything prompt.

Even with sandboxing, content-scoped ask rules like Bash(git push *) still prompt, deny rules still apply, and rm or rmdir against a critical path still goes through the normal flow. Commands that run outside the sandbox respect a bare Bash ask as usual.

Managed settings

Organisations can enforce rules through managed settings, which user and project files cannot override (bar a few security keys). allowManagedPermissionRulesOnly makes managed settings the only source of permission rules. disableBypassPermissionsMode is usually managed but works anywhere, so you can lock yourself out of bypass mode if you want the safety net.

How rules combine across files

Permission rules follow the normal settings precedence, but because rule lists merge and deny is evaluated first, the practical effect is simple: a deny anywhere wins. A managed deny cannot be undone with --allowedTools; --disallowedTools can add further restrictions. A project deny blocks a user allow, and a user deny blocks a project allow.

Embedding hosts can add managed policy through the SDK managedSettings option, including allow rules, unless the admin sets the allowManaged*Only locks.

Project allow rules and workspace trust

Allow rules and permissions.additionalDirectories in a committed .claude/settings.json grant power, so they only apply once you accept the workspace trust dialog for that folder. The dialog lists what they would grant. Deny and ask rules apply immediately because they only restrict. See security for the wider trust model.

Trust is recorded against:

  • the git repository root when you are in a repo (worktrees use the main checkout), covering everything except nested repositories such as submodules
  • the starting directory outside a repo, covering its subfolders except nested repositories
  • nothing on disk when you start in your home directory; trust lasts for that session only

The dialog appears only in interactive sessions. claude -p and SDK sessions never show it, and trusting a parent folder does not count for these rules. Background sessions check trust before starting: claude --bg in an untrusted folder shows the dialog first, and from a script it exits with a Workspace not trusted error.

When your local file needs trust

Your .claude/settings.local.json normally skips trust. If git tracks it, or .claude is a symlink, it is treated as repository-supplied and waits for trust like the shared file. To tell the difference Claude Code has to run git, which it only does once the folder is trusted (by you, by a covering parent, or implicitly in -p and SDK runs). Until then, a local file in your configuration home (your home directory, or the parent of a CLAUDE_CONFIG_DIR .claude folder) applies immediately; anywhere else it is held. Before v2.1.207, an untracked file's rules applied before the dialog.

~/.claude/settings.local.json is still local scope: it only applies to sessions started in your home directory. Put cross-project rules in ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR/settings.json).

What runs before you trust a folder

What the repo suppliesYou trusted only a parentclaude -p or SDK, never trusted
Settings hooks, the env block, helpers such as apiKeyHelper, a project skill's hooks and allowed-toolsUsedUsed (trust never gates a skill's allowed-tools)
permissions.allow and additionalDirectories in .claude/settings.jsonHeld until you accept the dialog, which reappears listing themNot used; a "this workspace has not been trusted" warning goes to stderr
Project subagent frontmatter hooks, a project @skills-dir plugin, extraKnownMarketplaces from the repo or an added directoryNot used, no dialog offeredNot used
Inline mcpServers in a repo or added-directory subagent's frontmatterNot used, no dialog offeredNot used
Servers in .mcp.jsonYou are asked before each connects; the repo's own approvals do not countConnected without asking (SDK only with project settingSources)
A headersHelper on a .mcp.json serverNot run until you accept the dialog; static headers used meanwhileNot run; static headers used and a "headersHelper not run" line printed

Rows that need this exact folder trusted can be trusted by hand by setting projects["<path>"].hasTrustDialogAccepted to true in ~/.claude.json, using the repo root (or the folder itself outside a repo). The exact key appears in the debug log or stderr message for whatever was skipped.

Before running claude -p against a repository you did not write, consider:

  • --setting-sources user (or SDK settingSources without project) so neither project settings nor .mcp.json are read
  • --bare, which skips project hooks, skills, commands, subagents, plugins and .mcp.json; the project env block and helpers like awsAuthRefresh still apply, and apiKeyHelper is read only from --settings
  • --settings '{"disableAllHooks": true}', since setting it only in your user file can be overridden by the project
  • a disabledMcpjsonServers entry to reject a named .mcp.json server everywhere

Starter configurations for common setups are in Anthropic's public claude-code repository, and this handbook's example settings files show full personal, team and managed files.