Permissions

Permissions decide what Command Code may do before it does it. Every tool call - shell, file edit, web fetch, MCP tool, sub-agent - hits one engine that answers one question:

allow, ask, or deny?

New? Read the one-minute version and common recipes. For full reference, jump to how a decision is made and the decision table.

Note

Rules are enforced by Command Code, not the model. Your prompt or AGENTS.md shapes what the agent tries; it doesn't change what's allowed. To grant or revoke, use a rule, a mode, or a PreToolUse hook.

Three things cover almost everything.

1. A mode sets the baseline. Cycle with shift+tab, or switch with /mode.

The mode and the flag that turns it on are different names - so both columns:

ModeHow to set itIn one line
defaultthe default; /mode:default, shift+tabPrompt before anything that changes things. Reads are free.
auto-accept/mode:auto-accept, shift+tab, --auto-acceptDo normal edits (and safe file commands) without asking.
plan/mode:plan, shift+tab, --planRead-only. Explore and plan; only the plan file may be written.
bypass--yolo / --dangerously-skip-permissions (launch only)Do basically everything without asking.
dont-ask"defaultMode": "dont-ask" in settings, or --permission-mode dont-askNever ask. Run what's pre-approved; deny the rest. The CI/CD mode.

2. Three rule lists tune it. Order: deny wins, then ask, then allow.

.commandcode/settings.json

{ "permissions": { "deny": ["Read(secrets/**)"], "ask": ["Shell(git push:*)"], "allow": ["Shell(git status:*)", "mcp__github__get_issue"] } }

3. Some things never prompt. Reads and read-only shell (ls, cat, git status), plus the control-flow tools ask_user_question, agent, and run_command.

Info

deny beats ask beats allow, in every mode. A mode only decides calls the rules leave unresolved. bypass still can't run past a deny rule, an ask rule, or the root/home delete breaker.

Drop any of these into .commandcode/settings.json (shared, checked in) or .commandcode/settings.local.json (personal, gitignored).

Read-only git, ask before pushing, block history rewrites:

.commandcode/settings.json

{ "permissions": { "allow": ["Shell(git status:*)", "Shell(git log:*)", "Shell(git diff:*)"], "ask": ["Shell(git push:*)", "Shell(git pull:*)"], "deny": ["Shell(git push --force*)", "Shell(git reset --hard*)", "Shell(git clean -*)"] } }

Gate secret reads (free by default - reads are never blocked unless a rule says so):

.commandcode/settings.json

{ "permissions": { "ask": ["Read(.env*)"], "deny": ["Read(secrets/**)", "Read(~/.ssh/**)"] } }

Protect lockfiles and the git directory:

.commandcode/settings.json

{ "permissions": { "ask": ["Edit(package.json)", "Edit(pnpm-lock.yaml)"], "deny": ["Edit(.git/**)", "Edit(.ssh/**)"] } }

Allow one MCP tool, or a whole server:

.commandcode/settings.json

{ "permissions": { "allow": ["mcp__github__get_issue", "mcp__linear__*"] } }

Run unattended (CI/CD) - never prompt; pre-approve exactly what CI may touch:

.commandcode/settings.json

{ "permissions": { "defaultMode": "dont-ask", "allow": ["Edit(src/**)", "Write(src/generated/**)"] } }

Those edits run silently; anything outside the allowlist is denied. dont-ask is a fail-closed allowlist, not an auto-edit mode - a wrong call fails instead of hanging on a prompt no one answers.

A mode sets the baseline. Cycle with shift+tab, or switch with /mode.

ModeMeaning
defaultPrompt for anything mutating; reads are free.
auto-acceptDo normal workspace edits (and safe filesystem shell: mkdir, touch, cp, mv, non-recursive rm, rmdir, sed) without asking. Does not auto-accept arbitrary shell, MCP tools, or recursive deletes.
planRead-only exploration. Reads, searches, and read-only shell (git status) run; only the plan file may be written.
bypassDo basically everything without asking. Deny rules, explicit ask rules, and the root/home removal breaker still apply. Enter with --yolo / --dangerously-skip-permissions; throwaway environments only.
dont-askNever ask. Run only what policy already allows (reads, read-only shell, allow rules); deny the rest. The mirror of bypass: for unattended runs where a wrong prompt should fail, not wait.

Aliases: manualdefault, acceptEditsauto-accept, dontAskdont-ask, bypassPermissionsbypass. Legacy standard also maps to default.

Switching modes

