command-code 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,76 +2,101 @@
2
2
 
3
3
  # Permissions
4
4
 
5
- Permissions decide what Command Code is allowed to do before it does it. Every tool call - a shell command, a file edit, a web fetch, an MCP tool, a sub-agent - passes through **one central permission engine** that answers a single question: **allow, ask, or deny?**
5
+ 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:
6
6
 
7
- The semantics in brief: `deny`/`ask` rules win for permission-gated tools, `plan` is read-only exploration, `auto-accept` accepts edits plus safe filesystem commands, `bypass` skips normal prompts, and `dont-ask` denies anything that would need a prompt. The `ask_user_question` and `agent` control-flow tools - plus `run_command`, which just dispatches a slash command you named - are always allowed. This page is the complete reference - the pipeline, the modes, the rule syntax, the full decision table, and the known limits.
7
+ > **allow, ask, or deny?**
8
+
9
+ New? Read [the one-minute version](#the-one-minute-version) and [common recipes](#common-recipes). For full reference, jump to [how a decision is made](#how-a-decision-is-made) and the [decision table](#the-decision-table).
10
+
11
+ 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.
8
12
 
9
13
  ---
10
14
 
11
- ## How a decision is made
15
+ ## The one-minute version
12
16
 
13
- Every tool call is evaluated in a fixed order. The order **is** the spec - tests pin it:
17
+ Three things cover almost everything.
14
18
 
15
- 1. **Deny rules** - blocked in every mode, including bypass.
16
- 2. **Ask rules** - always prompt, even under bypass. In `dont-ask` mode the would-be prompt becomes a deny.
17
- 3. **External-directory gate** - a read, write, or shell working directory **outside the workspace** first asks to admit the directory into the session's root set (like `/add-dir` / `permissions.additionalDirectories`); then the normal mode rule applies. Bypass grants the directory silently. The **OS temp directory** (`/tmp`, `$TMPDIR`, `os.tmpdir()`) is granted silently in every mode - reads there never prompt, and writes skip the escalation but still follow the normal mode rule. Registered skill directories are readable without a grant; plan-file writes in plan mode skip this gate.
18
- 4. **Plan mode gate** - operation-based: reads, searches, and read-only shell run; everything that writes or runs a mutating command is denied. The only write allowed is a plan file under `~/.commandcode/plans/`.
19
- 5. **Taste-directory writes** - a product workflow guard (allowed when taste learning is on, otherwise redirected to the `taste` tool); not a safety prompt, so bypass skips it.
20
- 6. **Malformed writes** - a write/edit with no resolvable file path is denied up front with a correction.
21
- 7. **Read-only fast path** - reads and read-only shell commands (`ls`, `cat`, `grep`, `echo`, `head`, …) run without a prompt, in 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 pointed at secret material (`cat .env`, `head id_rsa`) falls out of the fast path and prompts.
22
- 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`, where nothing can ask).
23
- 9. **Bypass** - everything that survived the gates above auto-allows.
24
- 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.
25
- 11. **Allow rules** - pre-approved calls run without a prompt.
26
- 12. **Accept-edits fast path** - in `auto-accept` mode, file edits and safe filesystem commands inside the workspace run without a prompt. Recursive deletes are excluded - they still prompt.
27
- 13. Otherwise, **ask** - and in `dont-ask` mode, **deny**.
19
+ **1. A mode sets the baseline.** Cycle with **shift+tab**, or switch with **`/mode`**.
28
20
 
29
- Two things follow from the order. First, **deny beats ask, which beats allow**, in every mode. Second, `bypass` sits *after* the deny/ask rules, the external-dir gate, and the circuit breaker - so `--yolo` skips ordinary prompts and the sensitive-write prompt, but not your explicit rules and not the root/home breaker.
21
+ The mode and the flag that turns it on are different names - so both columns:
30
22
 
31
- Rules are enforced by Command Code, not by the model. Instructions in your prompt or `AGENTS.md` shape what the agent *tries* to do, but they don't change what Command Code *allows*. To grant or revoke access, use permission rules, a permission mode, or a `PreToolUse` hook.
23
+ | Mode | How to set it | In one line |
24
+ | :--- | :--- | :--- |
25
+ | `default` | the default; `/mode:default`, shift+tab | Prompt before anything that changes things. Reads are free. |
26
+ | `auto-accept` | `/mode:auto-accept`, shift+tab, `--auto-accept` | Do normal edits (and safe file commands) without asking. |
27
+ | `plan` | `/mode:plan`, shift+tab, `--plan` | Read-only. Explore and plan; only the plan file may be written. |
28
+ | `bypass` | `--yolo` / `--dangerously-skip-permissions` (launch only) | Do basically everything without asking. |
29
+ | `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. |
32
30
 
33
- Every policy denial (a deny rule, a mode gate, a malformed input) carries guidance the model can act on, and the turn continues. Only a **human** answering "no" to a prompt ends the turn.
31
+ **2. Three rule lists tune it.** Order: `deny` wins, then `ask`, then `allow`.
34
32
 
35
- ---
33
+ ```json {{ title: '.commandcode/settings.json' }}
34
+ {
35
+ "permissions": {
36
+ "deny": ["Read(secrets/**)"],
37
+ "ask": ["Shell(git push:*)"],
38
+ "allow": ["Shell(git status:*)", "mcp__github__get_issue"]
39
+ }
40
+ }
41
+ ```
36
42
 
37
- ## Permission modes
43
+ **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`.
38
44
 
39
- A mode sets the baseline behavior. Cycle modes at any time with **shift+tab**, or switch by name with **`/mode`**.
45
+ **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.
40
46
 
41
- | Mode | Meaning |
42
- | :------------ | :------------------------------------------------------------------------------------------------ |
43
- | `default` | Prompt for anything mutating; reads are free. |
44
- | `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. |
45
- | `plan` | Read-only exploration. Reads, searches, and read-only shell (`git status`) run; the only write allowed is the plan file. |
46
- | `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`; use only in throwaway environments. |
47
- | `dont-ask` | Never ask. Only run things already allowed by policy (reads, read-only shell, `allow` rules); **deny** the rest. The mirror image of bypass: for unattended runs where a wrong prompt should fail, not wait. |
47
+ ---
48
48
 
49
- Alternate mode spellings are accepted as aliases: `manual` → `default`, `acceptEdits` → `auto-accept`, `dontAsk` → `dont-ask`, `bypassPermissions` → `bypass`. The legacy name `standard` also maps to `default`.
49
+ ## Common recipes
50
50
 
51
- ### Switching modes
51
+ Drop any of these into `.commandcode/settings.json` (shared, checked in) or `.commandcode/settings.local.json` (personal, gitignored).
52
52
 
53
- Three ways to switch mid-session:
53
+ **Read-only git, ask before pushing, block history rewrites:**
54
54
 
55
- - **shift+tab** cycles `default` → `auto-accept` → `plan` → `default`. When Command Code was launched with `--yolo`, `bypass` joins as a fourth rung after `plan`. (`alt+m` is an equivalent binding for terminals that don't deliver shift+tab.) `dont-ask` is not a rung - it's selected via settings or the CLI; pressing shift+tab from it re-enters the normal cycle at `auto-accept`.
56
- - **`/mode`** shows the current mode; **`/mode:default`**, **`/mode:auto-accept`**, and **`/mode:plan`** (or `/mode <name>`, any accepted spelling) switch directly. Bypass is deliberately **not** switchable from a slash command - slash commands are agent-invokable, so a mid-session route into bypass would let the model disable its own prompts. `/mode:yolo` no longer exists; the command answers "launch with `--yolo` to use it."
57
- - **CLI flags** pick the starting mode:
55
+ ```json {{ title: '.commandcode/settings.json' }}
56
+ {
57
+ "permissions": {
58
+ "allow": ["Shell(git status:*)", "Shell(git log:*)", "Shell(git diff:*)"],
59
+ "ask": ["Shell(git push:*)", "Shell(git pull:*)"],
60
+ "deny": ["Shell(git push --force*)", "Shell(git reset --hard*)", "Shell(git clean -*)"]
61
+ }
62
+ }
63
+ ```
58
64
 
59
- ```bash
60
- cmd --permission-mode auto-accept # default | standard | plan | auto-accept | dont-ask
61
- cmd --auto-accept # shorthand for --permission-mode auto-accept
62
- cmd --plan # start in plan mode
63
- cmd --yolo # bypass - alias for --dangerously-skip-permissions; use with care
65
+ **Gate secret reads** (free by default - reads are never blocked unless a rule says so):
66
+
67
+ ```json {{ title: '.commandcode/settings.json' }}
68
+ {
69
+ "permissions": {
70
+ "ask": ["Read(.env*)"],
71
+ "deny": ["Read(secrets/**)", "Read(~/.ssh/**)"]
72
+ }
73
+ }
64
74
  ```
65
75
 
66
- `--permission-mode` wins over `--plan`. Bypass has no `--permission-mode` value - it is **launch-flag-only** (`--yolo` / `--dangerously-skip-permissions`).
76
+ **Protect lockfiles and the git directory:**
77
+
78
+ ```json {{ title: '.commandcode/settings.json' }}
79
+ {
80
+ "permissions": {
81
+ "ask": ["Edit(package.json)", "Edit(pnpm-lock.yaml)"],
82
+ "deny": ["Edit(.git/**)", "Edit(.ssh/**)"]
83
+ }
84
+ }
85
+ ```
67
86
 
68
- Switching modes also resolves a permission prompt already on screen: entering `auto-accept` or `bypass` **approves** the pending prompt (you just said "stop asking"), and entering `plan` **denies** it. A safety-forced prompt - a destructive command, a sensitive file, an explicit `ask` rule - is never answered by a mode change; it stays on screen for an explicit yes/no.
87
+ **Allow one MCP tool, or a whole server:**
69
88
 
70
- ### `dont-ask` is the CI/CD mode
89
+ ```json {{ title: '.commandcode/settings.json' }}
90
+ {
91
+ "permissions": {
92
+ "allow": ["mcp__github__get_issue", "mcp__linear__*"]
93
+ }
94
+ }
95
+ ```
71
96
 
72
- `dont-ask` is a **fail-closed allowlist**, not an auto-edit mode. If CI should edit files, pre-approve the surface in settings:
97
+ **Run unattended (CI/CD)** - never prompt; pre-approve exactly what CI may touch:
73
98
 
74
- ```json
99
+ ```json {{ title: '.commandcode/settings.json' }}
75
100
  {
76
101
  "permissions": {
77
102
  "defaultMode": "dont-ask",
@@ -80,99 +105,97 @@ Switching modes also resolves a permission prompt already on screen: entering `a
80
105
  }
81
106
  ```
82
107
 
83
- Those edits run without prompting; anything outside the allowlist is denied.
108
+ 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.
84
109
 
85
110
  ---
86
111
 
87
- ## The decision table
112
+ ## Permission modes
88
113
 
89
- The complete matrix - every kind of operation against every mode. This is the authoritative behavior reference; the permission tests pin each row.
114
+ A mode sets the baseline. Cycle with **shift+tab**, or switch with **`/mode`**.
90
115
 
91
- | Operation | `default` | `auto-accept` | `plan` | `bypass` | `dont-ask` | Why |
92
- |---|---|---|---|---|---|---|
93
- | Matches `deny` rule | deny | deny | deny | deny | deny | Deny rules win for permission-gated tools. `ask_user_question`, `agent`, and `run_command` are unconditional exceptions. |
94
- | Matches `ask` rule | ask | ask | ask | ask | deny | Ask rules survive bypass; `dont-ask` cannot prompt. `ask_user_question`, `agent`, and `run_command` are exempt. |
95
- | Matches `allow` rule | allow | allow | allow if plan-compatible | allow | allow | Allow rules are honored after deny/ask and mode gates. |
96
- | Direct file read/search inside workspace | allow | allow | allow | allow | allow | Reads are free unless rules say otherwise. |
97
- | 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 are gated separately, then reads are allowed. |
98
- | Read under the OS temp directory (`/tmp`, `$TMPDIR`) | allow (dir granted silently) | allow (dir granted silently) | allow (dir granted silently) | allow | allow | Temp files are disposable working data - the external-dir escalation is skipped. Deny/ask rules still outrank; a symlink resolving outside temp takes the normal gate. |
99
- | Write under the OS temp directory | ask like a workspace write | allow; sensitive still asks | deny | allow | deny unless allow rule | No external-dir escalation, then the normal mode edit/write rule applies (sensitive names like `/tmp/.env` still force the prompt). |
100
- | Read under a registered skill directory | allow | allow | allow | allow | allow | Skill resources are readable without a grant; writes into skill dirs still gate. |
101
- | Web / read-only native tool | allow | allow | allow | allow | allow | Non-filesystem read-only tools are free unless rules say otherwise. |
102
- | `ask_user_question` | allow | allow | allow | allow | allow | Asking the user is the interaction; a permission prompt must never stack in front of the question. |
103
- | `agent` | allow | allow | allow | allow | allow | Delegation itself is always allowed. Every tool used by the sub-agent is still checked under its own permission policy. |
104
- | `run_command` | allow | allow | - | allow | allow | Dispatches a slash command you named, exactly as if you'd typed it - removed from the toolset in plan mode (a write tool) and never granted to sub-agents. |
105
- | 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 commands qualify when every part is read-only and nothing redirects. Shell cwd follows the external-dir row below. |
106
- | Secret-looking read via a file tool, e.g. `read_file .env` | allow | allow | allow | allow | allow | Reads are free by default; no built-in secret-read prompt for file tools. Use `ask`/`deny` rules for sensitive reads. |
107
- | 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-material argument (`.env`, `id_rsa`, `*.pem`) falls out of the read-only fast path. |
108
- | Workspace file edit | ask | allow | deny | allow | deny unless allow rule | `auto-accept` accepts normal edits; `dont-ask` only runs pre-approved edits. |
109
- | 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 does not. |
110
- | 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 comes first, then the mode-specific edit/write rule. |
111
- | 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. |
112
- | 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, constrained to non-sensitive workspace targets. Recursive `rm` is excluded - see the recursive-delete row. |
113
- | 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 directory is external-dir gated. |
114
- | Shell command args outside workspace | shell rule/mode | shell rule/mode | shell rule/mode | allow | shell rule/mode | Path arguments inside a command string are never external-dir gated (not a reliable file boundary); use shell `ask`/`deny` rules for sensitive commands. |
115
- | Other mutating shell | ask | ask | deny | allow | deny unless allow rule | Shell command arguments are governed by shell rules and the active mode. |
116
- | 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. |
117
- | Root/home removal, e.g. `rm -rf /` or `rm -rf ~` | ask | ask | deny | **ask** | deny | The circuit breaker survives bypass. |
118
- | Recursive workspace delete, e.g. `rm -rf dist` | ask | ask | deny | allow | deny unless allow rule | Recursive deletes prompt outside bypass - accepting edits is not accepting bulk irreversible deletion. A content-specific allow rule (`Shell(rm -rf dist)`) still auto-allows. |
116
+ | Mode | Meaning |
117
+ | :------------ | :------------------------------------------------------------------------------------------------ |
118
+ | `default` | Prompt for anything mutating; reads are free. |
119
+ | `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. |
120
+ | `plan` | Read-only exploration. Reads, searches, and read-only shell (`git status`) run; only the plan file may be written. |
121
+ | `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. |
122
+ | `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. |
123
+
124
+ Aliases: `manual` `default`, `acceptEdits` `auto-accept`, `dontAsk` `dont-ask`, `bypassPermissions` `bypass`. Legacy `standard` also maps to `default`.
119
125
 
120
- Reading down a column: a mode only changes the outcome of calls the rules leave **unresolved**. A `deny` rule and an `ask` rule behave the same in every interactive mode; `dont-ask` turns every would-be ask into a deny; `bypass` auto-allows everything except deny rules, ask rules, and the circuit breaker.
126
+ ### Switching modes
127
+
128
+ Three ways, mid-session:
129
+
130
+ - **shift+tab** cycles `default` → `auto-accept` → `plan` → `default`. 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`.
131
+ - **`/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`."
132
+ - **CLI flags** pick the start mode:
133
+
134
+ ```bash {{ title: 'Terminal' }}
135
+ cmd --permission-mode auto-accept # default | standard | plan | auto-accept | dont-ask
136
+ cmd --auto-accept # shorthand for --permission-mode auto-accept
137
+ cmd --plan # start in plan mode
138
+ cmd --yolo # bypass - alias for --dangerously-skip-permissions; use with care
139
+ ```
140
+
141
+ `--permission-mode` wins over `--plan`. Bypass has no `--permission-mode` value - it's **launch-flag-only** (`--yolo` / `--dangerously-skip-permissions`).
142
+
143
+ 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.
121
144
 
122
145
  ---
123
146
 
124
- ## Rule syntax
147
+ ## Writing rules
125
148
 
126
- Three lists in `permissions` settings, checked in this order: **deny** (always wins) → **ask** (always prompts, even in bypass; denies in dont-ask) → **allow**. Three tools bypass these lists: `ask_user_question`, because its question UI is already the interaction; `agent`, because delegation itself has no side effect; and `run_command`, because it just dispatches a slash command you named, exactly as if you'd typed it. Tools called by a delegated sub-agent remain permission-gated.
149
+ 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.
127
150
 
128
- A rule is a tool name, optionally with a specifier in parentheses:
151
+ A rule is a tool name, optionally with a specifier:
129
152
 
130
- ```
153
+ ```text {{ title: 'Rule shape' }}
131
154
  Tool # the whole tool
132
155
  Tool(specifier) # a specific use
133
156
  ```
134
157
 
135
158
  | Rule | Effect |
136
159
  | :-------------------------------- | :------------------------------------------------- |
137
- | `Shell` | Matches every shell command |
138
- | `Shell(git status)` | Matches `git status` (and `git status --short`) |
139
- | `Shell(npm run *)` | Matches any `npm run …` command |
140
- | `Read(./.env)` | Matches reading `.env` in the project root |
141
- | `Edit(src/**)` | Matches edits anywhere under `src/` |
142
- | `WebFetch(domain:example.com)` | Matches fetches to `example.com` |
143
- | `mcp__github__get_issue` | Matches one tool from the `github` MCP server |
144
- | `mcp__github__*` | Matches every tool from the `github` server |
145
- | `mcp__github__get_*` | Matches every `get_` tool from the `github` server |
146
- | `mcp__*` | Matches every tool from every MCP server |
147
- | `*` | Matches every tool |
148
- | `Shell(run_in_background:true)` | Matches a shell command run in the background |
160
+ | `Shell` | Every shell command |
161
+ | `Shell(git status)` | `git status` (and `git status --short`) |
162
+ | `Shell(npm run *)` | Any `npm run …` command |
163
+ | `Read(./.env)` | Reading `.env` in the project root |
164
+ | `Edit(src/**)` | Edits anywhere under `src/` |
165
+ | `WebFetch(domain:example.com)` | Fetches to `example.com` |
166
+ | `mcp__github__get_issue` | One tool from the `github` MCP server |
167
+ | `mcp__github__*` | Every tool from the `github` server |
168
+ | `mcp__github__get_*` | Every `get_` tool from the `github` server |
169
+ | `mcp__*` | Every tool from every MCP server |
170
+ | `*` | Every tool |
171
+ | `Shell(run_in_background:true)` | A shell command run in the background |
149
172
 
150
173
  Tool names are case-insensitive. `Tool()` and `Tool(*)` both mean the whole tool.
151
174
 
152
175
  ### Wildcards
153
176
 
154
- A `*` in a **tool name** (not a specifier) matches by name, so you can write broad rules without listing every tool:
177
+ A `*` in a **tool name** (not a specifier) matches by name:
155
178
 
156
- - `*` - every permission-gated tool (`ask_user_question`, `agent`, and `run_command` remain allowed).
179
+ - `*` - every permission-gated tool (`ask_user_question`, `agent`, `run_command` stay allowed).
157
180
  - `mcp__*` - every tool from every MCP server.
158
181
  - `mcp__github__get_*` - every read-style tool from one server.
159
182
  - `edit_*` - every tool whose name starts with `edit_`.
160
183
 
161
- Tool-name wildcards are honored in `deny` and `ask` lists, where a broad rule is what you want (`deny: ["mcp__*"]` blocks all MCP tools). They are **ignored in `allow` lists** 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 in every list.
184
+ 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.
162
185
 
163
186
  ### Match by input parameter
164
187
 
165
- Beyond a tool's own specifier, a `deny` or `ask` rule can gate on a single **top-level input parameter** with `Tool(param:value)`:
188
+ A `deny` or `ask` rule can gate on one **top-level input parameter** with `Tool(param:value)`:
166
189
 
167
190
  - `Shell(run_in_background:true)` - a background shell command.
168
191
 
169
- The value takes a `*` wildcard, and a parameter the model **omits** never matches. Each rule names one parameter - to gate on two, write two rules.
192
+ The value takes a `*` wildcard; a parameter the model **omits** never matches. One parameter per rule - to gate two, write two rules.
170
193
 
171
- Parameter matching is **deny/ask only**. An allow rule for one parameter can't establish that the whole call is safe, so `allow` rules keep to each tool's own specifier. The fields a tool already matches with its own syntax - `command` (Shell), `file_path` (Read/Edit/Write), `path` (Grep/Glob), `notebook_path`, and `url` (WebFetch) - are **not** parameter-matchable; use the tool's specifier for those (`Shell(rm *)`, not `Shell(command:rm *)`).
194
+ 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 *)`).
172
195
 
173
196
  ### Tool names
174
197
 
175
- Use the friendly capitalized names, or the exact tool names the model sees:
198
+ Use the friendly names, or the exact tool names the model sees:
176
199
 
177
200
  | Friendly | Covers |
178
201
  | :----------- | :----------------------------------------------------------- |
@@ -183,31 +206,24 @@ Use the friendly capitalized names, or the exact tool names the model sees:
183
206
  | `WebFetch` | `web_fetch` |
184
207
  | `WebSearch` | `web_search` |
185
208
 
186
- `Shell` is the name for shell commands, because the shell tool is called `shell_command`. `Bash(...)` is accepted as a legacy alias for `Shell(...)`, so older configurations keep working - but `Shell` is the name to use.
209
+ `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`.
187
210
 
188
211
  ### Shell patterns
189
212
 
190
- Shell specifiers support `*` wildcards:
213
+ Shell specifiers take `*` wildcards:
191
214
 
192
- - `Shell(npm run build)` - the exact command.
215
+ - `Shell(npm run build)` - exact command.
193
216
  - `Shell(npm run *)` - any command starting with `npm run `.
194
217
  - `Shell(git * main)` - `git checkout main`, `git merge main`, …
195
- - `Shell(git:*)` - the `:*` suffix is a trailing wildcard, the same as `Shell(git *)`.
196
-
197
- The space before `*` matters: `Shell(ls *)` matches `ls -la` but not `lsof`; `Shell(ls*)` matches both.
198
-
199
- Command Code understands shell operators, so a rule like `Shell(git status)` does **not** grant `git status && rm -rf .`. Every subcommand of a compound command (`&&`, `||`, `;`, `|`, `&`, newlines) must match on its own.
218
+ - `Shell(git:*)` - the `:*` suffix is a trailing wildcard, same as `Shell(git *)`.
200
219
 
201
- ### Asymmetric matching: deny/ask are aggressive, allow is conservative
220
+ The space matters: `Shell(ls *)` matches `ls -la` but not `lsof`; `Shell(ls*)` matches both.
202
221
 
203
- The matcher deliberately treats the lists differently - failing toward a prompt is always the safe direction:
204
-
205
- - **Deny and ask rules match aggressively:** They see through leading environment variables (`FOO=bar rm -rf x` matches `Shell(rm *)`), process wrappers (`timeout 30 …`, `nice …`), and every subcommand of a compound command; a command the parser can't fully understand is matched against the raw string; path matching folds case (on macOS/Windows `.ENV` opens `.env`) and fires when either the given path or its symlink target matches.
206
- - **Allow rules match conservatively:** No env stripping (`PATH=/tmp git status` does **not** match an allow for `Shell(git status:*)`); a compound command allows only when *every* subcommand matches some allow pattern; an unparseable command never auto-allows; path matching is case-exact and requires **both** the given path and its symlink target to match.
222
+ 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.
207
223
 
208
224
  ### File-path patterns
209
225
 
210
- `Read` and `Edit` patterns follow gitignore-style matching with four anchors:
226
+ `Read` and `Edit` use gitignore-style matching with four anchors:
211
227
 
212
228
  | Pattern | Anchored at | Example |
213
229
  | :------------- | :----------------------------------- | :----------------------------- |
@@ -216,83 +232,151 @@ The matcher deliberately treats the lists differently - failing toward a prompt
216
232
  | `/path` | Project root | `Edit(/src/**)` |
217
233
  | `path` | Any depth under the project | `Read(.env)` ≡ `Read(**/.env)` |
218
234
 
219
- `*` matches within one path segment; `**` matches across directories. When a rule points at a symlink, both the link and its target are checked. A trailing slash on the tool's input path can't dodge an exact-path rule (`read_directory /repo/secrets/` still matches `Read(//repo/secrets)`).
235
+ `*` 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)`).
236
+
237
+ 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](#known-limits)).
238
+
239
+ ### Aggressive deny, conservative allow
220
240
 
221
- Reads are allowed by default. `allow: ["Read(src/**)"]` is accepted but does **not** block reads outside `src/**` - use `ask`/`deny` rules for sensitive reads. A strict read allowlist would be a separate behavior change (see Known limits).
241
+ The lists are matched differently on purpose - failing toward a prompt is the safe direction:
242
+
243
+ - **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.
244
+ - **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.
222
245
 
223
246
  ### MCP and sub-agents
224
247
 
225
- MCP tools are named by their canonical `mcp__server__tool` name - display names are never permission keys. `mcp__github` and `mcp__github__*` both match every tool from the `github` server; `mcp__github__get_*` matches just its `get_` tools; and `mcp__*` (deny/ask only) matches every tool from every server.
248
+ 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.
226
249
 
227
- The `agent` tool itself is always allowed and never displays a permission prompt. A sub-agent runs under its own permission policy - 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**, because a sub-agent runs autonomously and has no human in its loop to answer. The hard guards still hold: a sub-agent can never run a tool your `deny` rules block, never write in plan mode, and anything the main loop would have prompted about for **safety** - a destructive command, a sensitive file, an outside-workspace write, an explicit `ask` rule - fails **closed** (denied) instead of sailing through.
250
+ 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).
228
251
 
229
252
  ---
230
253
 
231
- ## Prompt choices
254
+ ## What happens at a prompt
232
255
 
233
- Every permission prompt offers exactly **three** choices: allow once, allow **and remember**, or deny with feedback. The remembered scope matches the subject:
256
+ Every prompt offers three choices: allow once, allow **and remember**, or deny with feedback. The remembered scope matches the subject:
234
257
 
235
- - **File operations** - *"Yes, allow all edits this session"* turns on auto-accept for the rest of the session (same as shift+tab).
236
- - **Shell commands** - *"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 is written per non-read-only base. Bases that execute arbitrary code (`bash`, `python`, `curl`, …) persist the exact command instead of a prefix grant.
237
- - **MCP / other tools** - *"Yes, don't ask again for `mcp__server__tool` in this project"* persists the tool name to project settings.
258
+ - **File ops** - *"Yes, allow all edits this session"* turns on auto-accept for the session (same as shift+tab).
259
+ - **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.
260
+ - **MCP / other** - *"Yes, don't ask again for `mcp__server__tool` in this project"* persists the tool name to project settings.
238
261
 
239
262
  ### Command explanations on demand
240
263
 
241
- The shell permission prompt can explain the command it's asking about: press **ctrl+e** (**ctrl+y** in VS Code-family terminals, where the IDE intercepts ctrl+e) and a background model call summarizes what the command does, inline in the prompt. This is governed by the `on-demand-tool-descriptions` setting (**on by default**): explanations are generated only when you ask for one. Turn the setting off (via `/config`) to generate every explanation upfront instead.
264
+ 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.
242
265
 
243
266
  ---
244
267
 
245
268
  ## Safety behaviors
246
269
 
247
- Safety-forced prompts only offer one-time **Yes** or **No** choices, plus a banner saying which safety check fired. Session and project choices are intentionally hidden because destructive-command, sensitive-path, outside-workspace, and explicit `ask` approvals are never cached - the next risky call must ask again. To stop a recurring safety prompt, add a content-specific allow rule (e.g. `Edit(.commandcode/settings.json)`).
270
+ 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)`).
248
271
 
249
272
  ### The root/home removal circuit breaker
250
273
 
251
- The **only** built-in safety prompt that survives `bypass`: a removal targeting the filesystem root or your home directory - `rm -rf /`, `rm -rf ~`, `rm -rf $HOME` - always stops for confirmation, in every mode that can ask (`dont-ask` and `plan` deny instead, since they can't prompt).
274
+ 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).
252
275
 
253
- The breaker is spoof-resistant. It detects removals through environment-variable 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).
276
+ 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).
254
277
 
255
278
  ### Recursive deletes prompt under auto-accept
256
279
 
257
- The `auto-accept` safe-filesystem fast path refuses `rm -r/-R/-rf/--recursive` (and `find … -delete`): a recursive delete like `rm -rf dist` prompts exactly as it would in `default`. Plain single-file `rm` stays fast-pathed. This is deliberate, and in the safe direction: accepting edits is not accepting bulk irreversible deletion. A content-specific allow rule (`Shell(rm -rf dist)`) still auto-allows, and `bypass` runs it without a prompt.
280
+ 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.
258
281
 
259
282
  ### Sensitive writes
260
283
 
261
- Writing these prompts in `default` and `auto-accept` unless a content-specific allow rule opts in (`bypass` skips this gate):
284
+ Writing any of these prompts in `default` and `auto-accept` unless a content-specific allow rule opts in (`bypass` skips this gate):
262
285
 
263
- ```
286
+ ```text {{ title: 'Sensitive paths (prompt on write)' }}
264
287
  Secret material: .env, .env.*, *.pem, *.key, id_rsa, id_ed25519, credentials
265
288
  Persistence vectors: .bashrc, .zshrc, .profile, .gitconfig, .gitmodules, .mcp.json, …
266
289
  Control surfaces: .git/**, .ssh/**, .aws/**, .gnupg/**, .kube/**, .vscode/**, .idea/**,
267
290
  .husky/**, .devcontainer/**, node_modules/.bin/**, .commandcode settings
268
291
  ```
269
292
 
270
- A "content-specific" allow rule must carry a specifier that names the path (`Edit(.env.local)`) - a bare whole-tool allow (`Edit`) is deliberately not enough to wave a call past a safety check.
293
+ 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.
271
294
 
272
- Sensitive **reads** through file tools do not prompt. Reading `.env` is allowed by default. If you want it gated, say so 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 of it and prompts like any other command.
295
+ 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.
273
296
 
274
297
  ### External directories
275
298
 
276
- Outside-workspace reads, writes, and shell working directories first ask to admit the directory into the session's root set - the same grant as `/add-dir` or `permissions.additionalDirectories` - and then the normal mode rule applies. Details:
299
+ 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.
277
300
 
278
- - `bypass` grants the directory silently; `dont-ask` denies unless the directory is pre-approved in settings.
279
- - The **OS temp directory** (`/tmp`, `$TMPDIR`, `os.tmpdir()`) is granted silently in every mode - temp files are disposable working data, so asking to "extend access" to the machine's own temp dir is friction, not protection. Reads there run prompt-free; writes skip the outside-workspace escalation but still confirm like any workspace write (and sensitive names like `/tmp/.env` still force the sensitive-file prompt). Explicit `deny`/`ask` rules still outrank the grant, and a symlink under the temp dir that resolves outside it takes the normal gate.
301
+ - `bypass` grants the directory silently; `dont-ask` denies unless it's pre-approved in settings.
302
+ - 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.
280
303
  - Approving an `ask` rule on an outside path **also grants the directory**, so the tool-level backstop never rejects a call you just approved.
281
- - **Registered skill directories** are readable without a grant (skill resources live outside the workspace by design). Writes into skill directories still gate.
282
- - **Plan-file writes are exempt in plan mode** - they are the mode's sanctioned output.
283
- - Absolute paths **inside a shell command string** are *not* external-dir gated - a command argument is not a reliable file boundary. Use shell `ask`/`deny` rules for sensitive commands.
304
+ - **Registered skill directories** are readable without a grant (skill resources live outside the workspace by design). Writes into them still gate.
305
+ - **Plan-file writes are exempt in plan mode** - the mode's sanctioned output.
306
+ - 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.
284
307
 
285
308
  ### Disabling bypass
286
309
 
287
- Set `permissions.disableBypass` to `"disable"` (or `true`) to make bypass mode unenterable. This is enforced at every layer: the engine evaluates a requested `bypass` as `default`, and the CLI neutralizes `--dangerously-skip-permissions` (and its `--yolo` alias) at entry, so the TUI mode cycle and stored auto-approve decisions can never silently answer prompts as if bypass were live. Most useful in the user-global settings file a project can't edit.
310
+ 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.
288
311
 
289
312
  ---
290
313
 
291
- ## Settings
314
+ ## How a decision is made
315
+
316
+ The whole machine. Every tool call runs a fixed order - the order **is** the spec, and tests pin it:
317
+
318
+ 1. **Deny rules** - blocked in every mode, including bypass.
319
+ 2. **Ask rules** - always prompt, even under bypass. In `dont-ask` the would-be prompt becomes a deny.
320
+ 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.
321
+ 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/`.
322
+ 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.
323
+ 6. **Malformed writes** - a write/edit with no resolvable file path is denied up front with a correction.
324
+ 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.
325
+ 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`).
326
+ 9. **Bypass** - everything past the gates above auto-allows.
327
+ 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.
328
+ 11. **Allow rules** - pre-approved calls run without a prompt.
329
+ 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.
330
+ 13. Otherwise, **ask** - and in `dont-ask`, **deny**.
331
+
332
+ 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.
292
333
 
293
- Permission rules live under `permissions` in `.commandcode/settings.json` (shared, checked in) or `.commandcode/settings.local.json` (personal, gitignored). Interactive project approvals write to `settings.json` in a normal run and to `settings.local.json` when Command Code starts with `--local`:
334
+ 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.
335
+
336
+ ---
294
337
 
295
- ```json
338
+ ## The decision table
339
+
340
+ The full matrix - every operation against every mode. Authoritative; the tests pin each row.
341
+
342
+ | Operation | `default` | `auto-accept` | `plan` | `bypass` | `dont-ask` | Why |
343
+ |---|---|---|---|---|---|---|
344
+ | Matches `deny` rule | deny | deny | deny | deny | deny | Deny rules win. `ask_user_question`, `agent`, `run_command` are unconditional exceptions. |
345
+ | Matches `ask` rule | ask | ask | ask | ask | deny | Ask survives bypass; `dont-ask` can't prompt. Same three tools exempt. |
346
+ | Matches `allow` rule | allow | allow | allow if plan-compatible | allow | allow | Honored after deny/ask and mode gates. |
347
+ | Direct file read/search inside workspace | allow | allow | allow | allow | allow | Reads are free unless rules say otherwise. |
348
+ | 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. |
349
+ | 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. |
350
+ | 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). |
351
+ | Read under a registered skill dir | allow | allow | allow | allow | allow | Skill resources readable without a grant; writes into skill dirs still gate. |
352
+ | Web / read-only native tool | allow | allow | allow | allow | allow | Non-filesystem read-only tools are free unless rules say otherwise. |
353
+ | `ask_user_question` | allow | allow | allow | allow | allow | Asking the user is the interaction; no prompt may stack in front of it. |
354
+ | `agent` | allow | allow | allow | allow | allow | Delegation is always allowed. Every sub-agent tool is still checked. |
355
+ | `run_command` | allow | allow | - | allow | allow | Dispatches a slash command you named - removed in plan mode (a write tool), never granted to sub-agents. |
356
+ | 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. |
357
+ | 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`. |
358
+ | 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. |
359
+ | Workspace file edit | ask | allow | deny | allow | deny unless allow rule | `auto-accept` accepts normal edits; `dont-ask` runs only pre-approved edits. |
360
+ | 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. |
361
+ | 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. |
362
+ | 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. |
363
+ | 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. |
364
+ | 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. |
365
+ | 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. |
366
+ | Other mutating shell | ask | ask | deny | allow | deny unless allow rule | Governed by shell rules and the active mode. |
367
+ | 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. |
368
+ | Root/home removal, e.g. `rm -rf /` or `rm -rf ~` | ask | ask | deny | **ask** | deny | The circuit breaker survives bypass. |
369
+ | 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. |
370
+
371
+ 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.
372
+
373
+ ---
374
+
375
+ ## Settings reference
376
+
377
+ 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`:
378
+
379
+ ```json {{ title: '.commandcode/settings.json' }}
296
380
  {
297
381
  "permissions": {
298
382
  "defaultMode": "default",
@@ -317,91 +401,43 @@ Permission rules live under `permissions` in `.commandcode/settings.json` (share
317
401
  }
318
402
  ```
319
403
 
320
- - **`defaultMode`** - the starting mode (`default`, `auto-accept`, `plan`, `bypass`, or `dont-ask`; alternate spellings like `acceptEdits` and the legacy `standard` also work).
321
- - **`allow` / `ask` / `deny`** - rule lists. These **accumulate across settings files**: a `deny` rule in your user-global settings can't be erased by a project file.
322
- - **`additionalDirectories`** - extra directories that count as inside the workspace for reads and writes (same accumulation).
323
- - **`disableBypass`** - set to `"disable"` (or `true`) to make bypass mode unenterable.
404
+ - **`defaultMode`** - the start mode (`default`, `auto-accept`, `plan`, `bypass`, `dont-ask`; alternate spellings like `acceptEdits` and legacy `standard` work too).
405
+ - **`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.
406
+ - **`additionalDirectories`** - extra dirs that count as inside the workspace for reads and writes (same accumulation).
407
+ - **`disableBypass`** - `"disable"` (or `true`) makes bypass unenterable.
324
408
 
325
409
  ### Precedence
326
410
 
327
- If a tool is denied at any level, no other level can allow it. A managed or user-global `deny` beats a project `allow`; deny beats ask; ask beats allow. Specificity doesn't change the order - a broad `deny` still blocks a narrower `allow`.
328
-
329
- ---
330
-
331
- ## Examples
332
-
333
- **Read-only git, ask before pushing, block history rewrites:**
334
-
335
- ```json
336
- {
337
- "permissions": {
338
- "allow": ["Shell(git status:*)", "Shell(git log:*)", "Shell(git diff:*)"],
339
- "ask": ["Shell(git push:*)", "Shell(git pull:*)"],
340
- "deny": ["Shell(git push --force*)", "Shell(git reset --hard*)", "Shell(git clean -*)"]
341
- }
342
- }
343
- ```
344
-
345
- **Gate secret reads (they're free by default):**
346
-
347
- ```json
348
- {
349
- "permissions": {
350
- "ask": ["Read(.env*)"],
351
- "deny": ["Read(secrets/**)", "Read(~/.ssh/**)"]
352
- }
353
- }
354
- ```
355
-
356
- **Protect lockfiles and the git directory:**
357
-
358
- ```json
359
- {
360
- "permissions": {
361
- "ask": ["Edit(package.json)", "Edit(pnpm-lock.yaml)"],
362
- "deny": ["Edit(.git/**)", "Edit(.ssh/**)"]
363
- }
364
- }
365
- ```
366
-
367
- **Allow one MCP tool, or a whole server:**
368
-
369
- ```json
370
- {
371
- "permissions": {
372
- "allow": ["mcp__github__get_issue", "mcp__linear__*"]
373
- }
374
- }
375
- ```
411
+ 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`.
376
412
 
377
413
  ---
378
414
 
379
415
  ## Design decisions
380
416
 
381
- The choices behind the behavior above, so you don't have to reverse-engineer them:
417
+ The reasoning behind the behavior, so you don't reverse-engineer it:
382
418
 
383
- 1. **Rules over a hardcoded safety matrix.** There is no built-in `.env` file-read prompt and no bypass-surviving recursive-delete prompt. Sensitive file reads are a rules concern (`ask`/`deny`), not a hardcoded prompt, so each project decides what counts as sensitive. The one stricter carve-out: the read-only **shell** fast path screens secret-material arguments, so `cat .env` prompts instead of riding `cat`'s free pass.
419
+ 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.
384
420
  2. **One ordered pipeline.** The order in [How a decision is made](#how-a-decision-is-made) is the spec; tests pin it.
385
- 3. **Asymmetric rule matching.** Deny/ask match aggressively; allow matches conservatively. Failing toward a prompt is always the safe direction.
386
- 4. **External directories are a separate gate**, applied before the mode rule, with skill read roots and plan-file writes exempted where the mode sanctions them.
421
+ 3. **Asymmetric matching.** Deny/ask aggressive, allow conservative. Failing toward a prompt is the safe direction.
422
+ 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.
387
423
  5. **The circuit breaker survives bypass and is spoof-resistant.**
388
- 6. **`disableBypass` is enforced at every layer** - engine, CLI entry, TUI mode cycle, and decision store.
389
- 7. **Shell command arguments are not a file boundary.** Absolute paths inside a command string are governed by shell rules and the active mode, not the file-tool boundary.
390
- 8. **Generic tools get a real prompt.** MCP/custom tools render a generic permission prompt instead of a fake file-edit prompt.
391
- 9. **Recursive deletes prompt under auto-accept.** A deliberate choice in the safe direction: bulk irreversible deletion is not an edit.
392
- 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.
424
+ 6. **`disableBypass` is enforced at every layer** - engine, CLI entry, TUI mode cycle, decision store.
425
+ 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.
426
+ 8. **Generic tools get a real prompt.** MCP/custom tools render a generic permission prompt, not a fake file-edit prompt.
427
+ 9. **Recursive deletes prompt under auto-accept.** Safe direction: bulk irreversible deletion isn't an edit.
428
+ 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.
393
429
 
394
430
  ---
395
431
 
396
432
  ## Known limits
397
433
 
398
- Documented edges of the current engine - behaviors to know about when writing rules:
434
+ Documented edges of the current engine:
399
435
 
400
- - **Wildcard `include` patterns in `read_multiple_files` expand inside the tool**, so an exact-file deny rule (`Read(.env)`) cannot 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.
401
- - **No strict read allowlist:** `allow: ["Read(src/**)"]` does not deny reads outside `src/**` - reads are allowed by default and only `ask`/`deny` rules restrict them. A deny-everything-not-listed read mode is intentionally out of scope as a separate behavior change.
402
- - **Shell path arguments are not external-dir gated:** `cat /outside/file` is governed by shell rules and the mode, not the workspace boundary - a command string is not a reliable file boundary. Gate it with `deny: ["Shell(cat /outside/*)"]`-style rules if you need to.
403
- - **An allow rule never matches an opaque command:** If the shell parser can't fully understand a command, it can still be deny/ask-matched against the raw string, but it will never auto-allow - it prompts instead.
404
- - **Parameter rules only see parameters the model sends:** An omitted parameter never matches.
436
+ - **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.
437
+ - **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.
438
+ - **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.
439
+ - **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.
440
+ - **Parameter rules only see parameters the model sends:** an omitted parameter never matches.
405
441
 
406
442
  ---
407
443
 
@@ -409,4 +445,4 @@ Documented edges of the current engine - behaviors to know about when writing ru
409
445
 
410
446
  - [Plan Mode](./plan-mode.md) - read-only exploration and planning.
411
447
  - [Hooks](./hooks.md) - run your own scripts to allow, deny, or audit tool calls at runtime.
412
- - [MCP](./mcp.md) - connect external tools that the permission engine gates by server and tool name.
448
+ - [MCP](./mcp.md) - connect external tools the engine gates by server and tool name.