@giuseppe.trisciuoglio/pi-prevent-destructive-commands 1.2.0 → 1.3.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
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - New per-project read-only mode (`/read-only on|off|status`): when enabled, every mutating tool call is blocked — `write`, `edit`, any patch-style tool (`*apply_patch`), `bash`, and `powershell` — while read-only tools (`read`, `grep`, `find`, `ls`) keep working. The flag lives in `.pi/prevent-destructive-commands.json` under `readOnly` (default `false` when the file is missing or invalid), `/read-only` toggles it while preserving unrelated JSON keys, and it is independent of the destructive-command, git, migration, Nx, and comment guards. Adds `test/read-only-command-test.ts` (`npm run test:read-only-command`) and `docs/adr/0001-read-only-mode.md`.
13
+ - New guard: new comments in source files are blocked. Any tool call that would introduce a new comment into a source file (Java, TypeScript/JavaScript, Dart, Go, Rust, C/C++, C#, Swift, Kotlin, Scala, PHP, CSS/SCSS, Python, Ruby, shell, YAML, TOML, Terraform, GraphQL, SQL, Lua, Haskell, Elm, HTML/XML/SVG/Markdown) is stopped, covering `write`, `edit`, `apply_patch` (unified diff and `*** Begin Patch` formats), and bash write vectors (`cat > a.ts <<EOF`, `tee b.py <<EOF`, `echo "// x" >> c.ts`). Detection is string-aware (comment markers inside string literals are not comments), comments a file already contains may be carried over untouched, and shebang lines, tooling directives (`@ts-ignore`, `eslint-disable`, `biome-ignore`, `noqa`, …) and purely decorative lines are exempt. Opt out per project with `{"disableCommentGuard": true}` in the opt-out file. Adds `test/comment-guard-test.ts` (`npm run test:comment-guard`).
14
+ - New guard: interactive `git commit` without a message option (bare `git commit`, `git commit -a`, `git commit --amend`, ...) is blocked because it opens the editor and hangs the session when the agent runs unattended. Applies unconditionally, even on projects that opted out of the git add/commit/push guards; commits with a message (`git commit -m x`, `-F file`, `--amend --no-edit`, ...) are unaffected.
15
+
16
+ ### Changed
17
+
18
+ - Extracted shared helpers to remove all code clones reported by jscpd (≥ 50 tokens): `src/rules/args.ts` (positional-argument scanning and longest-subcommand matching for the checker and the aws/gh/file-reading rules), `src/patch-paths.ts` (patch/file-path extraction shared by the extension entry point and the Nx guard), `src/token-segments.ts` (command-chain segmentation for the bash-level guards), and `test/helpers.ts` (shared test utilities: `check`, `makeProject`, `isBlocked`, `runCheckCases`, `runIsolatedTest`). No behavior change.
19
+
10
20
  ## [1.2.0] - 2026-09-28
11
21
 
12
22
  ### Added
package/README.md CHANGED
@@ -18,6 +18,8 @@ A faithful port of Claude's [`prevent-destructive-commands.py`](https://github.c
18
18
  - **Works in all modes** — Protection is active even in non-interactive sessions (`-p`, JSON, RPC).
19
19
  - **Recursive analysis** — Traverses command wrappers, shell invocations, pipelines, and nested commands to catch obfuscated attacks.
20
20
  - **Nx configuration guard** — In Nx workspaces, existing `package.json` and TypeScript configuration files are immutable to the agent; missing ones can still be created.
21
+ - **Source comment guard** — The agent cannot introduce new comments into source files (Java, TypeScript, Dart, Python, SQL, HTML, and more) through writing tools or bash write vectors; existing comments may be carried over untouched and tooling directives (`@ts-ignore`, `eslint-disable`, …) stay allowed.
22
+ - **Read-only mode** — `/read-only on` turns the project into an inspection-only workspace: `write`, `edit`, patch tools, `bash`, and `powershell` are blocked while `read`, `grep`, `find`, and `ls` keep working.
21
23
  - **Configurable** — Tune protection levels via simple flags in `src/config.ts`.
22
24
  - **Zero dependencies** — Lightweight, fast, and self-contained.
23
25
  - **79+ test cases** — Comprehensive smoke test suite validates all blocking rules.
@@ -30,6 +32,7 @@ All rules are defined in [`src/config.ts`](src/config.ts) and can be customized.
30
32
  |----------|----------|
31
33
  | **Destructive Git** | `git reset --hard`, `git clean`, `git push --force` / `-f` / `--delete`, `git branch -D`, `git tag -d`, `git checkout -f`, `git rebase`, `git filter-branch`, `git filter-repo`, `git reflog expire`, `git update-ref -d` |
32
34
  | **Git add/commit/push** | `git add`, `git commit`, `git push` *(see `ENABLE_GIT_ADD_COMMIT_BLOCK` flag and the per-project opt-out below; force/delete push variants are always blocked)* |
35
+ | **Interactive git commit** | `git commit` without a message option (`-m`/`--message`, `-F`/`--file`, `-C`/`--reuse-message`, `-c`/`--reedit-message`, `--no-edit`, `--fixup`, or combined short forms like `-am`) — it would open the editor and hang the session. Always blocked, even when the project opts out of the git add/commit/push guards. |
33
36
  | **rm / path-sensitive** | `rm`, `rmdir`, `shred`, `unlink` targeting paths **outside** the working directory (e.g., `/etc`, `~`, `..`). Targets inside cwd are allowed. |
34
37
  | **find outside cwd** | `find` whose search root is **outside** the working directory (e.g., `find /etc`, `find ~`, `find ..`). Search roots inside cwd are allowed; `-exec` payloads are analyzed recursively as before. |
35
38
  | **Destructive Docker** | `docker rm` / `rmi`, `docker container/image/volume/network rm`, `docker * prune`, `docker compose down -v`, `docker compose rm`, `docker context rm`, `docker swarm leave --force` |
@@ -37,6 +40,7 @@ All rules are defined in [`src/config.ts`](src/config.ts) and can be customized.
37
40
  | **Destructive AWS CLI** | `aws s3 rm`, `aws ec2 terminate-instances`, `aws rds delete-db-instance`, `aws cloudformation delete-stack`, and 50+ more subcommands (full list in `src/config.ts`) |
38
41
  | **Sensitive file reads** | `cat`, `grep`, etc. on `.env`, SSH keys, `.pem` files — **disabled by default** via `ENABLE_SENSITIVE_FILE_CHECK` *(see Configuration)* |
39
42
  | **Existing Nx configuration** | `package.json`, `tsconfig.json`, `tsconfig.base.json`, `tsconfig.lib.json`, and `tsconfig.spec.json` anywhere below a workspace containing `nx.json`. Creating a missing file is allowed; changing or deleting an existing file is blocked. |
43
+ | **New comments in source files** | Any tool call that introduces a **new** comment into a source file: `write`/`edit`/`apply_patch` touching Java, TypeScript/JavaScript, Dart, Go, Rust, C/C++, C#, Swift, Kotlin, Scala, PHP, CSS/SCSS, Python, Ruby, shell, YAML, TOML, Terraform, GraphQL, SQL, Lua, Haskell, Elm, HTML/XML/SVG/Markdown files, plus bash write vectors (`cat > a.ts <<EOF`, `tee b.py <<EOF`, `echo "// x" >> c.ts`). Comments already in the file may be carried over; shebang lines, tooling directives (`@ts-ignore`, `eslint-disable`, `biome-ignore`, `noqa`, …) and purely decorative lines are exempt *(see the per-project opt-out below)* |
40
44
 
41
45
  ### Recursive Analysis
42
46
 
@@ -64,7 +68,7 @@ Anything the extractor cannot parse unambiguously (here-strings `<<<`, variable
64
68
  ### Via pi Marketplace
65
69
 
66
70
  ```bash
67
- pi install @giuseppetrisciuoglio/pi-prevent-destructive-commands
71
+ pi install npm:@giuseppe.trisciuoglio/pi-prevent-destructive-commands
68
72
  ```
69
73
 
70
74
  ### Manual Installation
@@ -104,14 +108,22 @@ pi /reload
104
108
 
105
109
  ### Per-Project Opt-Out
106
110
 
107
- Create `.pi/prevent-destructive-commands.json` in the project root to disable the `git add` / `git commit` / `git push` guards **for that project only**:
111
+ Create `.pi/prevent-destructive-commands.json` in the project root to tune the guards **for that project only**:
108
112
 
109
113
  ```json
110
114
  {
111
- "disableGitGuards": true
115
+ "disableGitGuards": true,
116
+ "disableCommentGuard": true,
117
+ "readOnly": false
112
118
  }
113
119
  ```
114
120
 
121
+ | Flag | Default | Effect |
122
+ |------|---------|--------|
123
+ | `disableGitGuards` | `false` | Disables the `git add` / `git commit` / `git push` guards. |
124
+ | `disableCommentGuard` | `false` | Disables the source comment guard, allowing the agent to write comments into source files again. |
125
+ | `readOnly` | `false` | When `true`, blocks every mutating tool call (`write`, `edit`, patch tools, `bash`, `powershell`) while read-only tools keep working. |
126
+
115
127
  Or use the `/git-guards` slash command from the pi TUI:
116
128
 
117
129
  | Command | Effect |
@@ -120,6 +132,18 @@ Or use the `/git-guards` slash command from the pi TUI:
120
132
  | `/git-guards off` | Write `{"disableGitGuards": true}` (unrelated JSON keys are preserved). |
121
133
  | `/git-guards on` | Write `{"disableGitGuards": false}`. |
122
134
 
135
+ ### Read-Only Mode
136
+
137
+ Use the `/read-only` slash command to make the project inspection-only:
138
+
139
+ | Command | Effect |
140
+ |---------|--------|
141
+ | `/read-only` | Show whether read-only mode is active for the current project, and the state of the configuration file. |
142
+ | `/read-only on` | Write `{"readOnly": true}` (unrelated JSON keys are preserved). |
143
+ | `/read-only off` | Write `{"readOnly": false}`. |
144
+
145
+ While read-only mode is active, the agent can inspect files with `read`, `grep`, `find`, and `ls`, but `write`, `edit`, every patch-style tool, `bash`, and `powershell` are blocked with a clear reason. Read-only mode is independent of the destructive-command, git, migration, Nx, and comment guards, and it defaults to off when the configuration file is missing or invalid.
146
+
123
147
  Notes:
124
148
 
125
149
  - Forceful and deleting push variants (`--force`, `-f`, `--force-with-lease`, `-d`, `--delete`) are **always blocked** and cannot be opted out.
@@ -138,10 +162,15 @@ prevent-destructive-commands/
138
162
  │ ├── config.ts # Blacklists and behavior flags — tune protection here
139
163
  │ ├── tokenizer.ts # Shell tokenizer (shlex-like)
140
164
  │ ├── checker.ts # Recursive command walker (wrappers/shell/find/xargs)
165
+ │ ├── read-only-command.ts # /read-only toggle and read-only tool-call gate
141
166
  │ ├── migration-guard.ts
142
167
  │ ├── nx-guard.ts # Protects existing Nx package and TypeScript configuration files
168
+ │ ├── comment-guard.ts # Blocks new comments written into source files
169
+ │ ├── patch-paths.ts # Shared patch/file-path extraction from tool inputs
170
+ │ ├── token-segments.ts # Shared command-segment splitting for bash-level guards
143
171
  │ └── rules/ # Per-category destructive-command handlers
144
172
  │ ├── types.ts # Shared CheckResult type + helpers
173
+ │ ├── args.ts # Token-scanning helpers (positional args, subcommand matching)
145
174
  │ ├── path-utils.ts # cwd-relative path resolution
146
175
  │ ├── git.ts # git reset --hard, push --force, ...
147
176
  │ ├── docker.ts # docker rm, system prune, ...
@@ -150,8 +179,10 @@ prevent-destructive-commands/
150
179
  │ └── path-sensitive.ts # rm/rmdir/... outside-cwd detection
151
180
  ├── test/
152
181
  │ ├── smoke-test.ts # Standalone test suite (79+ cases)
182
+ │ ├── helpers.ts # Shared test utilities (check, makeProject, runCheckCases, ...)
153
183
  │ ├── migration-guard-test.ts
154
184
  │ ├── nx-guard-test.ts # Verifies Nx configuration protection and allowed creation
185
+ │ ├── comment-guard-test.ts # Verifies new-comment blocking across write, edit, patch, bash
155
186
  │ └── e2e-install-test.ts # End-to-end: verifies real installation/discovery by pi
156
187
  ├── tsconfig.json # TypeScript configuration
157
188
  ├── package.json # Package metadata for pi marketplace
@@ -176,6 +207,8 @@ Standalone tests for the tokenizer/checker logic (no dependency on pi itself):
176
207
  npm run test:smoke
177
208
  npm run test:heredoc
178
209
  npm run test:nx-guard
210
+ npm run test:comment-guard
211
+ ```
179
212
 
180
213
  # Or directly with tsx
181
214
  npx tsx test/smoke-test.ts
@@ -203,6 +236,7 @@ npx tsx test/e2e-install-test.ts
203
236
  The test suite covers:
204
237
  - All destructive Git operations
205
238
  - Existing Nx package and TypeScript configuration protection, including direct tool writes, patches, shell redirection, and package-manager dependency changes
239
+ - Source comment blocking across `write`, `edit`, `apply_patch`, and bash write vectors (heredoc, `tee`, `echo`/`printf`), including string-literal false-positive checks and the per-project opt-out
206
240
  - Path-sensitive `rm` protection
207
241
  - Docker destructive commands
208
242
  - AWS CLI destructive subcommands
@@ -223,6 +257,7 @@ As with the original Claude plugin, the analysis is static and therefore cannot
223
257
  | Limitation | Example | Explanation |
224
258
  |------------|---------|-------------|
225
259
  | **Unknown wrappers** | Custom destructive tools | The extension covers known patterns; unknown wrappers or custom destructive tools are not intercepted. |
260
+ | **Comment guard scope** | `python -c`, `sed` insertion; `.vue`/`.svelte`/`.astro` files | The comment guard covers writing tools and the common bash write vectors (heredoc, `tee`, `echo`/`printf` arguments); other generators are not intercepted. Mixed-language single-file components are excluded to avoid false positives across template/script/style blocks. A bash rewrite of an existing file counts every comment in the heredoc body as new. |
226
261
 
227
262
  Two cases that used to be listed here are now handled:
228
263
 
@@ -233,13 +268,14 @@ Two cases that used to be listed here are now handled:
233
268
 
234
269
  ## How It Works
235
270
 
236
- When pi attempts to execute a bash command, this extension intercepts the `tool_call` event and:
271
+ When pi attempts to execute a tool call, this extension intercepts the `tool_call` event and:
237
272
 
238
273
  1. **Extracts heredoc bodies** so prose fed through `cat > file <<'EOF'` is treated as data, keeping bodies under analysis only when an executor on the command line would run them.
239
274
  2. **Tokenizes** the command string using a shlex-like shell tokenizer that respects quotes and escapes.
240
275
  3. **Analyzes** the token stream recursively, traversing wrappers, shell invocations, and pipelines.
241
- 4. **Blocks** if any destructive pattern is detected, returning a clear reason to the agent.
242
- 5. **Allows** safe commands to pass through without modification.
276
+ 4. **Checks writing tool calls** against the file-content guards: immutable Nx configuration, and new comments introduced into source files (string-aware, per language family, with only-newly-introduced comments blocked).
277
+ 5. **Blocks** if any destructive pattern is detected, returning a clear reason to the agent.
278
+ 6. **Allows** safe commands to pass through without modification.
243
279
 
244
280
  The agent never receives an interactive prompt — the block is final and must be handled by finding a safe alternative.
245
281
 
@@ -0,0 +1,53 @@
1
+ # ADR 0001: Read-only mode blocks shell execution wholesale
2
+
3
+ - Status: accepted
4
+ - Date: 2026-10-23
5
+ - Author: Giuseppe Trisciuoglio
6
+
7
+ ## Context
8
+
9
+ The extension guards against destructive commands and protected paths, but there
10
+ is no way to declare a project inspection-only. Users wanted a per-project flag
11
+ that lets the agent read files and explore the codebase while preventing any
12
+ modification. The natural home for the flag is the existing per-project
13
+ configuration file, `.pi/prevent-destructive-commands.json`, and the `/git-guards`
14
+ command already established the pattern of a user-invoked toggle that writes that
15
+ file from the extension process.
16
+
17
+ Tools that can mutate state are `write`, `edit`, any patch-style variant such as
18
+ `apply_patch`, `bash`, and `powershell`. Read-only tools are `read`, `grep`,
19
+ `find`, `ls`, and similar non-mutating tools.
20
+
21
+ The open design question was whether read-only mode should keep allowing
22
+ "obviously read-only" shell commands such as `cat`, `ls`, or `grep` invoked
23
+ through `bash`. Classifying a shell command line as read-only is the same
24
+ unsolved problem the destructive-command checker already deals with: commands
25
+ can hide mutations behind wrappers, redirects, command substitution, heredocs,
26
+ and arbitrary binaries.
27
+
28
+ ## Decision
29
+
30
+ Read-only mode blocks `bash` and `powershell` entirely, without attempting to
31
+ classify individual shell commands as safe. It also blocks `write`, `edit`, and
32
+ every tool whose name ends with `apply_patch`. Read-only tools keep working.
33
+
34
+ The mode is stored as a `readOnly` boolean in
35
+ `.pi/prevent-destructive-commands.json`. It defaults to `false` when the file is
36
+ missing or invalid, so the agent stays writable until the user explicitly turns
37
+ it on. The `/read-only on|off|status` slash command toggles the flag while
38
+ preserving unrelated JSON keys, mirroring `/git-guards`.
39
+
40
+ When read-only mode is active, the `tool_call` handler checks it first and
41
+ short-circuits the other write-related guards: a rejected write needs no further
42
+ analysis.
43
+
44
+ ## Consequences
45
+
46
+ - Inspection-only projects are safe: no shell invocation can mutate the
47
+ workspace while the mode is on.
48
+ - Users lose the convenience of running read-only shell commands through `bash`
49
+ in read-only mode; they use `read`, `grep`, `find`, and `ls` instead.
50
+ - The mode is per project, so different projects can be in different modes at
51
+ the same time.
52
+ - Read-only mode is independent of the destructive-command, git, migration, Nx,
53
+ and comment guards; those behaviors are unchanged when the mode is off.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giuseppe.trisciuoglio/pi-prevent-destructive-commands",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Unconditionally blocks destructive bash commands before execution. A pi extension that guards against rm, git reset --hard, docker rm, aws delete operations, and more.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -32,7 +32,7 @@
32
32
  "node": ">=18.0.0"
33
33
  },
34
34
  "scripts": {
35
- "test": "npm run test:smoke && npm run test:heredoc && npm run test:migration-guard && npm run test:nx-guard && npm run test:project-config && npm run test:find-start-path && npm run test:git-guards-command && npm run test:gh-guard && npm run test:e2e",
35
+ "test": "npm run test:smoke && npm run test:heredoc && npm run test:migration-guard && npm run test:nx-guard && npm run test:project-config && npm run test:find-start-path && npm run test:git-guards-command && npm run test:read-only-command && npm run test:gh-guard && npm run test:comment-guard && npm run test:e2e",
36
36
  "test:smoke": "npx tsx test/smoke-test.ts",
37
37
  "test:heredoc": "npx tsx test/heredoc-test.ts",
38
38
  "test:e2e": "npx tsx test/e2e-install-test.ts",
@@ -41,7 +41,9 @@
41
41
  "test:project-config": "npx tsx test/project-config-test.ts",
42
42
  "test:find-start-path": "npx tsx test/find-start-path-test.ts",
43
43
  "test:git-guards-command": "npx tsx test/git-guards-command-test.ts",
44
+ "test:read-only-command": "npx tsx test/read-only-command-test.ts",
44
45
  "test:gh-guard": "npx tsx test/gh-guard-test.ts",
46
+ "test:comment-guard": "npx tsx test/comment-guard-test.ts",
45
47
  "lint": "tsc --noEmit",
46
48
  "typecheck": "tsc --noEmit"
47
49
  },
package/src/checker.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { tokenize } from "./tokenizer";
12
+ import { forEachPositionalArg } from "./rules/args";
12
13
  import {
13
14
  DELEGATION_COMMANDS,
14
15
  FILE_READING_COMMANDS,
@@ -186,18 +187,15 @@ export function checkTokens(
186
187
 
187
188
  // Quoted-command wrappers: the first positional argument is a command
188
189
  if (QUOTED_COMMAND_WRAPPERS.has(token)) {
189
- let j = i + 1;
190
- while (j < tokens.length) {
191
- const arg = tokens[j];
192
- if (!arg || SHELL_OPERATORS.has(arg)) break;
193
- if (arg.startsWith("-")) {
194
- j++;
195
- continue;
196
- }
190
+ let dangerous: CheckResult | undefined;
191
+ forEachPositionalArg(tokens, i, (arg) => {
197
192
  const r = checkCommand(arg, currentCwd ?? cwd, depth + 1, boundaryCwd, options);
198
- if (r.dangerous) return r;
199
- break;
200
- }
193
+ if (r.dangerous) {
194
+ dangerous = r;
195
+ return false;
196
+ }
197
+ });
198
+ if (dangerous) return dangerous;
201
199
  i++;
202
200
  continue;
203
201
  }