Three ways, mid-session:

  • shift+tab cycles defaultauto-acceptplandefault. Launched with --yolo, bypass joins as a fourth rung after plan. (alt+m is an equivalent binding for terminals that swallow shift+tab.) dont-ask isn't a rung - set it via settings or CLI; shift+tab from it re-enters the cycle at auto-accept.
  • /mode shows the current mode; /mode:default, /mode:auto-accept, /mode:plan (or /mode <name>, any spelling) switch directly. Bypass is deliberately not slash-switchable - slash commands are agent-invokable, so a mid-session route into bypass would let the model kill its own prompts. /mode:yolo is gone; the command says "launch with --yolo."
  • CLI flags pick the start mode:

Terminal

cmd --permission-mode auto-accept # default | standard | plan | auto-accept | dont-ask cmd --auto-accept # shorthand for --permission-mode auto-accept cmd --plan # start in plan mode cmd --yolo # bypass - alias for --dangerously-skip-permissions; use with care

--permission-mode wins over --plan. Bypass has no --permission-mode value - it's launch-flag-only (--yolo / --dangerously-skip-permissions).

A mode switch also answers a prompt already on screen: auto-accept or bypass approves it (you said "stop asking"); plan denies it. A safety-forced prompt - destructive command, sensitive file, explicit ask rule - is never answered this way; it stays put for an explicit yes/no.

Three lists under permissions, checked in order: deny (always wins) → ask (always prompts, even in bypass; denies in dont-ask) → allow. Three tools skip the lists: ask_user_question (its UI is the interaction), agent (delegation has no side effect), and run_command (it just dispatches a slash command you named). Tools a sub-agent calls are still gated.

A rule is a tool name, optionally with a specifier:

Rule shape

Tool # the whole tool Tool(specifier) # a specific use
RuleEffect
ShellEvery shell command
Shell(git status)git status (and git status --short)
Shell(npm run *)Any npm run … command
Read(./.env)Reading .env in the project root
Edit(src/**)Edits anywhere under src/
WebFetch(domain:example.com)Fetches to example.com
mcp__github__get_issueOne tool from the github MCP server
mcp__github__*Every tool from the github server
mcp__github__get_*Every get_ tool from the github server
mcp__*Every tool from every MCP server
*Every tool
Shell(run_in_background:true)A shell command run in the background

Tool names are case-insensitive. Tool() and Tool(*) both mean the whole tool.

Wildcards

A * in a tool name (not a specifier) matches by name:

  • * - every permission-gated tool (ask_user_question, agent, run_command stay allowed).
  • mcp__* - every tool from every MCP server.
  • mcp__github__get_* - every read-style tool from one server.
  • edit_* - every tool whose name starts with edit_.

Tool-name wildcards work in deny and ask (deny: ["mcp__*"] blocks all MCP tools). They're ignored in allow unless the server is named (mcp__github__get_* is fine; a bare * or mcp__* is not) - an allow rule should say what it grants, not hand out everything. Specifier wildcards (Shell(npm *), Edit(src/**), WebFetch(domain:*.example.com)) work everywhere.

Match by input parameter

A deny or ask rule can gate on one top-level input parameter with Tool(param:value):

  • Shell(run_in_background:true) - a background shell command.

The value takes a * wildcard; a parameter the model omits never matches. One parameter per rule - to gate two, write two rules.

Parameter matching is deny/ask only - an allow rule for one parameter can't prove the whole call is safe. The fields a tool already matches with its own syntax - command (Shell), file_path (Read/Edit/Write), path (Grep/Glob), notebook_path, url (WebFetch) - are not parameter-matchable; use the specifier (Shell(rm *), not Shell(command:rm *)).

Tool names

Use the friendly names, or the exact tool names the model sees:

FriendlyCovers
Shellshell_command, monitor_command, kill_shell
Readread_file, read_multiple_files, read_directory, glob, grep
Editedit_file, write_file
Writewrite_file
WebFetchweb_fetch
WebSearchweb_search
Note

Shell is the name, because the shell tool is shell_command. Bash(...) is a legacy alias for Shell(...) so old configs keep working - but use Shell.

Shell patterns

Shell specifiers take * wildcards:

  • Shell(npm run build) - exact command.
  • Shell(npm run *) - any command starting with npm run .
  • Shell(git * main) - git checkout main, git merge main, …
  • Shell(git:*) - the :* suffix is a trailing wildcard, same as Shell(git *).

The space matters: Shell(ls *) matches ls -la but not lsof; Shell(ls*) matches both.

Command Code parses shell operators, so Shell(git status) does not grant git status && rm -rf .. Every subcommand of a compound command (&&, ||, ;, |, &, newlines) must match on its own.

File-path patterns

Read and Edit use gitignore-style matching with four anchors:

PatternAnchored atExample
//pathFilesystem rootRead(//etc/**)
~/pathHome directoryEdit(~/.config/**)
/pathProject rootEdit(/src/**)
pathAny depth under the projectRead(.env)Read(**/.env)

