@gotgenes/pi-permission-system 18.0.2 → 18.1.1

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.
@@ -0,0 +1,201 @@
1
+ # Permission Frontmatter for Subagent Extensions
2
+
3
+ A convention guide for pi-subagent extension authors who want to offer users richer per-agent permission control.
4
+
5
+ ## Motivation
6
+
7
+ Pi subagent extensions already let users restrict **which tools** an agent can see via frontmatter keys like `tools:`, `disallowed_tools:`, or `deny-tools:`.
8
+ These binary allow/deny mechanisms are simple and effective for tool visibility.
9
+
10
+ The `pi-permission-system` extension adds a second layer: **policy evaluation** with three states — `allow`, `ask`, and `deny` — across multiple permission surfaces (tools, bash commands, MCP operations, skills, external directories, and special operations).
11
+
12
+ By documenting the `permission:` frontmatter key in your extension, you give users a single agent file that expresses both visibility restrictions (your extension) and runtime policy (the permission system) without any code coupling between the two extensions.
13
+
14
+ ## The Two-Layer Model
15
+
16
+ ```text
17
+ ┌──────────────────────────────────────────────────────┐
18
+ │ Layer 1 – Visibility (your extension) │
19
+ │ Controls which tools are registered / active │
20
+ │ before the agent session starts. │
21
+ ├──────────────────────────────────────────────────────┤
22
+ │ Layer 2 – Policy (pi-permission-system) │
23
+ │ Controls allow / ask / deny decisions on every │
24
+ │ tool call, bash command, MCP operation, etc. │
25
+ └──────────────────────────────────────────────────────┘
26
+ ```
27
+
28
+ The two layers compose additively:
29
+
30
+ 1. A tool hidden by your extension is never seen by the permission system — policy for it is irrelevant.
31
+ 2. A tool denied by the permission system is removed from the active set before the agent starts — your extension's allowlist cannot restore it.
32
+ 3. Both denylist mechanisms are additive.
33
+ A tool blocked by either layer stays blocked.
34
+
35
+ ## The `permission:` Frontmatter Format
36
+
37
+ The `permission:` key uses a flat policy map.
38
+ Each top-level key is either a tool name (for per-tool policy) or a named surface (`bash`, `mcp`, `skill`, `external_directory`, `special`).
39
+ The special key `"*"` is the universal fallback.
40
+
41
+ ### Minimal example
42
+
43
+ ```yaml
44
+ ---
45
+ permission:
46
+ "*": ask
47
+ read: allow
48
+ write: deny
49
+ ---
50
+ ```
51
+
52
+ This means: allow all read operations without prompting, deny all write operations, and ask the user for everything else.
53
+
54
+ ### Full example with bash patterns
55
+
56
+ ```yaml
57
+ ---
58
+ permission:
59
+ "*": ask
60
+ read: allow
61
+ bash:
62
+ "*": ask
63
+ "git status": allow
64
+ "git diff *": allow
65
+ "npm test": allow
66
+ mcp:
67
+ "*": deny
68
+ skill:
69
+ "*": ask
70
+ external_directory:
71
+ "*": deny
72
+ "~/projects/*": allow
73
+ ---
74
+ ```
75
+
76
+ ### Composing with your extension's keys
77
+
78
+ Users can freely combine `permission:` with your extension's tool restriction key:
79
+
80
+ ```yaml
81
+ ---
82
+ # nicobailon/pi-subagents: restrict visible tools
83
+ tools: bash,read_file,write_file
84
+
85
+ # pi-permission-system: policy within the visible set
86
+ permission:
87
+ "*": ask
88
+ read_file: allow
89
+ bash:
90
+ "*": ask
91
+ "git *": allow
92
+ ---
93
+ ```
94
+
95
+ Both keys are read independently by their respective extensions.
96
+ There is no key collision — `permission:` is exclusively consumed by `pi-permission-system`.
97
+
98
+ ## Permission Surfaces
99
+
100
+ | Surface | Key | Value format | Description |
101
+ | -------------------- | -------------------- | ---------------------------- | ------------------------------------------ |
102
+ | Tools | `<tool_name>` | `"allow" \| "ask" \| "deny"` | Per-tool invocation policy |
103
+ | Bash | `bash` | `{ pattern: decision }` | Pattern-matched bash commands (glob-style) |
104
+ | MCP | `mcp` | `{ pattern: decision }` | MCP tool-level policy |
105
+ | Skills | `skill` | `{ pattern: decision }` | Skill invocation policy |
106
+ | External directories | `external_directory` | `{ pattern: decision }` | Path-based access outside the project |
107
+ | Special | `special` | `{ pattern: decision }` | Special operations (e.g. `subagent_spawn`) |
108
+ | Universal fallback | `"*"` | `"allow" \| "ask" \| "deny"` | Applies when no specific rule matches |
109
+
110
+ Pattern maps use last-match-wins ordering: put broad catch-alls first and specific overrides after.
111
+
112
+ ## What Adoption Looks Like
113
+
114
+ Adopting this convention does **not** require your extension to:
115
+
116
+ - Import or depend on `pi-permission-system`
117
+ - Evaluate the `permission:` key at runtime
118
+ - Change your existing tool restriction mechanism
119
+
120
+ Adoption means:
121
+
122
+ 1. **Document** the `permission:` key as an optional frontmatter field in your extension's README or agent authoring guide.
123
+ 2. **Explain** that it is consumed by `pi-permission-system` when both extensions are installed.
124
+ 3. **Show** a combined example with your extension's key alongside `permission:`.
125
+
126
+ The permission system handles all evaluation, prompt dialogs, and policy enforcement independently.
127
+
128
+ ## Runtime Integration (Optional)
129
+
130
+ If your extension runs subagents in-process (e.g. via `createAgentSession()`), you can optionally query the permission system's policy at runtime via the event bus API — no package import needed.
131
+
132
+ ### Querying policy
133
+
134
+ ```typescript
135
+ const requestId = crypto.randomUUID();
136
+
137
+ pi.events.on(
138
+ `permissions:rpc:check:reply:${requestId}`,
139
+ (raw) => {
140
+ const reply = raw as { success: boolean; data?: { result: string } };
141
+ if (reply.success) {
142
+ console.log(reply.data?.result); // "allow" | "deny" | "ask"
143
+ }
144
+ },
145
+ );
146
+
147
+ pi.events.emit("permissions:rpc:check", {
148
+ requestId,
149
+ surface: "bash",
150
+ value: "git push",
151
+ agentName: "Worker",
152
+ });
153
+ ```
154
+
155
+ ### Forwarding permission prompts
156
+
157
+ When a child agent encounters an `ask` permission and has no UI, the prompt can be forwarded to the parent session:
158
+
159
+ ```typescript
160
+ const requestId = crypto.randomUUID();
161
+
162
+ pi.events.on(
163
+ `permissions:rpc:prompt:reply:${requestId}`,
164
+ (raw) => {
165
+ const reply = raw as { success: boolean; data?: { approved: boolean } };
166
+ if (reply.success && reply.data?.approved) {
167
+ // proceed
168
+ } else {
169
+ // deny
170
+ }
171
+ },
172
+ );
173
+
174
+ pi.events.emit("permissions:rpc:prompt", {
175
+ requestId,
176
+ surface: "bash",
177
+ value: "rm -rf /tmp/build",
178
+ message: "Allow rm -rf /tmp/build?",
179
+ agentName: "Worker",
180
+ });
181
+ ```
182
+
183
+ If `pi-permission-system` is not installed, no reply arrives.
184
+ Implement a timeout and treat no-reply as `deny` for graceful degradation.
185
+
186
+ For full API documentation, see [Cross-extension API](../cross-extension-api.md).
187
+
188
+ ## Benefits for Your Users
189
+
190
+ 1. **Richer semantics** — `ask` is more useful than binary allow/deny; users can permit a tool but require approval for each invocation.
191
+ 2. **Unified config** — one `permission:` block per agent instead of separate restriction keys in multiple extensions.
192
+ 3. **Surface coverage** — policy covers bash patterns, MCP tools, skills, external directories, and special operations, not just tool names.
193
+ 4. **Forwarding** — permission prompts from headless child agents surface in the parent session's UI.
194
+ 5. **Programmatic access** — the event bus API lets your extension query policy at runtime without importing the package.
195
+
196
+ ## Further Reading
197
+
198
+ - [Subagent Integration](../subagent-integration.md) — full coexistence documentation and interaction rules
199
+ - [Cross-extension API](../cross-extension-api.md) — service accessor, event bus reference (decision broadcasts, RPC check, RPC prompt)
200
+ - [Configuration](../configuration.md) — full policy reference including merge precedence
201
+ - [Schema](../../schemas/permissions.schema.json) — canonical JSON Schema for the flat permission format
@@ -0,0 +1,113 @@
1
+ # Upstream Issue Template
2
+
3
+ Template text for proposing the `permission:` frontmatter convention to subagent extension repositories.
4
+ Customize the placeholders (`{{...}}`) for each target repo.
5
+
6
+ ---
7
+
8
+ ## Issue Title
9
+
10
+ > Proposal: document `permission:` frontmatter for per-agent permission policy
11
+
12
+ ## Issue Body
13
+
14
+ ````markdown
15
+ ## Context
16
+
17
+ Users of both `{{your-extension}}` and [`pi-permission-system`](https://github.com/gotgenes/pi-permission-system) currently configure tool restrictions in two places:
18
+
19
+ 1. **Tool visibility** via `{{your-key}}` in agent frontmatter (consumed by your extension)
20
+ 2. **Permission policy** via `permission:` in the same frontmatter (consumed by pi-permission-system)
21
+
22
+ These two layers compose correctly today — there is no conflict — but users may not realize they can combine them in the same agent file.
23
+
24
+ ## Proposal
25
+
26
+ Document the `permission:` key as an optional companion to `{{your-key}}` in your agent authoring docs.
27
+ This is purely a documentation change — no code dependency on pi-permission-system is needed.
28
+
29
+ ### What `permission:` provides
30
+
31
+ - **Three-state policy**: `allow`, `ask` (prompt the user), or `deny` — richer than binary allow/deny
32
+ - **Multiple surfaces**: tools, bash commands (glob patterns), MCP operations, skills, external directories
33
+ - **Prompt forwarding**: `ask` decisions in headless child agents surface in the parent session's UI
34
+ - **Event bus API**: other extensions can query policy at runtime without importing the package
35
+
36
+ ### Example
37
+
38
+ ```yaml
39
+ ---
40
+ # {{your-extension}}: restrict visible tools
41
+ {{your-key}}: {{example-value}}
42
+
43
+ # pi-permission-system (optional): policy within the visible set
44
+ permission:
45
+ "*": ask
46
+ read_file: allow
47
+ bash:
48
+ "*": ask
49
+ "git *": allow
50
+ ---
51
+ ```
52
+
53
+ ### Two-layer model
54
+
55
+ ```text
56
+ Layer 1 – Visibility ({{your-extension}})
57
+ → Controls which tools are registered before the session starts
58
+
59
+ Layer 2 – Policy (pi-permission-system)
60
+ → Controls allow/ask/deny decisions on every tool call, bash command, etc.
61
+ ```
62
+
63
+ A tool hidden by Layer 1 is never evaluated by Layer 2.
64
+ A tool denied by Layer 2 cannot be restored by Layer 1.
65
+ Both mechanisms are additive — a tool blocked by either stays blocked.
66
+
67
+ ### What adoption requires from you
68
+
69
+ 1. Add a section to your README noting that `permission:` is an optional key consumed by pi-permission-system
70
+ 2. Show a combined example with `{{your-key}}` + `permission:`
71
+ 3. Link to the [convention guide](https://github.com/gotgenes/pi-permission-system/blob/main/docs/guides/permission-frontmatter-for-subagent-extensions.md) for full details
72
+
73
+ No code changes, no new dependency, no schema enforcement.
74
+
75
+ ## References
76
+
77
+ - [Convention guide](https://github.com/gotgenes/pi-permission-system/blob/main/docs/guides/permission-frontmatter-for-subagent-extensions.md)
78
+ - [Subagent integration docs](https://github.com/gotgenes/pi-permission-system/blob/main/docs/subagent-integration.md)
79
+ - [Cross-extension API docs](https://github.com/gotgenes/pi-permission-system/blob/main/docs/cross-extension-api.md)
80
+ ````
81
+
82
+ ---
83
+
84
+ ## Per-Repository Customization
85
+
86
+ ### nicobailon/pi-subagents
87
+
88
+ | Placeholder | Value |
89
+ | -------------------- | --------------------------- |
90
+ | `{{your-extension}}` | `pi-subagents` |
91
+ | `{{your-key}}` | `tools` |
92
+ | `{{example-value}}` | `bash,read_file,write_file` |
93
+
94
+ ### tintinweb/pi-subagents
95
+
96
+ | Placeholder | Value |
97
+ | -------------------- | ------------------ |
98
+ | `{{your-extension}}` | `pi-subagents` |
99
+ | `{{your-key}}` | `disallowed_tools` |
100
+ | `{{example-value}}` | `write_file,bash` |
101
+
102
+ Additional note for tintinweb: since this extension runs subagents in-process via `createAgentSession()`, mention the [Event Bus RPC](https://github.com/gotgenes/pi-permission-system/blob/main/docs/cross-extension-api.md#policy-query-rpc-deprecated) as an optional runtime integration path for querying or forwarding permission prompts without spawning a subprocess.
103
+
104
+ ### HazAT/pi-interactive-subagents
105
+
106
+ | Placeholder | Value |
107
+ | -------------------- | -------------------------- |
108
+ | `{{your-extension}}` | `pi-interactive-subagents` |
109
+ | `{{your-key}}` | `deny-tools` |
110
+ | `{{example-value}}` | `write_file,bash` |
111
+
112
+ Additional note for HazAT: this extension already uses `PI_DENY_TOOLS` env var for subprocess tool denial.
113
+ The `permission:` frontmatter provides the same effect via `tool_name: deny` but adds `ask` as an intermediate option and covers surfaces beyond tools.
@@ -0,0 +1,365 @@
1
+ # Migration guide: legacy format → flat permission format
2
+
3
+ This guide covers migration from the pre-#66 config format to the flat `permission` format introduced in #66.
4
+
5
+ ## Summary of changes
6
+
7
+ The old format had six top-level policy keys (`defaultPolicy`, `tools`, `bash`, `mcp`, `skills`, `special`).
8
+ The new format has a single `permission` key whose top-level entries map surface names to actions.
9
+
10
+ Runtime knobs (`debugLog`, `permissionReviewLog`, `yoloMode`) are **unchanged** and stay at the top level.
11
+
12
+ ## Checklist
13
+
14
+ Go through each section below.
15
+ For each key present in your config, apply the translation and remove the old key.
16
+
17
+ - [ ] `defaultPolicy`
18
+ - [ ] `tools`
19
+ - [ ] `bash`
20
+ - [ ] `mcp`
21
+ - [ ] `skills`
22
+ - [ ] `special`
23
+ - [ ] Per-agent frontmatter
24
+
25
+ ## Translation reference
26
+
27
+ ### `defaultPolicy`
28
+
29
+ `defaultPolicy` set per-surface fallback actions.
30
+ In the flat format the universal fallback is `permission["*"]`; per-surface catch-alls are entries in the `permission` object.
31
+
32
+ ```jsonc
33
+ // Before
34
+ {
35
+ "defaultPolicy": {
36
+ "tools": "ask",
37
+ "bash": "ask",
38
+ "mcp": "ask",
39
+ "skills": "ask",
40
+ "special": "ask"
41
+ }
42
+ }
43
+
44
+ // After — all surfaces default to "ask" via the universal fallback
45
+ {
46
+ "permission": {
47
+ "*": "ask"
48
+ }
49
+ }
50
+ ```
51
+
52
+ If surfaces had **different** defaults, express each one explicitly:
53
+
54
+ ```jsonc
55
+ // Before
56
+ {
57
+ "defaultPolicy": {
58
+ "tools": "allow",
59
+ "bash": "ask",
60
+ "mcp": "ask",
61
+ "skills": "ask",
62
+ "special": "deny"
63
+ }
64
+ }
65
+
66
+ // After
67
+ {
68
+ "permission": {
69
+ "*": "allow",
70
+ "bash": "ask",
71
+ "mcp": "ask",
72
+ "skill": "ask",
73
+ "external_directory": "deny"
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### `tools`
79
+
80
+ Each entry in `tools` maps a tool name to a permission.
81
+ In the flat format, tool names are surface keys directly inside `permission`.
82
+
83
+ ```jsonc
84
+ // Before
85
+ {
86
+ "tools": {
87
+ "read": "allow",
88
+ "write": "deny",
89
+ "edit": "ask"
90
+ }
91
+ }
92
+
93
+ // After
94
+ {
95
+ "permission": {
96
+ "read": "allow",
97
+ "write": "deny",
98
+ "edit": "ask"
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Special case — `tools.bash` and `tools.mcp`:** These were catch-all overrides for their respective surfaces.
104
+ In the flat format, use a string shorthand or an explicit `"*"` pattern:
105
+
106
+ ```jsonc
107
+ // Before
108
+ {
109
+ "tools": { "bash": "allow", "mcp": "deny" }
110
+ }
111
+
112
+ // After — string shorthand (equivalent to { "*": "allow" })
113
+ {
114
+ "permission": {
115
+ "bash": "allow",
116
+ "mcp": "deny"
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### `bash`
122
+
123
+ Bash patterns translate directly; the surface name stays `bash`.
124
+ If you also had a `tools.bash` or `defaultPolicy.bash` value different from `defaultPolicy.tools`, add an explicit `"*"` catch-all pattern at the **start** of the object (so specific patterns placed after it override it via last-match-wins).
125
+
126
+ ```jsonc
127
+ // Before
128
+ {
129
+ "defaultPolicy": { "tools": "allow", "bash": "ask" },
130
+ "bash": {
131
+ "git status": "allow",
132
+ "git diff": "allow",
133
+ "git *": "ask",
134
+ "rm -rf *": "deny"
135
+ }
136
+ }
137
+
138
+ // After
139
+ {
140
+ "permission": {
141
+ "*": "allow",
142
+ "bash": {
143
+ "*": "ask",
144
+ "git status": "allow",
145
+ "git diff": "allow",
146
+ "git *": "ask",
147
+ "rm -rf *": "deny"
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ > **Note:** Pattern ordering matters within an object.
154
+ > `normalizeFlatConfig` preserves insertion order, and `evaluate` uses last-match-wins.
155
+ > Put broad catch-alls **first** and specific overrides **after** them.
156
+
157
+ ### `mcp`
158
+
159
+ MCP patterns translate directly; the surface name stays `mcp`.
160
+
161
+ ```jsonc
162
+ // Before
163
+ {
164
+ "mcp": {
165
+ "mcp_status": "allow",
166
+ "mcp_list": "allow",
167
+ "exa:*": "allow",
168
+ "dangerous-server": "deny"
169
+ }
170
+ }
171
+
172
+ // After
173
+ {
174
+ "permission": {
175
+ "mcp": {
176
+ "mcp_status": "allow",
177
+ "mcp_list": "allow",
178
+ "exa:*": "allow",
179
+ "dangerous-server": "deny"
180
+ }
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### `skills`
186
+
187
+ The surface name changes from `skills` (plural) to `skill` (singular).
188
+
189
+ ```jsonc
190
+ // Before
191
+ {
192
+ "skills": {
193
+ "*": "ask",
194
+ "librarian": "allow",
195
+ "dangerous-*": "deny"
196
+ }
197
+ }
198
+
199
+ // After — note: "skills" → "skill"
200
+ {
201
+ "permission": {
202
+ "skill": {
203
+ "*": "ask",
204
+ "librarian": "allow",
205
+ "dangerous-*": "deny"
206
+ }
207
+ }
208
+ }
209
+ ```
210
+
211
+ ### `special`
212
+
213
+ `special.external_directory` becomes a top-level surface key in `permission`.
214
+ Other deprecated keys (`doom_loop`, `tool_call_limit`) are simply dropped.
215
+
216
+ ```jsonc
217
+ // Before
218
+ {
219
+ "special": {
220
+ "external_directory": "ask",
221
+ "doom_loop": "deny",
222
+ "tool_call_limit": "deny"
223
+ }
224
+ }
225
+
226
+ // After — doom_loop and tool_call_limit are removed entirely
227
+ {
228
+ "permission": {
229
+ "external_directory": "ask"
230
+ }
231
+ }
232
+ ```
233
+
234
+ > **Note:** In the old format, `special.external_directory: "deny"` produced a rule with `matchedPattern: "external_directory"`.
235
+ > In the flat format, the string shorthand produces `pattern: "*"`, so `matchedPattern` is now `"*"` when the explicit rule matches.
236
+
237
+ ## Full before/after example
238
+
239
+ ```jsonc
240
+ // Before (legacy format)
241
+ {
242
+ "$schema": "...",
243
+ "debugLog": false,
244
+ "permissionReviewLog": true,
245
+ "yoloMode": false,
246
+ "defaultPolicy": {
247
+ "tools": "ask",
248
+ "bash": "ask",
249
+ "mcp": "ask",
250
+ "skills": "ask",
251
+ "special": "ask"
252
+ },
253
+ "tools": {
254
+ "read": "allow",
255
+ "write": "deny"
256
+ },
257
+ "bash": {
258
+ "git status": "allow",
259
+ "git *": "ask"
260
+ },
261
+ "mcp": {
262
+ "mcp_status": "allow"
263
+ },
264
+ "skills": {
265
+ "*": "ask"
266
+ },
267
+ "special": {
268
+ "external_directory": "ask"
269
+ }
270
+ }
271
+ ```
272
+
273
+ ```jsonc
274
+ // After (flat format)
275
+ {
276
+ "$schema": "...",
277
+ "debugLog": false,
278
+ "permissionReviewLog": true,
279
+ "yoloMode": false,
280
+ "permission": {
281
+ "*": "ask",
282
+ "read": "allow",
283
+ "write": "deny",
284
+ "bash": {
285
+ "git status": "allow",
286
+ "git *": "ask"
287
+ },
288
+ "mcp": {
289
+ "mcp_status": "allow"
290
+ },
291
+ "skill": { "*": "ask" },
292
+ "external_directory": "ask"
293
+ }
294
+ }
295
+ ```
296
+
297
+ ## Per-agent frontmatter
298
+
299
+ Frontmatter in agent `.md` files uses the same flat shape under the `permission` key.
300
+
301
+ ```yaml
302
+ # Before (legacy nested shape)
303
+ ---
304
+ permission:
305
+ defaultPolicy:
306
+ tools: allow
307
+ bash:
308
+ "git *": allow
309
+ tools:
310
+ mcp: deny
311
+ mcp:
312
+ exa_web_search_exa: allow
313
+ special:
314
+ external_directory: allow
315
+ ---
316
+ ```
317
+
318
+ ```yaml
319
+ # After (flat shape)
320
+ ---
321
+ permission:
322
+ "*": allow
323
+ bash:
324
+ "git *": allow
325
+ mcp:
326
+ "*": deny
327
+ exa_web_search_exa: allow
328
+ external_directory: allow
329
+ ---
330
+ ```
331
+
332
+ Key differences from the old frontmatter:
333
+
334
+ 1. The `"*"` key (quoted in YAML) replaces `defaultPolicy.tools`.
335
+ 2. `tools.bash` / `tools.mcp` catch-alls become `bash: <state>` or `mcp: { "*": <state>, ... }`.
336
+ 3. `special.external_directory` becomes `external_directory` at the top level of `permission`.
337
+ 4. Any surface key now works in frontmatter — extension tool names and `mcp` are no longer silently ignored.
338
+
339
+ ## Behavioral differences
340
+
341
+ ### Agent scope catch-alls override parent scope patterns
342
+
343
+ In the old format, `tools.bash: allow` (override layer) was lower priority than config-layer patterns from any scope, including global.
344
+ In the flat format, `bash: allow` in an agent scope is a config-layer catch-all with **higher** priority than global-scope patterns (last-match-wins, agent rules come later).
345
+
346
+ If you relied on global `rm -rf *: deny` surviving an agent's `tools.bash: allow`, you must now explicitly deny the pattern within the agent's own `bash` object:
347
+
348
+ ```yaml
349
+ # Old agent frontmatter — global "rm -rf *": "deny" survived
350
+ permission:
351
+ tools:
352
+ bash: allow
353
+
354
+ # New agent frontmatter — must repeat the deny if you want it preserved
355
+ permission:
356
+ bash:
357
+ "*": allow
358
+ "rm -rf *": deny
359
+ ```
360
+
361
+ ### `matchedPattern` for `external_directory`
362
+
363
+ In the old format, an explicit `special.external_directory: "deny"` rule had `matchedPattern: "external_directory"`.
364
+ In the flat format, `external_directory: "deny"` (string shorthand) has `matchedPattern: "*"`.
365
+ Code that inspected `matchedPattern` to detect explicit external-directory config must be updated.