@czottmann/pi-automode 1.11.0 → 1.13.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 ADDED
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## [1.13.0] - 2026-08-25
6
+
7
+ ## New features
8
+
9
+ - **Bash AST analysis** — Replace handwritten shell parsing with `unbash`. Permission and hard-deny checks now inspect command structure, nested commands, wrappers, redirects, and malformed input. Bash allow rules require complete structural coverage and fail closed when analysis is unsafe. (#26)
10
+ - **Extension-owned global config** — Store global settings at `~/.pi/agent/extensions/pi-automode/config.json`. Migrate the legacy file automatically, preserve a safe fallback after migration errors, and report conflicts through notifications and diagnostics. (#27)
11
+
12
+ ## Bug fixes
13
+
14
+ - **Conservative permission rules** — Malformed deny and ask patterns block actions. Malformed patterns do not expand allow rules. Permission checks examine each Bash subcommand and normalize whitespace. Path checks resolve symlinks and normalize `file://` and Windows paths. (#22)
15
+ - **Recursive deletion hard-deny** — Detect uppercase flags and GNU abbreviations for recursive `rm`, including commands behind `command`, `exec`, and `env`. Parse the `--` delimiter and shell tilde expansion. Protect Linux and macOS system roots without blocking active-home subdirectories, `/opt`, or `/srv`. (#23)
16
+ - **Global Pi safety-control paths** — Hard-deny direct writes and edits to `~/.pi/agent/extensions/`, `~/.pi/agent/settings.json`, and `~/.pi/agent/settings/`. Resolve case variants and symlink targets before matching. (#24)
17
+ - **Case-insensitive protected paths** — Match protected paths without case distinctions and normalize Unicode spellings. This closes path bypasses on case-insensitive filesystems. (#25)
18
+
19
+ ## [1.12.0] - 2026-08-23
20
+
21
+ ## New features
22
+
23
+ - **Deterministic permission allows** — Add user-owned `permissions.allow` patterns that skip classifier review after deterministic checks pass. Accepted ask rules still require classifier review. Thank you, @sergeykonkin! (#14)
24
+ - **Configurable classifier request timeout** — new `autoMode.classifierTimeoutMs` setting. The default is 20000 ms. The timeout applies to each classifier request. The fast stage and the detailed stage each have their own budget. A request that exceeds the timeout is aborted. Auto mode fails closed and blocks the action.
25
+ - **Read-only agent diagnostics** — Add `automode_inspect` tool for status, configuration, defaults, and recent denial metadata. Thank you, @blalor! (#11)
26
+
27
+ ## Bug fixes
28
+
29
+ - **Reject invalid config values** — Invalid boolean and log config values (e.g. `enabled: 0`, `log.enabled: 1`) are now rejected at merge time instead of being applied with diagnostics only. (#20)
30
+ - **Preserve defaults for malformed rule lists** — A malformed `hard_deny` entry like `[42]` no longer strips all built-in hard-deny rules. Malformed entries are rejected and defaults preserved conservatively. (#21)
31
+ - **Runtime classifier providers** — Dispatch classifier calls through Pi's runtime model registry so providers registered with `pi.registerProvider()` work immediately. Preserve normalized reasoning and header-only authentication on the temporary simple-completion bridge. (#15)
32
+ - **Bounded wildcard matching** — Replace regex-based permission and denied-path globs with a linear-time matcher. Reject oversized patterns and fail closed for oversized runtime inputs. (#19)
33
+ - **Complete classifier action input** — Send the exact current tool input to both classifier stages in a dedicated message. Block the action if it cannot fit without truncation. (#17)
34
+ - **Path policy normalization** — Use Pi-compatible resolution for file-tool paths, including file URLs, `@` and tilde aliases, and read fallback names. Enforce denied paths across omitted and recursive search scopes and both sides of symlink aliases. (#18)
35
+ - **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)
36
+ - **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)
37
+
38
+ [1.13.0]: https://github.com/czottmann/pi-automode/compare/v1.12.0...v1.13.0
39
+ [1.12.0]: https://github.com/czottmann/pi-automode/compare/v1.11.0...v1.12.0
package/README.md CHANGED
@@ -4,7 +4,9 @@ Claude Code-style auto mode for Pi.
4
4
 
5
5
  This is a guardrail extension. It intercepts agent tool calls before execution and blocks actions that match permission deny rules, deterministic hard-deny checks, or the auto-mode classifier's block decision.
6
6
 
7
- It is not a sandbox. Extensions run in the Pi process, and a determined malicious extension can do anything your user account can do. It also does not guard user `!` / `!!` shell commands; by design, it guards agent tool calls only. Use this to reduce unsafe autonomous tool use, not as an OS security boundary.
7
+ It is not a sandbox. Extensions run in the Pi process. A malicious extension can do anything that your user account can do.
8
+
9
+ Pi-automode does not guard user `!` or `!!` shell commands. It guards only agent tool calls. Use it to reduce unsafe autonomous tool use. Do not use it as an OS security boundary.
8
10
 
9
11
  ## Install
10
12
 
@@ -37,134 +39,59 @@ pi -e ./extensions/auto-mode.ts
37
39
  /automode defaults # print the built-in rule lists
38
40
  /automode config # effective config, resolved log file path, + diagnostics
39
41
  /automode denials # denial history for this session
40
- /automode model # open classifier model selector and save to ~/.pi/agent/automode.json
41
- /automode model provider/model-id # save classifier model to ~/.pi/agent/automode.json
42
+ /automode model # open classifier model selector and save to ~/.pi/agent/extensions/pi-automode/config.json
43
+ /automode model provider/model-id # save classifier model to ~/.pi/agent/extensions/pi-automode/config.json
42
44
  ```
43
45
 
44
46
  `/auto-mode` is an alias.
45
47
 
46
- ## Status line
47
-
48
- When the Pi TUI is available, the extension renders a persistent status line:
49
-
50
- ```text
51
- AM● a:12 d:2 ca:5 cd:1
52
- ```
53
-
54
- - `AM` — auto-mode prefix; `●` when enabled, `○` when disabled (via config or `/automode off`).
55
- - `a:` — actions allowed so far (checked minus denied).
56
- - `d:` — actions denied so far, for any reason (permission rule, deterministic hard-deny, or classifier).
57
- - `ca:` / `cd:` — classifier decisions split into allowed vs denied. These segments appear only after the classifier has run at least once; `d:` counts all denials, so `d:` is always `>= cd:`.
58
-
59
- ## Docs
60
-
61
- - [Defaults and rule-list behavior](docs/defaults.md)
62
- - [Auto-mode classifier flow](docs/automode-classifier-flow.md)
63
- - [Observability logging](docs/observability-logging.md)
64
-
65
- ## Configuration
48
+ ## Agent diagnostics
66
49
 
67
- The extension follows Claude Code's documented config model where Pi can support it.
50
+ The package registers one model-callable, read-only tool:
68
51
 
69
- It reads `autoMode` from Pi-owned config only:
52
+ `automode_inspect` accepts one `action`:
70
53
 
71
- - `~/.pi/agent/automode.json`
72
- - `.pi/automode.local.json`
73
- - `PI_AUTOMODE_SETTINGS_JSON`
54
+ - `status`: active state and counters
55
+ - `config`: active effective configuration, log path, and diagnostics
56
+ - `defaults`: built-in rule lists
57
+ - `denials`: recent denial timestamps, kinds, and tool names
74
58
 
75
- It deliberately does not read `autoMode` from shared project `.pi/automode.json`, because a checked-in repo should not be able to weaken auto-mode rules. Shared project config may still contribute `permissions.deny` and `permissions.ask`.
59
+ The tool reads the same in-memory configuration and state that the guardrail enforces. Permission and deterministic checks run before the bypass.
76
60
 
77
- To disable pi-automode for the current project, create or edit `.pi/automode.local.json`:
61
+ The bypass does not change automode counters, persisted state, or observability logs. The extension verifies the source of the tool before it applies the exemption. Thus, a name collision does not exempt a tool from another extension.
78
62
 
79
- ```json
80
- {
81
- "autoMode": {
82
- "enabled": false
83
- }
84
- }
85
- ```
86
-
87
- This is project-local and should not be committed. Shared project `.pi/automode.json` cannot disable auto-mode.
88
-
89
- Set a global default classifier model in `~/.pi/agent/automode.json`; override it per project in `.pi/automode.local.json`.
90
-
91
- `classifierReasoningLevel` optionally requests `low`, `medium`, `high`, `xhigh`, or `max` reasoning for both classifier stages. If the key is absent, pi-automode sends no reasoning preference and leaves the choice to the server. Pi AI clamps unsupported values to the nearest level supported by the selected model; a non-reasoning model resolves to `off`. `low` matches Codex Auto Review's reasoning effort and the practical default when an explicit value is needed. Higher levels can consume the existing 512/1200-token stage limits before producing visible output, which causes the classifier to fail closed. Raise `fastClassifierMaxTokens` (default 512, integer ≥ 16) if you run a reasoning model whose fast-stage budget is truncated before it emits the required `0`/`1` digit.
92
-
93
- `allowInsideWorkingDirectory` (default `false`) adds a deterministic silent-allow tier for the file tools (`read`, `write`, `edit`, `grep`, `find`, `ls`): when `true`, a call whose resolved path is inside the working directory is allowed without any classifier call, and file access outside the working directory is routed to the classifier (including reads, which would otherwise take the read-only fast path). This matches the Codex/Claude Code "inside the sandbox = silent, outside = review" model. The tier takes precedence over `classifyReadOnlyTools`: with both enabled, in-tree file access is still allowed without a classifier call, and out-of-tree file access is classified. `classifyReadOnlyTools: true` only routes in-tree reads to the classifier when `allowInsideWorkingDirectory` is `false`. Writes and edits to protected in-tree paths (`.git/hooks`, `.pi` controls, shell profiles, config files) are exempt from the silent-allow tier and still go to the classifier.
94
-
95
- `deniedPaths` (default `[]`) is a list of path glob patterns that are hard-denied before the classifier and before the inside-working-directory tier — the file-tool equivalent of a secret/system deny list. Patterns support `~`, `$HOME`, and `${HOME}` expansion and `*` (which matches any characters, including `/`, so `**/id_rsa` matches a private key at any depth). Matching checks both the path as typed and its symlink-resolved form, so a `~/.ssh/*` rule still matches when `~/.ssh` is a symlink. A matching path blocks the call unconditionally (no classifier, no override). The deny list applies to file tools only; `bash` path access is governed by the classifier. Both keys follow the normal scalar/array precedence.
96
-
97
- The setting follows the normal scalar precedence: global, then project-local, then `PI_AUTOMODE_SETTINGS_JSON`. Shared project `.pi/automode.json` cannot set it. Omitting the key at a higher-precedence scope does not clear a lower-precedence value.
98
-
99
- Example:
100
-
101
- ```json
102
- {
103
- "autoMode": {
104
- "classifierModel": "provider/model-id",
105
- "classifierReasoningLevel": "low",
106
- "classifyReadOnlyTools": false,
107
- "fastClassifierMaxTokens": 512,
108
- "allowInsideWorkingDirectory": false,
109
- "deniedPaths": [],
110
- "maxUserTranscriptTokens": 4000,
111
- "maxToolTranscriptTokens": 4000,
112
- "environment": [
113
- "$defaults",
114
- "Source control: github.example.com/acme-corp and all repos under it",
115
- "Trusted internal domains: *.corp.example.com, git.example.com",
116
- "Trusted cloud buckets: s3://acme-dev-artifacts, gs://acme-ci-cache",
117
- "Key internal services: staging deploy API at deploy.corp.example.com"
118
- ],
119
- "allow": ["$defaults"],
120
- "protectedPaths": ["$defaults"],
121
- "soft_deny": ["$defaults"],
122
- "hard_deny": [
123
- "$defaults",
124
- "Never send repository contents to third-party code-review APIs"
125
- ]
126
- },
127
- "permissions": {
128
- "deny": ["bash(rm -rf *)"],
129
- "ask": ["bash(git push *)"]
130
- }
131
- }
132
- ```
63
+ The tool cannot enable or disable auto mode. It cannot reload configuration, reset state, select a model, or change configuration.
133
64
 
134
- `maxUserTranscriptTokens` and `maxToolTranscriptTokens` are approximate per-category budgets; both default to 4000 and accept integers of at least 32. The former `maxTranscriptLines` setting is no longer supported because evidence selection is token-budgeted rather than line-based.
65
+ Pi sends tool output to the current model. The `status` and `denials` views omit denial reasons and action payloads.
135
66
 
136
- ### Ask-user tools and explicit authorization
67
+ If a diagnosis requires a reason, inspect a known-safe entry in the local observability log. The `config` view includes effective rule text. It removes raw JSON parser details from diagnostics.
137
68
 
138
- Classifier evidence includes normal user messages and assistant tool-call inputs, but excludes assistant prose and all tool results. This includes answers returned by ask-user tools such as `@vanillagreen/pi-questions`. Selecting "Yes" there helps the agent decide what to do next, but pi-automode does not treat that tool result as explicit authorization to override a soft deny. Send the authorization as a normal chat message instead; the agent can then retry the action. Tool results are excluded because they may contain untrusted or prompt-injected content.
69
+ Do not put credentials or other secrets in automode rules.
139
70
 
140
- ### `$defaults`
71
+ The bundled `automode-diagnostics` skill uses this tool to diagnose unexpected decisions without asking the user to copy output from slash commands. Configuration edits and automode state changes remain user-controlled. See [Agent diagnostics](docs/diagnostics.md) for the inspection contract, privacy limits, and diagnosis workflow.
141
72
 
142
- See [Defaults and rule-list behavior](docs/defaults.md) for built-in `environment`, `allow`, `protectedPaths`, `soft_deny`, and `hard_deny` entries, plus replacement behavior when `$defaults` is omitted.
143
-
144
- ### Observability logging
73
+ ## Status line
145
74
 
146
- Auto mode can write a JSONL observability log next to the current Pi session file, so you can inspect decisions and classifier usage. It is off by default.
75
+ When the Pi TUI is available, the extension renders a persistent status line:
147
76
 
148
- ```json
149
- {
150
- "autoMode": {
151
- "log": {
152
- "enabled": true,
153
- "classifierIo": false
154
- }
155
- }
156
- }
77
+ ```text
78
+ AM● a:12 d:2 ca:5 cd:1
157
79
  ```
158
80
 
159
- With logging enabled, the sidecar also writes ccusage-compatible entries for every classifier response. `ccusage pi` reports this usage as a separate `-pi-automode` session even when `classifierIo` is off.
160
-
161
- See [Observability logging](docs/observability-logging.md) for the log file location, entry schema, and the `classifierIo` privacy tradeoff. Run `/automode config` to see the resolved log file path.
81
+ - `AM` auto-mode prefix. `●` means enabled. `○` means disabled through configuration or `/automode off`.
82
+ - `a:` — actions allowed so far (checked minus denied).
83
+ - `d:` actions denied so far, for any reason (permission rule, deterministic hard-deny, or classifier).
84
+ - `ca:` / `cd:` — classifier decisions split into allowed and denied. These segments appear after the first classifier call. `d:` counts all denials, so `d:` is always `>= cd:`.
162
85
 
163
- ### Permission patterns
86
+ ## Docs
164
87
 
165
- Permission patterns use Pi tool names, for example `bash(...)`, `write(...)`, `edit(...)`, `read(...)`. The parser accepts capitalized names like `Bash(...)` for convenience, but the documented form is lowercase because Pi tool names are lowercase.
88
+ - [Configuration](docs/configuration.md)
89
+ - [Defaults and rule-list behavior](docs/defaults.md)
90
+ - [Auto-mode classifier flow](docs/automode-classifier-flow.md)
91
+ - [Observability logging](docs/observability-logging.md)
92
+ - [Architecture decisions](docs/adr/INDEX.md)
166
93
 
167
- ## What is enforced before the classifier
94
+ ## What runs before the classifier
168
95
 
169
96
  The extension blocks these before any allow or classifier decision:
170
97
 
@@ -177,11 +104,27 @@ The extension blocks these before any allow or classifier decision:
177
104
  - root, home, and system-path destructive deletes
178
105
  - edits to `.pi/automode*`, `.pi` auto-mode files, and this extension's safety-control files
179
106
 
180
- Read-only Pi tools (`read`, `grep`, `find`, `ls`) are allowed after those checks. Every side-effecting action goes to the classifier, including all `write` and `edit` calls, `bash`, MCP, subagent, network-capable tools, and unknown tools. This keeps classifier hard-deny rules unconditional; direct file writes cannot bypass them. Set `classifyReadOnlyTools: true` (default `false`) to route read-only tools through the classifier as well, so reads outside the trusted working tree can be denied by policy. With it enabled, every `read`, `grep`, `find`, and `ls` call runs the two-stage classifier, which raises the number of model calls, the latency, and the cost per session.
107
+ After these checks, pi-automode applies `permissions.allow`. Protected `write` and `edit` targets continue to the classifier.
108
+
109
+ Accepted ask rules also continue to the classifier. They cannot use an allow tier. Pi-automode then allows the read-only tools `read`, `grep`, `find`, and `ls`. Every remaining action goes to the classifier.
110
+
111
+ A `permissions.allow` rule intentionally skips classifier policy, including classifier `hard_deny` rules. Use narrow patterns and user-owned configuration. Deterministic hard-deny and path controls remain unconditional.
112
+
113
+ Set `classifyReadOnlyTools: true` (default `false`) to route read-only tools through the classifier. This configuration value increases model calls, latency, and session cost.
181
114
 
182
- Path matches in `deniedPaths` are blocked before every classifier and fast-path decision, so secret and system paths never reach the model through the file tools. The deny list does not govern `bash`; shell access to those paths is handled by the classifier and the deterministic hard-deny checks. With `allowInsideWorkingDirectory: true`, file tools inside the working directory are allowed without a classifier call, and outside-working-directory file access (reads included) goes to the classifier.
115
+ Pi-automode blocks a `deniedPaths` match before classifier review or an allow tier. Thus, file tools cannot send matching secret or system paths to the model.
183
116
 
184
- Classification starts with a one-token conservative filter and runs structured review only when that filter requests it. Both stages use a classifier-specific session key and short provider cache retention where the provider supports it. Missing models, provider failures, or malformed responses block the action.
117
+ The list does not govern `bash`. The classifier and deterministic hard-deny checks govern shell access to these paths.
118
+
119
+ The value `allowInsideWorkingDirectory: true` allows file access inside the working directory locally. Pi-automode sends all outside file access to the classifier, including reads.
120
+
121
+ Classification starts with a conservative one-token filter. If the filter requests review, pi-automode runs structured review.
122
+
123
+ 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.
124
+
125
+ 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.
126
+
127
+ 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.
185
128
 
186
129
  ## Examples
187
130
 
@@ -199,19 +142,44 @@ npm test
199
142
  npm pack --dry-run
200
143
  ```
201
144
 
202
- The tests cover the risky parts: scoped permission matching, config-source precedence, `$defaults` behavior, config diagnostics, deterministic hard-deny checks, shell parsing, write/edit classifier routing, symlink-aware safety-control checks, token-budgeted transcript selection, staged classifier parsing and caching options, and hook-level blocking/allowing.
145
+ The tests cover these safety-sensitive areas:
146
+
147
+ - scoped permission matching
148
+ - the `permissions.allow` tier and its precedence
149
+ - configuration-source precedence and diagnostics
150
+ - `$defaults` behavior
151
+ - deterministic hard-deny checks and Bash AST analysis
152
+ - classifier routing for `write` and `edit`
153
+ - symlink-aware safety-control checks
154
+ - token-budgeted transcript selection
155
+ - staged classifier parsing and cache behavior
156
+ - hook-level allow and block behavior
203
157
 
204
158
  ## Publishing
205
159
 
206
- GitHub Actions publishes the package to npm when a GitHub Release is published. The release tag must match `package.json` exactly, with or without a leading `v` (`v1.0.0` and `1.0.0` both work for version `1.0.0`).
160
+ 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`.
207
161
 
208
162
  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.
209
163
 
210
164
  ### Release tag must point at the version bump
211
165
 
212
- The publish workflow checks out the commit the release tag points at and compares `package.json` against the tag name. **The tag must point at a commit where `package.json` already carries the new version.** Concretely: commit the version bump (`chore: release X.Y.Z`), push `main`, then create the GitHub release targeting that pushed commit. Creating the release before pushing the version bump — or targeting a commit that still has the old version — fails the `Check release tag` step with `Release tag (vX.Y.Z) does not match package.json version (x.y.z)`.
166
+ The publish workflow checks the commit that the release tag identifies. It compares the version in `package.json` with the tag name.
167
+
168
+ **The tag must identify a commit that contains the new version.**
169
+
170
+ 1. Commit the version change with `chore: release X.Y.Z`.
171
+ 2. Push `main`.
172
+ 3. Create the GitHub release from that commit.
173
+
174
+ If the tag identifies an older version, the `Check release tag` step fails. It reports `Release tag (vX.Y.Z) does not match package.json version (x.y.z)`.
175
+
176
+ CAUTION: Do not move a correct release tag. Moving the tag changes a published reference.
177
+
178
+ 1. If the tag identifies the wrong commit, force-move it to the version-change commit.
179
+ 2. Force-push the corrected tag.
180
+ 3. Run `gh workflow run publish.yml --ref vX.Y.Z`.
213
181
 
214
- If the tag was cut against the wrong commit, fix it by force-moving it to the version-bump commit and pushing, then trigger the workflow via `gh workflow run publish.yml --ref vX.Y.Z` (the `release` event fires on tag creation; re-running a failed `release`-triggered run reuses the original ref and won't pick up the moved tag).
182
+ When GitHub creates the tag, the `release` event occurs. A rerun of a failed `release` workflow uses the original reference. It does not use the moved tag.
215
183
 
216
184
  ## Author
217
185
 
package/docs/GLOSSARY.md CHANGED
@@ -1,35 +1,39 @@
1
1
  # Glossary
2
2
 
3
- Canonical vocabulary for `@czottmann/pi-automode`. Project-specific terms only standard technical words appear here only when this project uses them in a specific way. Longer explanations live in the docs linked from each entry.
3
+ This glossary defines project terms for `@czottmann/pi-automode`. It does not define standard technical words. Each entry links to a longer explanation where applicable.
4
4
 
5
5
  ## Enforcement flow
6
6
 
7
- The ordered pipeline that runs on every agent tool call before execution. See [Auto-mode classifier flow](automode-classifier-flow.md).
7
+ The enforcement flow is the ordered pipeline that runs before each agent tool call. See [Auto-mode classifier flow](automode-classifier-flow.md).
8
8
 
9
- **Auto mode** — Claude Code-style guardrail posture: a pre-execution classifier allows routine, reversible actions and blocks risky ones, replacing routine permission prompts.
9
+ **Auto mode** — A Claude Code-style guardrail posture. A pre-execution classifier allows routine, reversible actions and blocks risky actions. It replaces routine permission prompts.
10
10
 
11
- **Deterministic hard-deny** — Local code checks that block high-risk actions before any classifier call and cannot be overridden. Distinct from the config-level [hard_deny](#classifier-policy-and-rules); independent of the model.
11
+ **Deterministic hard-deny** — Local code checks that block high-risk actions before classifier review. A user or model cannot override them. They are independent of the classifier-level [hard_deny](#classifier-policy-and-rules) rules.
12
12
 
13
- **Read-only bypass** — The default fast path where the read-only tools (`read`, `grep`, `find`, `ls`) are allowed without classifier review once permission and deterministic checks pass. `classifyReadOnlyTools` routes them through the classifier instead.
13
+ **`permissions.allow` tier** — A user-owned list of scoped tool patterns. A match skips classifier review after deterministic checks pass. An accepted `permissions.ask` rule disables this tier for the current call. The tier cannot override a denial or cover protected `write` and `edit` targets. See [ADR-001](adr/ADR-001-permission-precedence-and-trust-boundaries.md).
14
14
 
15
- **Staged classifier** — The two-stage safety classifier: a conservative one-token filter gates an optional structured review. See [Fast stage](#enforcement-flow) and [Detailed stage](#enforcement-flow).
15
+ **Read-only bypass** — The default allow tier for `read`, `grep`, `find`, and `ls`. Permission and deterministic checks run before this tier. `classifyReadOnlyTools` sends these calls to the classifier instead.
16
16
 
17
- **Fast stage** — The first classifier stage: a one-token filter that returns `0` (clearly allowed) or `1` (may need review).
17
+ **Staged classifier** — A two-stage safety classifier. A conservative one-token filter controls access to an optional structured review. See [Fast stage](#enforcement-flow) and [Detailed stage](#enforcement-flow).
18
18
 
19
- **Detailed stage** — The second classifier stage, run when the fast stage requests review, that returns a structured allow/block decision.
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
+
21
+ **Detailed stage** — The second classifier stage. It runs after a fast-stage review result and returns a structured allow or block decision.
20
22
 
21
23
  ## Classifier policy and rules
22
24
 
23
- The classifier's deny tiers and rule-list syntax. See [Defaults and rule-list behavior](defaults.md).
25
+ The classifier policy defines denial tiers and rule-list syntax. See [Defaults and rule-list behavior](defaults.md).
26
+
27
+ **hard_deny** — Classifier rules that block unconditionally. They are independent of the code-level [deterministic hard-deny](#enforcement-flow) checks.
24
28
 
25
- **hard_deny** — Unconditional classifier rules that cannot be overridden. Distinct from the code-level [deterministic hard-deny](#enforcement-flow) checks.
29
+ **soft_deny** — Classifier rules that normally block but support defined overrides. Unlike [hard_deny](#classifier-policy-and-rules), these rules are not unconditional.
26
30
 
27
- **soft_deny** — Overridable classifier block rules, unlike [hard_deny](#classifier-policy-and-rules).
31
+ **explicit_intent** — A classifier tier for direct user authorization. The latest user instruction specifically authorizes an action that matches a [soft_deny](#classifier-policy-and-rules) rule.
28
32
 
29
- **explicit_intent** — A classifier tier meaning the allow was justified because the user's latest instruction directly and specifically authorized an otherwise [soft-denied](#classifier-policy-and-rules) action.
33
+ **allow exception** (`autoMode.allow`) — A prose rule that overrides a matching [soft_deny](#classifier-policy-and-rules) rule. It cannot override [hard_deny](#classifier-policy-and-rules). It is independent of the [`permissions.allow` tier](#enforcement-flow).
30
34
 
31
- **`$defaults`** — A section-local marker in a rule list that expands to the built-in entries for that list.
35
+ **`$defaults`** — A section-local marker in a rule list. It expands to the built-in entries for that list.
32
36
 
33
37
  ## Status
34
38
 
35
- **AM status line** — The persistent TUI footer (prefixed `AM`) that reports auto-mode status: enabled/disabled and action/classifier counts.
39
+ **AM status line** — The persistent TUI footer that starts with `AM`. It reports the auto-mode state and action counts.
@@ -0,0 +1,46 @@
1
+ # ADR-001: Permission precedence and configuration trust boundaries
2
+
3
+ **Date:** 2026-08-23
4
+
5
+ ## Context
6
+
7
+ Pi-automode supports `permissions.deny`, `permissions.ask`, and `permissions.allow` tool patterns. These rules affect whether a tool call reaches the classifier.
8
+
9
+ An allow rule reduces classifier coverage. A checked-in repository file must not reduce that coverage. An accepted ask rule must also keep its promise of classifier review.
10
+
11
+ The wildcard matcher limits its input size to keep matching time bounded. Deny and ask rules must fail closed for oversized inputs. The same behavior is unsafe for allow rules.
12
+
13
+ ## Decision
14
+
15
+ Pi-automode uses this permission policy:
16
+
17
+ 1. A matching `permissions.deny` rule blocks the tool call.
18
+ 2. A matching `permissions.ask` rule requires user confirmation.
19
+ 3. If the user declines, pi-automode blocks the tool call.
20
+ 4. If the user accepts, deterministic denial checks continue.
21
+ 5. After these checks pass, the classifier reviews the accepted tool call.
22
+ 6. An accepted ask rule disables all later deterministic allow tiers for that call.
23
+ 7. If no ask rule matched, a `permissions.allow` rule can skip classifier review.
24
+ 8. Deterministic hard-deny checks, path-deny checks, and protected-path controls take precedence over allow rules.
25
+
26
+ Pi-automode reads `permissions.allow` only from user-owned configuration sources:
27
+
28
+ - `~/.pi/agent/automode.json`
29
+ - trusted `.pi/automode.local.json`
30
+ - `PI_AUTOMODE_SETTINGS_JSON`
31
+
32
+ Shared `.pi/automode.json` can add deny and ask rules after project trust. It cannot add allow rules.
33
+
34
+ [ADR-002](ADR-002-global-config-in-extension-data-directory.md) replaces only the global configuration path. Permission precedence and trust boundaries stay unchanged.
35
+
36
+ For an oversized matcher input, deny and ask rules return a match. Allow rules return no match. This behavior keeps denial rules fail-closed without broadening allow rules.
37
+
38
+ ## Consequences
39
+
40
+ A repository cannot use checked-in configuration to remove classifier review. Users can still add project-specific allow rules in the local configuration file.
41
+
42
+ An accepted ask rule always causes classifier review after deterministic checks pass. This behavior is stricter than a model where confirmation grants direct permission.
43
+
44
+ The matcher needs separate overflow behavior for denial and allow contexts. Tests must cover both behaviors.
45
+
46
+ The permission lists are not symmetric across configuration sources. The documentation and diagnostics must make this difference clear.
@@ -0,0 +1,60 @@
1
+ # ADR-002: Store the global config in the extension data directory
2
+
3
+ **Date:** 2026-08-25
4
+
5
+ ## Context
6
+
7
+ Pi-automode stores its global configuration at `~/.pi/agent/automode.json`. The extension stores application-owned logs below `~/.pi/agent/extensions/pi-automode/logs/`.
8
+
9
+ The separate global configuration path makes extension-owned data harder to identify and manage. The global configuration belongs beside the existing log directory.
10
+
11
+ An existing installation can contain the legacy file. Migration must not discard configuration data or create different read and write sources.
12
+
13
+ This decision refines the global user-owned configuration source from [ADR-001](ADR-001-permission-precedence-and-trust-boundaries.md). It does not change permission precedence or project trust boundaries.
14
+
15
+ Tracking issue: [#27](https://github.com/czottmann/pi-automode/issues/27)
16
+
17
+ ## Decision
18
+
19
+ Pi-automode stores the global configuration at:
20
+
21
+ ```text
22
+ ~/.pi/agent/extensions/pi-automode/config.json
23
+ ```
24
+
25
+ The existing log directory stays at:
26
+
27
+ ```text
28
+ ~/.pi/agent/extensions/pi-automode/logs/
29
+ ```
30
+
31
+ Project configuration files keep their current paths and semantics:
32
+
33
+ - `.pi/automode.local.json`
34
+ - `.pi/automode.json`
35
+
36
+ At startup, pi-automode selects one active global configuration path for the session:
37
+
38
+ 1. If only the new file is present, pi-automode uses the new file.
39
+ 2. If only the legacy file is present, pi-automode moves it to the new path before loading configuration.
40
+ 3. If the two files are present, pi-automode uses the new file and does not change the legacy file.
41
+ 4. If migration fails, pi-automode uses the legacy file for reads and writes during that session.
42
+ 5. If neither file is present, pi-automode uses the new path for the next write.
43
+
44
+ Pi-automode attempts migration once per session. A later session retries a failed migration.
45
+
46
+ A successful migration produces one UI notification. A path conflict produces a warning during every startup. A migration error produces one warning per session.
47
+
48
+ Conflict and migration error details also appear in configuration diagnostics. This behavior makes the details available in headless sessions.
49
+
50
+ ## Consequences
51
+
52
+ All extension-owned global data has one identifiable root directory. Configuration backups and extension cleanup become easier to understand.
53
+
54
+ The new configuration inherits the deterministic protection for `~/.pi/agent/extensions/`. Direct agent file tools cannot modify the configuration.
55
+
56
+ The extension must pass the selected active path to configuration reads and writes. This requirement prevents split-brain configuration during migration errors.
57
+
58
+ The migration is one-way. A downgraded pi-automode version does not automatically find the new configuration path.
59
+
60
+ Startup gains a small filesystem operation and migration state. Tests must cover migration, conflicts, errors, notifications, diagnostics, and unchanged project precedence.
@@ -0,0 +1,6 @@
1
+ # Architecture Decision Records
2
+
3
+ | # | Decision | Date |
4
+ |---|----------|------|
5
+ | [ADR-001](ADR-001-permission-precedence-and-trust-boundaries.md) | Permission precedence and configuration trust boundaries | 2026-08-23 |
6
+ | [ADR-002](ADR-002-global-config-in-extension-data-directory.md) | Store the global config in the extension data directory | 2026-08-25 |