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.
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:
| Mode | How to set it | In one line |
|---|---|---|
default | the default; /mode:default, shift+tab | Prompt before anything that changes things. Reads are free. |
auto-accept | /mode:auto-accept, shift+tab, --auto-accept | Do normal edits (and safe file commands) without asking. |
plan | /mode:plan, shift+tab, --plan | Read-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-ask | Never 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
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.
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
Gate secret reads (free by default - reads are never blocked unless a rule says so):
.commandcode/settings.json
Protect lockfiles and the git directory:
.commandcode/settings.json
Allow one MCP tool, or a whole server:
.commandcode/settings.json
Run unattended (CI/CD) - never prompt; pre-approve exactly what CI may touch:
.commandcode/settings.json
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.
| Mode | Meaning |
|---|---|
default | Prompt for anything mutating; reads are free. |
auto-accept | Do 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. |
plan | Read-only exploration. Reads, searches, and read-only shell (git status) run; only the plan file may be written. |
bypass | Do 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-ask | Never 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: manual → default, acceptEdits → auto-accept, dontAsk → dont-ask, bypassPermissions → bypass. Legacy standard also maps to default.
Switching modes
Three ways, mid-session:
- shift+tab cycles
default→auto-accept→plan→default. Launched with--yolo,bypassjoins as a fourth rung afterplan. (alt+mis an equivalent binding for terminals that swallow shift+tab.)dont-askisn't a rung - set it via settings or CLI; shift+tab from it re-enters the cycle atauto-accept. /modeshows 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:yolois gone; the command says "launch with--yolo."- CLI flags pick the start mode:
Terminal
--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
| Rule | Effect |
|---|---|
Shell | Every 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_issue | One 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_commandstay allowed).mcp__*- every tool from every MCP server.mcp__github__get_*- every read-style tool from one server.edit_*- every tool whose name starts withedit_.
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:
| Friendly | Covers |
|---|---|
Shell | shell_command, monitor_command, kill_shell |
Read | read_file, read_multiple_files, read_directory, glob, grep |
Edit | edit_file, write_file |
Write | write_file |
WebFetch | web_fetch |
WebSearch | web_search |
Shell patterns
Shell specifiers take * wildcards:
Shell(npm run build)- exact command.Shell(npm run *)- any command starting withnpm run.Shell(git * main)-git checkout main,git merge main, …Shell(git:*)- the:*suffix is a trailing wildcard, same asShell(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:
| Pattern | Anchored at | Example |
|---|---|---|
//path | Filesystem root | Read(//etc/**) |
~/path | Home directory | Edit(~/.config/**) |
/path | Project root | Edit(/src/**) |
path | Any depth under the project | Read(.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)).
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 xmatchesShell(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.ENVopens.env) and fires when either the path or its symlink target matches. - Allow matches conservatively: no env stripping (
PATH=/tmp git statusdoes not matchShell(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
gitcommands in this project" persists aShell(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__toolin 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)
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.
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.
bypassgrants the directory silently;dont-askdenies 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/.envstill force the prompt).deny/askrules still outrank the grant, and a symlink resolving outside temp takes the normal gate. - Approving an
askrule 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/denyrules.
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:
- Deny rules - blocked in every mode, including bypass.
- Ask rules - always prompt, even under bypass. In
dont-askthe would-be prompt becomes a deny. - 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. - 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/. - Taste-directory writes - a product-workflow guard (allowed when taste learning is on, else redirected to the
tastetool); not a safety prompt, so bypass skips it. - Malformed writes - a write/edit with no resolvable file path is denied up front with a correction.
- 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 noneis free. A read-only command aimed at secret material (cat .env,head id_rsa) falls out and prompts. - Root/home removal circuit breaker -
rm -rf /,rm -rf ~,rm -rf $HOMEand spoofed variants ask even under bypass (deny indont-ask/plan). - Bypass - everything past the gates above auto-allows.
- Sensitive-write ask - writing a secret file, a persistence vector (
.bashrc,.gitconfig, …), or a control surface (.git/**,.ssh/**,.commandcodesettings) prompts indefaultandauto-acceptunless a content-specific allow rule opts in. - Allow rules - pre-approved calls run without a prompt.
- 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. - 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.
The full matrix - every operation against every mode. Authoritative; the tests pin each row.
| Operation | default | auto-accept | plan | bypass | dont-ask | Why |
|---|---|---|---|---|---|---|
Matches deny rule | deny | deny | deny | deny | deny | Deny rules win. ask_user_question, agent, run_command are unconditional exceptions. |
Matches ask rule | ask | ask | ask | ask | deny | Ask survives bypass; dont-ask can't prompt. Same three tools exempt. |
Matches allow rule | allow | allow | allow if plan-compatible | allow | allow | Honored after deny/ask and mode gates. |
| Direct file read/search inside workspace | allow | allow | allow | allow | allow | Reads are free unless rules say otherwise. |
| Direct file read/search outside workspace | ask external-dir, then allow | ask external-dir, then allow | ask external-dir, then allow | allow | deny unless pre-approved | Outside 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) | allow | allow | Disposable working data - escalation skipped. Deny/ask still outrank; a symlink resolving outside temp takes the normal gate. |
| Write under the OS temp dir | ask like a workspace write | allow; sensitive still asks | deny | allow | deny unless allow rule | No escalation, then the normal mode edit/write rule (sensitive names like /tmp/.env still force the prompt). |
| Read under a registered skill dir | allow | allow | allow | allow | allow | Skill resources readable without a grant; writes into skill dirs still gate. |
| Web / read-only native tool | allow | allow | allow | allow | allow | Non-filesystem read-only tools are free unless rules say otherwise. |
ask_user_question | allow | allow | allow | allow | allow | Asking the user is the interaction; no prompt may stack in front of it. |
agent | allow | allow | allow | allow | allow | Delegation is always allowed. Every sub-agent tool is still checked. |
run_command | allow | allow | - | allow | allow | Dispatches 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 -5 | allow | allow | allow | allow | allow | Plan 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 .env | allow | allow | allow | allow | allow | Reads free by default; no built-in secret-read prompt for file tools. Use ask/deny. |
Secret-looking read via shell, e.g. cat .env | ask | ask | deny | allow | deny unless allow rule | A read-only shell command with a secret arg (.env, id_rsa, *.pem) falls out of the fast path. |
| Workspace file edit | ask | allow | deny | allow | deny unless allow rule | auto-accept accepts normal edits; dont-ask runs only pre-approved edits. |
Protected workspace write, e.g. .env or .git/config | ask | ask | deny | allow | deny unless pre-approved | Bypass skips normal write-risk prompts; auto-accept doesn't. |
| Direct file write outside workspace | ask external-dir, then ask edit/write | ask external-dir, then allow normal edit/write; sensitive still asks | deny, except plan-file writes | allow | deny unless pre-approved | External-dir gate first, then the mode edit/write rule. |
Plan file write under ~/.commandcode/plans/ | ask like outside write | ask external-dir, then allow | allow (no external-dir prompt) | allow | deny unless pre-approved | Plan mode's sanctioned output; never gated in plan mode. |
Safe filesystem shell inside workspace: mkdir, touch, cp, mv, non-recursive rm, rmdir, sed | ask | allow | deny | allow | deny unless allow rule | Accepting edits extends to safe filesystem commands on non-sensitive workspace targets. Recursive rm excluded. |
| Shell cwd outside workspace | ask external-dir, then apply shell rule/mode | ask external-dir, then apply shell rule/mode | ask external-dir for read-only shell; deny mutating shell | allow | deny unless pre-approved | The shell working dir is external-dir gated. |
| Shell command args outside workspace | shell rule/mode | shell rule/mode | shell rule/mode | allow | shell rule/mode | Path args inside a command string are never external-dir gated; use shell ask/deny rules. |
| Other mutating shell | ask | ask | deny | allow | deny unless allow rule | Governed by shell rules and the active mode. |
| MCP / custom side-effecting or unknown tool | ask | ask | deny | allow | deny unless allow rule | Unknown side effects prompt unless bypass or an explicit rule. |
Root/home removal, e.g. rm -rf / or rm -rf ~ | ask | ask | deny | ask | deny | The circuit breaker survives bypass. |
Recursive workspace delete, e.g. rm -rf dist | ask | ask | deny | allow | deny unless allow rule | Recursive 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
defaultMode- the start mode (default,auto-accept,plan,bypass,dont-ask; alternate spellings likeacceptEditsand legacystandardwork too).allow/ask/deny- rule lists. They accumulate across settings files: adenyin 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"(ortrue) 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:
- Rules over a hardcoded safety matrix. No built-in
.envfile-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, socat .envprompts instead of ridingcat's free pass. - One ordered pipeline. The order in How a decision is made is the spec; tests pin it.
- Asymmetric matching. Deny/ask aggressive, allow conservative. Failing toward a prompt is the safe direction.
- External directories are a separate gate, before the mode rule, with skill read roots and plan-file writes exempted where the mode sanctions them.
- The circuit breaker survives bypass and is spoof-resistant.
disableBypassis enforced at every layer - engine, CLI entry, TUI mode cycle, decision store.- 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.
- Generic tools get a real prompt. MCP/custom tools render a generic permission prompt, not a fake file-edit prompt.
- Recursive deletes prompt under auto-accept. Safe direction: bulk irreversible deletion isn't an edit.
- 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
includepatterns inread_multiple_filesexpand 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'stargetDirectoryis matched as a touched path. - No strict read allowlist:
allow: ["Read(src/**)"]doesn't deny reads outsidesrc/**- reads are free by default; onlyask/denyrestrict 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/filefollows shell rules and the mode, not the workspace boundary. Gate it withdeny: ["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.