* matches within one path segment; ** crosses directories. For a symlink, both link and target are checked. A trailing slash can't dodge an exact-path rule (read_directory /repo/secrets/ still matches Read(//repo/secrets)).

Note

Reads are allowed by default. allow: ["Read(src/**)"] is accepted but does not block reads outside src/** - use ask/deny for sensitive reads. A strict read allowlist would be a separate change (see Known limits).

Aggressive deny, conservative allow

The lists are matched differently on purpose - failing toward a prompt is the safe direction:

  • Deny and ask match aggressively: they see through leading env vars (FOO=bar rm -rf x matches Shell(rm *)), process wrappers (timeout 30 …, nice …), and every subcommand; an unparseable command is matched against the raw string; path matching folds case (on macOS/Windows .ENV opens .env) and fires when either the path or its symlink target matches.
  • Allow matches conservatively: no env stripping (PATH=/tmp git status does not match Shell(git status:*)); a compound command allows only when every subcommand matches; an unparseable command never auto-allows; path matching is case-exact and needs both the path and its symlink target to match.

MCP and sub-agents

MCP tools are keyed by their canonical mcp__server__tool name - display names are never keys. mcp__github and mcp__github__* both match the whole github server; mcp__github__get_* matches its get_ tools; mcp__* (deny/ask only) matches every server.

The agent tool is always allowed and never prompts. A sub-agent runs the same ordered pipeline as the main loop, with one difference: where the main loop would draw an interactive prompt, the sub-agent policy auto-allows - it runs autonomously, with no human to answer. The hard guards hold: a sub-agent can't run a tool your deny rules block, can't write in plan mode, and anything the main loop would prompt on for safety - destructive command, sensitive file, outside-workspace write, explicit ask rule - fails closed (denied).

Every prompt offers three choices: allow once, allow and remember, or deny with feedback. The remembered scope matches the subject:

  • File ops - "Yes, allow all edits this session" turns on auto-accept for the session (same as shift+tab).
  • Shell - "Yes, don't ask again for git commands in this project" persists a Shell(git:*) allow rule to project settings. For a compound command, one rule per non-read-only base. Bases that run arbitrary code (bash, python, curl, …) persist the exact command, not a prefix.
  • MCP / other - "Yes, don't ask again for mcp__server__tool in this project" persists the tool name to project settings.

Command explanations on demand

The shell prompt can explain the command: press ctrl+e (ctrl+y in VS Code-family terminals, where the IDE grabs ctrl+e) and a background model call summarizes it inline. Governed by the on-demand-tool-descriptions setting (on by default): explanations only when asked. Turn it off (via /config) to generate every one upfront.

Some prompts are forced by a safety check, not a mode or rule. They offer only one-time Yes/No, plus a banner naming the check. Session and project choices are hidden on purpose - destructive-command, sensitive-path, outside-workspace, and explicit ask approvals are never cached, so the next risky call asks again. To stop a recurring one, add a content-specific allow rule (e.g. Edit(.commandcode/settings.json)).

The root/home removal circuit breaker

The only built-in safety prompt that survives bypass: a removal of the filesystem root or your home dir - rm -rf /, rm -rf ~, rm -rf $HOME - always stops for confirmation, in every mode that can ask (dont-ask and plan deny instead).

Spoof-resistant. It catches removals through env-var prefixes, process wrappers (timeout, nice, …), compound commands, quoted payloads (bash -c "rm -rf /; echo ok"), command substitution (echo $(rm -rf /;)), glued separators (/;), and $HOME/${HOME} spellings (quoted or not, with / or /* suffixes - $HOMEDIR stays literal).

Recursive deletes prompt under auto-accept

The auto-accept safe-filesystem fast path refuses rm -r/-R/-rf/--recursive (and find … -delete): rm -rf dist prompts exactly as in default. Plain single-file rm stays fast. Deliberate, safe direction: accepting edits isn't accepting bulk irreversible deletion. A content-specific allow rule (Shell(rm -rf dist)) still auto-allows, and bypass runs it.

Sensitive writes

Writing any of these prompts in default and auto-accept unless a content-specific allow rule opts in (bypass skips this gate):

Sensitive paths (prompt on write)

Secret material: .env, .env.*, *.pem, *.key, id_rsa, id_ed25519, credentials Persistence vectors: .bashrc, .zshrc, .profile, .gitconfig, .gitmodules, .mcp.json, … Control surfaces: .git/**, .ssh/**, .aws/**, .gnupg/**, .kube/**, .vscode/**, .idea/**, .husky/**, .devcontainer/**, node_modules/.bin/**, .commandcode settings

A "content-specific" allow rule must name the path (Edit(.env.local)) - a bare whole-tool allow (Edit) won't wave a call past a safety check.

Note

Sensitive reads through file tools don't prompt. Reading .env is allowed by default. Gate it with a rule: ask: ["Read(.env*)"] or deny: ["Read(.env*)", "Read(secrets/**)"]. The read-only shell fast path is stricter: cat .env or head id_rsa falls out and prompts.

External directories

Outside-workspace reads, writes, and shell working dirs first ask to admit the directory into the session's root set - the same grant as /add-dir or permissions.additionalDirectories - then the normal mode rule applies.

  • bypass grants the directory silently; dont-ask denies unless it's pre-approved in settings.
  • The OS temp dir (/tmp, $TMPDIR, os.tmpdir()) is granted silently in every mode - temp files are disposable, so asking to "extend access" to the machine's own temp dir is friction, not protection. Reads run prompt-free; writes skip the escalation but still confirm like any workspace write (sensitive names like /tmp/.env still force the prompt). deny/ask rules still outrank the grant, and a symlink resolving outside temp takes the normal gate.
  • Approving an ask rule on an outside path also grants the directory, so the tool-level backstop never rejects a call you just approved.
  • Registered skill directories are readable without a grant (skill resources live outside the workspace by design). Writes into them still gate.
  • Plan-file writes are exempt in plan mode - the mode's sanctioned output.
  • Absolute paths inside a shell command string are not external-dir gated - a command arg isn't a reliable file boundary. Use shell ask/deny rules.

Disabling bypass

Set permissions.disableBypass to "disable" (or true) to make bypass unenterable. Enforced at every layer: the engine evaluates a requested bypass as default, and the CLI neutralizes --dangerously-skip-permissions (and --yolo) at entry, so the TUI mode cycle and stored auto-approve decisions can't silently answer prompts as if bypass were live. Best set in the user-global file a project can't edit.

The whole machine. Every tool call runs a fixed order - the order is the spec, and tests pin it:

  1. Deny rules - blocked in every mode, including bypass.
  2. Ask rules - always prompt, even under bypass. In dont-ask the would-be prompt becomes a deny.
  3. External-directory gate - a read, write, or shell cwd outside the workspace first asks to admit the directory (like /add-dir / permissions.additionalDirectories); then the normal mode rule applies. Bypass grants silently. The OS temp dir (/tmp, $TMPDIR, os.tmpdir()) is granted silently in every mode - reads never prompt, writes skip the escalation but follow the normal mode rule. Skill dirs are readable without a grant; plan-file writes in plan mode skip this gate.
  4. Plan mode gate - operation-based: reads, searches, read-only shell run; everything that writes or mutates is denied. The only write allowed is a plan file under ~/.commandcode/plans/.
  5. Taste-directory writes - a product-workflow guard (allowed when taste learning is on, else redirected to the taste tool); not a safety prompt, so bypass skips it.
  6. Malformed writes - a write/edit with no resolvable file path is denied up front with a correction.
  7. Read-only fast path - reads and read-only shell (ls, cat, grep, echo, head, …) run without a prompt, every mode. A compound command counts when every part is read-only and nothing redirects - cat file | head -5 || echo none is free. A read-only command aimed at secret material (cat .env, head id_rsa) falls out and prompts.
  8. Root/home removal circuit breaker - rm -rf /, rm -rf ~, rm -rf $HOME and spoofed variants ask even under bypass (deny in dont-ask/plan).
  9. Bypass - everything past the gates above auto-allows.
  10. Sensitive-write ask - writing a secret file, a persistence vector (.bashrc, .gitconfig, …), or a control surface (.git/**, .ssh/**, .commandcode settings) prompts in default and auto-accept unless a content-specific allow rule opts in.
  11. Allow rules - pre-approved calls run without a prompt.
  12. Accept-edits fast path - in auto-accept, file edits and safe filesystem commands inside the workspace run without a prompt. Recursive deletes excluded - they still prompt.
  13. Otherwise, ask - and in dont-ask, deny.

Two consequences. First, deny beats ask beats allow, every mode. Second, bypass sits after the deny/ask rules, the external-dir gate, and the breaker - so --yolo skips ordinary prompts and the sensitive-write prompt, but not your explicit rules and not the root/home breaker.

Note

Every policy denial (deny rule, mode gate, malformed input) carries guidance the model can act on, and the turn continues. Only a human "no" ends the turn.

The full matrix - every operation against every mode. Authoritative; the tests pin each row.

Operationdefaultauto-acceptplanbypassdont-askWhy
Matches deny ruledenydenydenydenydenyDeny rules win. ask_user_question, agent, run_command are unconditional exceptions.
Matches ask ruleaskaskaskaskdenyAsk survives bypass; dont-ask can't prompt. Same three tools exempt.
Matches allow ruleallowallowallow if plan-compatibleallowallowHonored after deny/ask and mode gates.
Direct file read/search inside workspaceallowallowallowallowallowReads are free unless rules say otherwise.
Direct file read/search outside workspaceask external-dir, then allowask external-dir, then allowask external-dir, then allowallowdeny unless pre-approvedOutside dirs gated separately, then reads allowed.
Read under the OS temp dir (/tmp, $TMPDIR)allow (dir granted silently)allow (dir granted silently)allow (dir granted silently)allowallowDisposable working data - escalation skipped. Deny/ask still outrank; a symlink resolving outside temp takes the normal gate.
Write under the OS temp dirask like a workspace writeallow; sensitive still asksdenyallowdeny unless allow ruleNo escalation, then the normal mode edit/write rule (sensitive names like /tmp/.env still force the prompt).
Read under a registered skill dirallowallowallowallowallowSkill resources readable without a grant; writes into skill dirs still gate.
Web / read-only native toolallowallowallowallowallowNon-filesystem read-only tools are free unless rules say otherwise.
ask_user_questionallowallowallowallowallowAsking the user is the interaction; no prompt may stack in front of it.
agentallowallowallowallowallowDelegation is always allowed. Every sub-agent tool is still checked.
run_commandallowallow-allowallowDispatches a slash command you named - removed in plan mode (a write tool), never granted to sub-agents.
Read-only shell, e.g. git status, cat README.md | head -5allowallowallowallowallowPlan may explore with read-only shell. Compound qualifies when every part is read-only and nothing redirects. Shell cwd follows the external-dir row.
Secret-looking read via a file tool, e.g. read_file .envallowallowallowallowallowReads free by default; no built-in secret-read prompt for file tools. Use ask/deny.
Secret-looking read via shell, e.g. cat .envaskaskdenyallowdeny unless allow ruleA read-only shell command with a secret arg (.env, id_rsa, *.pem) falls out of the fast path.
Workspace file editaskallowdenyallowdeny unless allow ruleauto-accept accepts normal edits; dont-ask runs only pre-approved edits.
Protected workspace write, e.g. .env or .git/configaskaskdenyallowdeny unless pre-approvedBypass skips normal write-risk prompts; auto-accept doesn't.
Direct file write outside workspaceask external-dir, then ask edit/writeask external-dir, then allow normal edit/write; sensitive still asksdeny, except plan-file writesallowdeny unless pre-approvedExternal-dir gate first, then the mode edit/write rule.
Plan file write under ~/.commandcode/plans/ask like outside writeask external-dir, then allowallow (no external-dir prompt)allowdeny unless pre-approvedPlan mode's sanctioned output; never gated in plan mode.
Safe filesystem shell inside workspace: mkdir, touch, cp, mv, non-recursive rm, rmdir, sedaskallowdenyallowdeny unless allow ruleAccepting edits extends to safe filesystem commands on non-sensitive workspace targets. Recursive rm excluded.
Shell cwd outside workspaceask external-dir, then apply shell rule/modeask external-dir, then apply shell rule/modeask external-dir for read-only shell; deny mutating shellallowdeny unless pre-approvedThe shell working dir is external-dir gated.
Shell command args outside workspaceshell rule/modeshell rule/modeshell rule/modeallowshell rule/modePath args inside a command string are never external-dir gated; use shell ask/deny rules.
Other mutating shellaskaskdenyallowdeny unless allow ruleGoverned by shell rules and the active mode.
MCP / custom side-effecting or unknown toolaskaskdenyallowdeny unless allow ruleUnknown side effects prompt unless bypass or an explicit rule.
Root/home removal, e.g. rm -rf / or rm -rf ~askaskdenyaskdenyThe circuit breaker survives bypass.
Recursive workspace delete, e.g. rm -rf distaskaskdenyallowdeny unless allow ruleRecursive deletes prompt outside bypass. A content-specific allow rule (Shell(rm -rf dist)) still auto-allows.

Reading a column: a mode only changes calls the rules leave unresolved. deny and ask behave the same in every interactive mode; dont-ask turns every would-be ask into a deny; bypass auto-allows everything but deny rules, ask rules, and the breaker.

Rules live under permissions in .commandcode/settings.json (shared, checked in) or .commandcode/settings.local.json (personal, gitignored). Interactive approvals write to settings.json normally, and to settings.local.json when started with --local:

.commandcode/settings.json

{ "permissions": { "defaultMode": "default", "allow": [ "Shell(git status:*)", "Shell(git log:*)", "Shell(npm run *)", "mcp__github__get_issue" ], "ask": [ "Shell(git push:*)", "Read(.env*)" ], "deny": [ "Shell(git reset --hard*)", "Read(secrets/**)", "Edit(.git/**)" ], "additionalDirectories": ["~/shared-libs"], "disableBypass": "disable" } }
  • defaultMode - the start mode (default, auto-accept, plan, bypass, dont-ask; alternate spellings like acceptEdits and legacy standard work too).
  • allow / ask / deny - rule lists. They accumulate across settings files: a deny in your user-global settings can't be erased by a project file.
  • additionalDirectories - extra dirs that count as inside the workspace for reads and writes (same accumulation).
  • disableBypass - "disable" (or true) makes bypass unenterable.

Precedence

Denied at any level, allowed at none. A managed or user-global deny beats a project allow; deny beats ask; ask beats allow. Specificity doesn't reorder anything - a broad deny still blocks a narrower allow.

The reasoning behind the behavior, so you don't reverse-engineer it:

  1. Rules over a hardcoded safety matrix. No built-in .env file-read prompt, no bypass-surviving recursive-delete prompt. Sensitive reads are a rules concern (ask/deny), so each project decides what's sensitive. One carve-out: the read-only shell fast path screens secret args, so cat .env prompts instead of riding cat's free pass.
  2. One ordered pipeline. The order in How a decision is made is the spec; tests pin it.
  3. Asymmetric matching. Deny/ask aggressive, allow conservative. Failing toward a prompt is the safe direction.
  4. External directories are a separate gate, before the mode rule, with skill read roots and plan-file writes exempted where the mode sanctions them.
  5. The circuit breaker survives bypass and is spoof-resistant.
  6. disableBypass is enforced at every layer - engine, CLI entry, TUI mode cycle, decision store.
  7. Shell command args aren't a file boundary. Absolute paths in a command string follow shell rules and the mode, not the file-tool boundary.
  8. Generic tools get a real prompt. MCP/custom tools render a generic permission prompt, not a fake file-edit prompt.
  9. Recursive deletes prompt under auto-accept. Safe direction: bulk irreversible deletion isn't an edit.
  10. Policy denials guide the model; only human vetoes end the turn. Every engine denial carries actionable guidance, so the model adapts and the turn continues.

Documented edges of the current engine:

  • Wildcard include patterns in read_multiple_files expand inside the tool, so an exact-file deny rule (Read(.env)) can't match a wildcard include (*.env) before expansion. Directory-tree rules (Read(//etc/**)) still cover this, because the tool's targetDirectory is matched as a touched path.
  • No strict read allowlist: allow: ["Read(src/**)"] doesn't deny reads outside src/** - reads are free by default; only ask/deny restrict them. A deny-everything-not-listed read mode is out of scope as a separate change.
  • Shell path args aren't external-dir gated: cat /outside/file follows shell rules and the mode, not the workspace boundary. Gate it with deny: ["Shell(cat /outside/*)"]-style rules.
  • An allow rule never matches an opaque command: if the parser can't fully understand a command, it can still be deny/ask-matched against the raw string, but never auto-allows - it prompts.
  • Parameter rules only see parameters the model sends: an omitted parameter never matches.

  • Plan Mode - read-only exploration and planning.
  • Hooks - run your own scripts to allow, deny, or audit tool calls at runtime.
  • MCP - connect external tools the engine gates by server and tool name.