howtofixcli 0.7.7 → 0.7.9

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.
Files changed (152) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-3FYCFLNK.js +1 -0
  3. package/dist/chunk-56SEAGAQ.js +1 -0
  4. package/dist/chunk-63ZCQDXH.js +6 -0
  5. package/dist/chunk-6FUWMLWO.js +1 -0
  6. package/dist/chunk-7CHDNTH6.js +1 -0
  7. package/dist/chunk-ACOMNDUL.js +185 -0
  8. package/dist/chunk-AEVPET7X.js +2 -0
  9. package/dist/chunk-AGGIYXN7.js +2 -0
  10. package/dist/chunk-BHVBO4SC.js +1 -0
  11. package/dist/chunk-FPXVDP32.js +8 -0
  12. package/dist/chunk-GIUYIKH3.js +109 -0
  13. package/dist/chunk-GQCAJ56I.js +5 -0
  14. package/dist/chunk-GXB7RZHH.js +1 -0
  15. package/dist/chunk-IGEY3AWF.js +1 -0
  16. package/dist/chunk-ILDDDLRC.js +1 -0
  17. package/dist/chunk-JFYVRWJI.js +2 -0
  18. package/dist/chunk-MVLCGU3G.js +1 -0
  19. package/dist/chunk-N345KWWK.js +1 -0
  20. package/dist/chunk-NBQFPCRT.js +1 -0
  21. package/dist/chunk-OIPSLIZE.js +5 -0
  22. package/dist/chunk-ORJ2QZBG.js +3 -0
  23. package/dist/chunk-PCALBZOO.js +2 -0
  24. package/dist/chunk-PDNBGUYP.js +2 -0
  25. package/dist/chunk-S656RE4A.js +1 -0
  26. package/dist/chunk-TSEKPGK4.js +1 -0
  27. package/dist/chunk-U6CI6KXN.js +34 -0
  28. package/dist/chunk-UTYFLLSM.js +1 -0
  29. package/dist/chunk-V3R4FYTJ.js +1 -0
  30. package/dist/chunk-V656XVXV.js +8 -0
  31. package/dist/chunk-WKUJ7CFO.js +1 -0
  32. package/dist/chunk-WOT6VMZA.js +1 -0
  33. package/dist/chunk-WUPU6LTD.js +4 -0
  34. package/dist/chunk-WX3V5P7U.js +5 -0
  35. package/dist/chunk-ZKZ6WCI3.js +2 -0
  36. package/dist/main.js +94 -536
  37. package/package.json +15 -62
  38. package/dist/account-credential.js +0 -136
  39. package/dist/account-credential.test.js +0 -38
  40. package/dist/activity.js +0 -124
  41. package/dist/agent-defs.js +0 -237
  42. package/dist/agent-limiter.js +0 -68
  43. package/dist/agents.js +0 -554
  44. package/dist/anim.js +0 -361
  45. package/dist/args.js +0 -228
  46. package/dist/at-file.js +0 -28
  47. package/dist/brand-extension.js +0 -87
  48. package/dist/brand.js +0 -76
  49. package/dist/browser.js +0 -58
  50. package/dist/cache-report.js +0 -139
  51. package/dist/checkpoint.js +0 -437
  52. package/dist/claims.js +0 -167
  53. package/dist/cli-auth.js +0 -142
  54. package/dist/cli-checkin.js +0 -116
  55. package/dist/cli-credentials.js +0 -254
  56. package/dist/clipboard.js +0 -149
  57. package/dist/collapse.js +0 -129
  58. package/dist/commands.js +0 -301
  59. package/dist/compaction-guard.js +0 -81
  60. package/dist/context-extension.js +0 -17
  61. package/dist/dcp/commands.js +0 -172
  62. package/dist/dcp/compress-state.js +0 -139
  63. package/dist/dcp/compress.js +0 -329
  64. package/dist/dcp/dcp-extension.js +0 -288
  65. package/dist/dcp/dedup.js +0 -93
  66. package/dist/dcp/ids.js +0 -189
  67. package/dist/dcp/index.js +0 -25
  68. package/dist/dcp/prompts.js +0 -68
  69. package/dist/dcp/protected-content.js +0 -221
  70. package/dist/dcp/protected.js +0 -161
  71. package/dist/dcp/prune.js +0 -418
  72. package/dist/dcp/state.js +0 -176
  73. package/dist/device-id.js +0 -90
  74. package/dist/device-id.test.js +0 -23
  75. package/dist/errors.js +0 -63
  76. package/dist/extensions.js +0 -48
  77. package/dist/fatal.js +0 -35
  78. package/dist/feed.js +0 -498
  79. package/dist/fs-atomic.js +0 -54
  80. package/dist/gateway-errors.js +0 -90
  81. package/dist/git.js +0 -230
  82. package/dist/goal-extension.js +0 -88
  83. package/dist/goal.js +0 -222
  84. package/dist/hooks-extension.js +0 -176
  85. package/dist/hooks.js +0 -443
  86. package/dist/howtofix-account.js +0 -715
  87. package/dist/hud-line.js +0 -153
  88. package/dist/hwid.js +0 -130
  89. package/dist/interrupt.js +0 -60
  90. package/dist/json-events.js +0 -50
  91. package/dist/level-style.js +0 -112
  92. package/dist/loop-guard.js +0 -95
  93. package/dist/lsp-tools.js +0 -32
  94. package/dist/lsp.js +0 -347
  95. package/dist/mcp-config.js +0 -85
  96. package/dist/mcp-lifecycle.js +0 -147
  97. package/dist/model-catalog.js +0 -217
  98. package/dist/model-resolve.js +0 -65
  99. package/dist/notes.js +0 -78
  100. package/dist/oauth-login.js +0 -568
  101. package/dist/paths.js +0 -61
  102. package/dist/permissions.js +0 -298
  103. package/dist/plan-mode.js +0 -363
  104. package/dist/preset-command.js +0 -227
  105. package/dist/preset-extension.js +0 -35
  106. package/dist/preset-mcp.js +0 -292
  107. package/dist/presets.js +0 -727
  108. package/dist/prompt-files.js +0 -103
  109. package/dist/prompt-text.js +0 -41
  110. package/dist/proxy.js +0 -137
  111. package/dist/quality/ast.js +0 -196
  112. package/dist/quality/cognitive.js +0 -83
  113. package/dist/quality/cyclomatic.js +0 -30
  114. package/dist/quality/halstead.js +0 -83
  115. package/dist/quality/index.js +0 -18
  116. package/dist/quality/length.js +0 -3
  117. package/dist/quality/maintainability.js +0 -15
  118. package/dist/quality/nesting.js +0 -57
  119. package/dist/quality/report.js +0 -803
  120. package/dist/quality/smells.js +0 -67
  121. package/dist/quality-tool.js +0 -63
  122. package/dist/read-only-commands.js +0 -597
  123. package/dist/repo-map.js +0 -292
  124. package/dist/rtk.js +0 -193
  125. package/dist/sandbox.js +0 -105
  126. package/dist/schema.js +0 -214
  127. package/dist/session-lifecycle.js +0 -19
  128. package/dist/session-retention.js +0 -111
  129. package/dist/session.js +0 -446
  130. package/dist/sessions.js +0 -100
  131. package/dist/settings.js +0 -148
  132. package/dist/skills.js +0 -35
  133. package/dist/slop-tool.js +0 -96
  134. package/dist/spend-cap.js +0 -43
  135. package/dist/subagent.js +0 -171
  136. package/dist/tar.js +0 -146
  137. package/dist/theme.js +0 -79
  138. package/dist/thinking.js +0 -45
  139. package/dist/todo.js +0 -114
  140. package/dist/tool-loadout.js +0 -97
  141. package/dist/tool-render.js +0 -769
  142. package/dist/tools.js +0 -176
  143. package/dist/ui.js +0 -4751
  144. package/dist/update.js +0 -533
  145. package/dist/usage-segment.js +0 -69
  146. package/dist/web-tools.js +0 -46
  147. package/dist/web.js +0 -265
  148. package/dist/welcome.js +0 -179
  149. package/dist/workflow-engine.js +0 -246
  150. package/dist/workflow-progress.js +0 -446
  151. package/dist/workflow-view.js +0 -195
  152. package/dist/workflow.js +0 -349
@@ -1,597 +0,0 @@
1
- /**
2
- * Which shell commands are read-only, ported from Claude Code's bash classifier.
3
- *
4
- * The predecessor of this file was a single loose regex:
5
- *
6
- * /^(?:cd\s+\S+\s*&&\s*)?(?:ls|cat|…|git\s+(?:status|diff|log|show|branch|remote))\b/
7
- *
8
- * which was wrong in both directions, and both directions cost the user:
9
- *
10
- * 1. **Too strict.** Any `|`, `;`, `&`, `<`, `>` or `$(` bailed out to "ask".
11
- * `ls | head`, `git log --oneline | head -20`, `grep -rn x src | wc -l` —
12
- * the bread and butter of reading a codebase — each raised a prompt. That is
13
- * the "it asks for permission just to look at my code" complaint.
14
- * 2. **Too loose.** `\b` after `git\s+(?:…|branch|remote)` matched the
15
- * subcommand only, so `git branch -D main` and `git remote remove origin`
16
- * were auto-allowed as read-only. Those are writes.
17
- *
18
- * The reference's model fixes both: a command is read-only only if **every**
19
- * segment of the compound is read-only, and a segment is read-only only if its
20
- * flags are all in a per-subcommand allowlist. That is what this file ports.
21
- *
22
- * Deliberately not ported: the git-hook sandbox-escape guards
23
- * (`cd X && git status` where X contains a malicious `hooks/pre-commit`). They
24
- * exist because Claude Code runs git inside a sandbox it wants to escape-proof.
25
- * The equivalent guard here is simpler and stricter — see `cdWithGit`.
26
- */
27
- // ---------------------------------------------------------------------------
28
- // Flag validation
29
- // ---------------------------------------------------------------------------
30
- const FLAG_PATTERN = /^-[a-zA-Z0-9_-]/;
31
- function flagArgumentIsValid(value, type) {
32
- switch (type) {
33
- case "number":
34
- return /^\d+$/.test(value);
35
- case "string":
36
- return true;
37
- default:
38
- return false;
39
- }
40
- }
41
- /**
42
- * Expand a short-flag cluster or an attached value: `-d:`, `-f1`, `-nr`, `-U3`.
43
- *
44
- * The allowlist is written one flag per entry, but a user types the getopt
45
- * forms: `cut -d: -f1` attaches the value to the flag, and `sort -nr` clusters
46
- * two `none` flags. Without this, both read as an unknown flag and every one of
47
- * them raises an approval prompt — a read-only command asking permission, which
48
- * is the complaint this fixes.
49
- *
50
- * getopt semantics, and the reason it stays safe: a value-taking flag consumes
51
- * the **rest of its own token** and never reaches into the next one, so a
52
- * cluster cannot smuggle a value past the check.
53
- */
54
- function expandShortCluster(token, safeFlags) {
55
- if (!/^-[^-]/.test(token) || token.includes("="))
56
- return false;
57
- for (let index = 1; index < token.length; index++) {
58
- const type = safeFlags[`-${token[index]}`];
59
- if (!type)
60
- return false;
61
- if (type === "none")
62
- continue;
63
- return flagArgumentIsValid(token.slice(index + 1), type);
64
- }
65
- // Every character was a known flag that takes no value.
66
- return true;
67
- }
68
- /**
69
- * Walk a tokenized command's flags against a subcommand's allowlist.
70
- *
71
- * Positional arguments are accepted (they are usually paths or refs). A flag
72
- * that is not in `safeFlags` fails the whole check, which is what turns
73
- * `git branch -D main` into a prompt while `git branch -a` stays silent.
74
- */
75
- export function validateFlags(tokens, startIndex, config) {
76
- let index = startIndex;
77
- while (index < tokens.length) {
78
- const token = tokens[index];
79
- if (!token) {
80
- index++;
81
- continue;
82
- }
83
- if (token === "--")
84
- break; // Everything after `--` is a positional argument.
85
- if (FLAG_PATTERN.test(token)) {
86
- // git's count shorthand: `git log -5` means `--max-count=5`. It reads as
87
- // a flag to every other rule here, so it needs its own branch, and it is
88
- // only meaningful where the command already accepts `-n`.
89
- if (/^-\d+$/.test(token) && config.safeFlags["-n"]) {
90
- index++;
91
- continue;
92
- }
93
- // `--flag=value` is validated against the attached value, never by
94
- // consuming the next token: `-E=` with an empty value must not be able to
95
- // reach into the following argument.
96
- const hasEquals = token.includes("=");
97
- const [flag = "", ...valueParts] = token.split("=");
98
- const type = config.safeFlags[flag];
99
- if (!type) {
100
- // Not a flag on its own: try the getopt forms before refusing.
101
- if (!expandShortCluster(token, config.safeFlags))
102
- return false;
103
- index++;
104
- continue;
105
- }
106
- if (type === "none") {
107
- // A `none` flag must not carry a value in either form.
108
- if (hasEquals)
109
- return false;
110
- index++;
111
- continue;
112
- }
113
- // `optional` is git's PARSE_OPT_OPTARG: the value may be attached
114
- // (`--porcelain=v2`, `--abbrev=10`) but is never taken from the next
115
- // token, which would swallow a path or a ref.
116
- if (type === "optional") {
117
- index++;
118
- continue;
119
- }
120
- if (hasEquals) {
121
- if (!flagArgumentIsValid(valueParts.join("="), type))
122
- return false;
123
- index++;
124
- continue;
125
- }
126
- const next = tokens[index + 1];
127
- if (next === undefined || !flagArgumentIsValid(next, type))
128
- return false;
129
- index += 2;
130
- continue;
131
- }
132
- // A bare argument. Usually a path or a ref, but for an interpreter or a
133
- // package runner it names code to execute, so those reject it.
134
- if (config.allowPositional === false)
135
- return false;
136
- index++;
137
- }
138
- return true;
139
- }
140
- // ---------------------------------------------------------------------------
141
- // git
142
- // ---------------------------------------------------------------------------
143
- const GIT_REF_SELECTION = { "--all": "none", "--branches": "none", "--tags": "none", "--remotes": "none" };
144
- const GIT_LOG_DISPLAY = { "--oneline": "none", "--graph": "none", "--decorate": "none", "--no-decorate": "none", "--date": "string", "--relative-date": "none" };
145
- const GIT_COUNT = { "--max-count": "number", "-n": "number" };
146
- const GIT_STAT = { "--stat": "none", "--numstat": "none", "--shortstat": "none", "--name-only": "none", "--name-status": "none" };
147
- const GIT_COLOR = { "--color": "none", "--no-color": "none" };
148
- const GIT_PATCH = { "--patch": "none", "-p": "none", "--no-patch": "none", "--no-ext-diff": "none", "-s": "none" };
149
- const GIT_AUTHOR = { "--author": "string", "--committer": "string", "--grep": "string" };
150
- /** Flags that only ever *list*, shared by `branch` and `tag`. */
151
- const GIT_LIST_ONLY = { "-l": "none", "--list": "none", "-a": "none", "-r": "none", "-v": "none", "-vv": "none", "-i": "none" };
152
- /**
153
- * git subcommands that only read, with the flags each may take.
154
- *
155
- * `config` is deliberately `--get` only: `git config user.email x` writes the
156
- * config file, and the reference splits it into its own entry for that reason.
157
- */
158
- export const GIT_READ_ONLY = {
159
- "git status": { safeFlags: { "-s": "none", "--short": "none", "-b": "none", "--branch": "none", "-u": "optional", "--untracked-files": "optional", "--porcelain": "optional", "--ignored": "none", "--long": "none", "--no-renames": "none", ...GIT_COLOR } },
160
- "git diff": {
161
- safeFlags: {
162
- ...GIT_STAT,
163
- ...GIT_COLOR,
164
- ...GIT_PATCH,
165
- "--cached": "none",
166
- "--staged": "none",
167
- "--word-diff": "none",
168
- "--check": "none",
169
- "--quiet": "none",
170
- "--exit-code": "none",
171
- "--no-renames": "none",
172
- "--diff-filter": "string",
173
- "--relative": "string",
174
- "--full-index": "none",
175
- "--binary": "none",
176
- "--abbrev": "number",
177
- "--unified": "number",
178
- "-U": "number",
179
- "--ignore-all-space": "none",
180
- "--ignore-space-change": "none",
181
- "--ignore-blank-lines": "none",
182
- },
183
- },
184
- "git log": {
185
- safeFlags: {
186
- ...GIT_LOG_DISPLAY,
187
- ...GIT_COUNT,
188
- ...GIT_STAT,
189
- ...GIT_COLOR,
190
- ...GIT_PATCH,
191
- ...GIT_AUTHOR,
192
- ...GIT_REF_SELECTION,
193
- "--since": "string",
194
- "--after": "string",
195
- "--until": "string",
196
- "--before": "string",
197
- "--follow": "none",
198
- "--no-merges": "none",
199
- "--merges": "none",
200
- "--first-parent": "none",
201
- "--reverse": "none",
202
- "--skip": "number",
203
- "--all": "none",
204
- "--format": "string",
205
- "--pretty": "string",
206
- "--abbrev-commit": "none",
207
- "--no-abbrev-commit": "none",
208
- "--left-right": "none",
209
- },
210
- },
211
- "git show": { safeFlags: { ...GIT_STAT, ...GIT_COLOR, ...GIT_PATCH, ...GIT_LOG_DISPLAY, "--format": "string", "--pretty": "string", "--abbrev-commit": "none", "--no-patch": "none", "-q": "none" } },
212
- "git shortlog": { safeFlags: { ...GIT_COUNT, "-s": "none", "--summary": "none", "-n": "none", "--numbered": "none", "-e": "none", "--email": "none", "-sn": "none", "-ns": "none", "--all": "none" } },
213
- "git reflog": { safeFlags: { ...GIT_COUNT, ...GIT_LOG_DISPLAY, ...GIT_AUTHOR, "--all": "none", "--date": "string" } },
214
- "git blame": { safeFlags: { "-L": "string", "-w": "none", "-c": "none", "-n": "none", "-s": "none", "-e": "none", "-p": "none", "--porcelain": "none", "--line-porcelain": "none", "--date": "string", "--since": "string", "-C": "none", "-M": "none" } },
215
- "git ls-files": {
216
- safeFlags: {
217
- "-m": "none",
218
- "--modified": "none",
219
- "-o": "none",
220
- "--others": "none",
221
- "-i": "none",
222
- "--ignored": "none",
223
- "-s": "none",
224
- "--stage": "none",
225
- "-d": "none",
226
- "--deleted": "none",
227
- "-c": "none",
228
- "--cached": "none",
229
- "-u": "none",
230
- "--unmerged": "none",
231
- "--exclude-standard": "none",
232
- "--error-unmatch": "none",
233
- "-z": "none",
234
- },
235
- },
236
- "git ls-remote": { safeFlags: { "--heads": "none", "--tags": "none", "--refs": "none", "--symref": "none" } },
237
- "git rev-parse": {
238
- safeFlags: {
239
- "--short": "optional",
240
- "--abbrev-ref": "none",
241
- "--verify": "none",
242
- "--quiet": "none",
243
- "--git-dir": "none",
244
- "--show-toplevel": "none",
245
- "--show-cdup": "none",
246
- "--is-inside-work-tree": "none",
247
- "--is-bare-repository": "none",
248
- "--symbolic-full-name": "none",
249
- "--abbrev": "optional",
250
- },
251
- },
252
- "git rev-list": { safeFlags: { ...GIT_COUNT, "--count": "none", "--all": "none", "--objects": "none", "--reverse": "none", "--max-parents": "number", "--min-parents": "number", "--no-merges": "none", "--first-parent": "none", "--abbrev-commit": "none" } },
253
- "git describe": { safeFlags: { "--tags": "none", "--all": "none", "--long": "none", "--always": "none", "--dirty": "none", "--abbrev": "number", "--match": "string", "--exclude": "string", "--first-parent": "none" } },
254
- "git cat-file": { safeFlags: { "-p": "none", "-t": "none", "-s": "none", "-e": "none", "--batch-check": "none" } },
255
- "git for-each-ref": { safeFlags: { "--format": "string", "--count": "number", "--sort": "string", "--points-at": "string", "--merged": "string", "--no-merged": "string" } },
256
- "git grep": {
257
- safeFlags: {
258
- "-n": "none",
259
- "--line-number": "none",
260
- "-i": "none",
261
- "--ignore-case": "none",
262
- "-l": "none",
263
- "--files-with-matches": "none",
264
- "-c": "none",
265
- "--count": "none",
266
- "-w": "none",
267
- "--word-regexp": "none",
268
- "-E": "none",
269
- "--extended-regexp": "none",
270
- "-F": "none",
271
- "--fixed-strings": "none",
272
- "--cached": "none",
273
- "--untracked": "none",
274
- "-e": "string",
275
- "--and": "none",
276
- "--or": "none",
277
- "--not": "none",
278
- },
279
- },
280
- "git merge-base": { safeFlags: { "--all": "none", "--is-ancestor": "none", "--fork-point": "none", "--octopus": "none" } },
281
- "git stash list": { safeFlags: { ...GIT_LOG_DISPLAY } },
282
- "git worktree list": { safeFlags: { "--porcelain": "none", "-v": "none", "--verbose": "none", "-z": "none" } },
283
- "git branch": {
284
- safeFlags: { ...GIT_LIST_ONLY, "--merged": "string", "--no-merged": "string", "--contains": "string", "--no-contains": "string", "--sort": "string", "--format": "string", "--points-at": "string", "--column": "none", "--show-current": "none" },
285
- // A positional argument to `git branch` is not a filter, it is a *new
286
- // branch*: `git branch foo` writes `.git/refs/heads/foo`. Listing accepts
287
- // only ref filters, and those take a value, so any bare word is a write.
288
- dangerous: (args) => {
289
- const takesValue = new Set(["--merged", "--no-merged", "--contains", "--no-contains", "--sort", "--format", "--points-at"]);
290
- let expectValue = false;
291
- for (const arg of args) {
292
- if (expectValue) {
293
- expectValue = false;
294
- continue;
295
- }
296
- if (takesValue.has(arg)) {
297
- expectValue = true;
298
- continue;
299
- }
300
- if (!arg.startsWith("-"))
301
- return true; // A branch name being created.
302
- }
303
- return false;
304
- },
305
- },
306
- "git tag": {
307
- safeFlags: { "-l": "none", "--list": "none", "-n": "number", "--contains": "string", "--no-contains": "string", "--merged": "string", "--sort": "string", "--points-at": "string", "--column": "none" },
308
- // Same shape as `branch`: `git tag v1` creates `.git/refs/tags/v1`.
309
- dangerous: (args) => {
310
- const takesValue = new Set(["--contains", "--no-contains", "--merged", "--sort", "--points-at"]);
311
- let expectValue = false;
312
- for (const arg of args) {
313
- if (expectValue) {
314
- expectValue = false;
315
- continue;
316
- }
317
- if (takesValue.has(arg)) {
318
- expectValue = true;
319
- continue;
320
- }
321
- if (!arg.startsWith("-"))
322
- return true; // A tag name being created.
323
- }
324
- return false;
325
- },
326
- },
327
- "git remote": {
328
- safeFlags: { "-v": "none", "--verbose": "none" },
329
- // Every other `git remote` verb (`add`, `remove`, `rename`, `set-url`,
330
- // `prune`, `update`) changes the repository.
331
- dangerous: (args) => args.some((arg) => !arg.startsWith("-")),
332
- },
333
- "git config --get": { safeFlags: { "--local": "none", "--global": "none", "--system": "none", "--get-all": "none", "--get-regexp": "none", "--list": "none", "-l": "none", "--show-origin": "none", "--null": "none", "-z": "none" } },
334
- };
335
- // ---------------------------------------------------------------------------
336
- // Everything else
337
- // ---------------------------------------------------------------------------
338
- /**
339
- * Commands whose whole flag surface is read-only, so no flag list is needed.
340
- * `env` and `printenv` are deliberately absent: this process holds provider API
341
- * keys, and the reference removed them for exactly that reason.
342
- */
343
- export const READ_ONLY_COMMANDS = [
344
- "ls",
345
- "pwd",
346
- "cat",
347
- "head",
348
- "tail",
349
- "wc",
350
- "which",
351
- "whoami",
352
- "date",
353
- "echo",
354
- "sleep",
355
- "true",
356
- "false",
357
- "type",
358
- "test",
359
- "seq",
360
- "basename",
361
- "dirname",
362
- "realpath",
363
- "stat",
364
- "file",
365
- "du",
366
- "df",
367
- "tree",
368
- "uname",
369
- "hostname",
370
- "id",
371
- "groups",
372
- "tty",
373
- "cal",
374
- "uptime",
375
- "free",
376
- "locale",
377
- "printf",
378
- "grep",
379
- "rg",
380
- "fd",
381
- "jq",
382
- "bat",
383
- "column",
384
- "paste",
385
- "nl",
386
- "expand",
387
- "fold",
388
- "fmt",
389
- // Hashing and comparing read their input and write nothing to it: `md5sum`
390
- // and its sha siblings only print a digest, and `diff`/`cmp` only report a
391
- // difference. `xxd` is NOT here — its `-r` form writes a file — so it lives
392
- // in FLAGGED_READ_ONLY where the flag surface is checked.
393
- "md5sum",
394
- "sha1sum",
395
- "sha256sum",
396
- "sha512sum",
397
- "b2sum",
398
- "cksum",
399
- "diff",
400
- "cmp",
401
- ];
402
- /** Commands that read, but only with the right flags. */
403
- export const FLAGGED_READ_ONLY = {
404
- find: {
405
- safeFlags: {
406
- "-name": "string",
407
- "-iname": "string",
408
- "-type": "string",
409
- "-maxdepth": "number",
410
- "-mindepth": "number",
411
- "-newer": "string",
412
- "-path": "string",
413
- "-not": "none",
414
- "-print": "none",
415
- "-print0": "none",
416
- "-empty": "none",
417
- "-size": "string",
418
- "-mtime": "number",
419
- "-atime": "number",
420
- "-perm": "string",
421
- "-L": "none",
422
- "-H": "none",
423
- "-P": "none",
424
- },
425
- // `-delete`, `-exec`, `-execdir`, `-ok`, `-fprint`, `-fls` and `-fprintf`
426
- // all write or run code; the flag list above already rejects them, and this
427
- // states the intent so a future addition cannot reintroduce one.
428
- dangerous: (args) => args.some((arg) => /^-(delete|exec|execdir|ok|okdir|fprint0?|fls|fprintf)$/.test(arg)),
429
- },
430
- sort: { safeFlags: { "-n": "none", "-r": "none", "-u": "none", "-k": "string", "-t": "string", "-f": "none", "-h": "none", "-V": "none", "-s": "none", "-c": "none", "-m": "none", "-b": "none", "-z": "none" } },
431
- uniq: { safeFlags: { "-c": "none", "-d": "none", "-u": "none", "-i": "none", "-f": "number", "-s": "number", "-w": "number" } },
432
- cut: { safeFlags: { "-d": "string", "-f": "string", "-c": "string", "-b": "string", "-s": "none", "--complement": "none", "-z": "none" } },
433
- tr: { safeFlags: { "-d": "none", "-s": "none", "-c": "none" } },
434
- node: { safeFlags: { "-v": "none", "--version": "none" }, allowPositional: false },
435
- python: { safeFlags: { "--version": "none" }, allowPositional: false },
436
- python3: { safeFlags: { "--version": "none" }, allowPositional: false },
437
- npm: { safeFlags: { "--version": "none", "-v": "none" }, allowPositional: false },
438
- // `sed` and `awk` are programs, not filters: `sed -i` edits in place and a
439
- // `sed` script can `w` a file. Only the pure-stream forms are allowed, and
440
- // the script is checked for the commands that write.
441
- sed: {
442
- safeFlags: { "-n": "none", "--quiet": "none", "-e": "string", "-E": "none", "-r": "none", "--regexp-extended": "none", "-s": "none", "-u": "none", "-z": "none", "--posix": "none" },
443
- dangerous: (args) => args.some((arg) => /^-i|^--in-place/.test(arg) || /\b[we]\s/.test(arg) || /^[we]$/.test(arg)),
444
- },
445
- awk: {
446
- safeFlags: { "-F": "string", "-v": "string", "-f": "string" },
447
- dangerous: (args) => args.some((arg) => /\bsystem\s*\(|\|\s*"|>\s*"/.test(arg)),
448
- },
449
- // `xxd file` dumps hex and only reads. `xxd -r` is the reverse: it writes a
450
- // file from hex, so it must ask.
451
- xxd: {
452
- safeFlags: { "-l": "number", "-s": "string", "-c": "number", "-g": "number", "-p": "none", "-i": "none", "-u": "none", "-b": "none", "-e": "none", "-o": "number" },
453
- dangerous: (args) => args.some((arg) => arg === "-r" || /^-[a-z]*r/.test(arg)),
454
- },
455
- };
456
- /**
457
- * Multi-word commands that read only while they take no further argument:
458
- * `npm ls` lists, but `npm ls --json > f` and `docker ps --format …` are fine
459
- * too, so these are matched as subcommands with the same flag walk.
460
- */
461
- export const SUBCOMMAND_CONFIGS = {
462
- "npm ls": { safeFlags: { "--depth": "number", "--json": "none", "--long": "none", "--parseable": "none", "--all": "none", "--prod": "none", "--dev": "none", "--omit": "string", "--link": "none", "--global": "none", "-g": "none", "--silent": "none" } },
463
- "npm view": { safeFlags: { "--json": "none", "--registry": "string" } },
464
- "docker ps": { safeFlags: { "-a": "none", "--all": "none", "-q": "none", "--quiet": "none", "--no-trunc": "none", "--format": "string", "-n": "number", "--last": "number", "-s": "none", "--size": "none", "--filter": "string" } },
465
- "docker images": { safeFlags: { "-a": "none", "--all": "none", "-q": "none", "--quiet": "none", "--no-trunc": "none", "--format": "string", "--filter": "string", "--digests": "none" } },
466
- "docker inspect": { safeFlags: { "--format": "string", "-f": "string", "-s": "none", "--size": "none", "--type": "string" } },
467
- "kubectl get": {
468
- safeFlags: {
469
- "-o": "string",
470
- "--output": "string",
471
- "-n": "string",
472
- "--namespace": "string",
473
- "-A": "none",
474
- "--all-namespaces": "none",
475
- "--no-headers": "none",
476
- "-l": "string",
477
- "--selector": "string",
478
- "--show-labels": "none",
479
- "-w": "none",
480
- "--watch": "none",
481
- "--sort-by": "string",
482
- },
483
- },
484
- "kubectl describe": { safeFlags: { "-n": "string", "--namespace": "string", "-l": "string", "--selector": "string" } },
485
- "cargo tree": { safeFlags: { "--depth": "number", "-e": "string", "--edges": "string", "--no-dev-dependencies": "none", "--prefix": "string", "-i": "string", "--invert": "string", "-p": "string", "--package": "string" } },
486
- "go version": { safeFlags: {} },
487
- };
488
- /**
489
- * Commands that are read-only when run through `git`, but are shell built-ins
490
- * or paths that need no validation.
491
- */
492
- export const SHELL_BUILTINS = ["cd", "alias", "history", "arch", "ip addr", "ifconfig"];
493
- // ---------------------------------------------------------------------------
494
- // Compound commands
495
- // ---------------------------------------------------------------------------
496
- /** Split a compound command into its segments, or `undefined` if it cannot be
497
- * split safely. A `$(…)`, backtick or redirect anywhere makes the whole thing
498
- * non-read-only, because it can write or substitute a value. */
499
- export function splitCompound(command) {
500
- if (/\$\(|`|\$\{|>|</.test(command))
501
- return undefined;
502
- // `||` and `&&` and `;` and `|` all separate commands that each must be
503
- // read-only. `&` alone backgrounds, which is not a read of anything.
504
- if (/(?<!&)&(?!&)/.test(command))
505
- return undefined;
506
- return command
507
- .split(/\|\||&&|;|\|/)
508
- .map((segment) => segment.trim())
509
- .filter((segment) => segment.length > 0);
510
- }
511
- /** `cd X && git …` is refused: the git invocation would run in a directory the
512
- * caller chose, where a planted `hooks/pre-commit` executes. The reference
513
- * guards this because it sandboxes git; here the simpler answer is to ask. */
514
- export function cdWithGit(segments) {
515
- const hasCd = segments.some((segment) => /^cd\b/.test(segment));
516
- const hasGit = segments.some((segment) => /^git\b/.test(segment));
517
- return hasCd && hasGit;
518
- }
519
- /** Tokenize on whitespace, keeping quoted runs together. */
520
- export function tokenize(segment) {
521
- const tokens = [];
522
- const pattern = /'[^']*'|"[^"]*"|\S+/g;
523
- for (const match of segment.match(pattern) ?? []) {
524
- tokens.push(match.length >= 2 && /^['"]/.test(match) ? match.slice(1, -1) : match);
525
- }
526
- return tokens;
527
- }
528
- /** The longest command name a segment starts with, from a set of candidates. */
529
- function matchCommand(tokens) {
530
- if (tokens.length === 0)
531
- return undefined;
532
- const [first = "", second = ""] = tokens;
533
- const twoWord = `${first} ${second}`;
534
- // `git config --get` is three words, and is the only read-only config form.
535
- // `-l`/`--list` are the same read: they print the config instead of writing it.
536
- if (first === "git" && second === "config") {
537
- const verb = tokens[2];
538
- const reads = verb === "--get" || verb === "--get-all" || verb === "--get-regexp" || verb === "-l" || verb === "--list";
539
- return reads ? { name: "git config --get", rest: tokens.slice(3) } : undefined;
540
- }
541
- if (first === "git" && second === "stash") {
542
- return tokens[2] === "list" ? { name: "git stash list", rest: tokens.slice(3) } : undefined;
543
- }
544
- if (first === "git" && second === "worktree") {
545
- return tokens[2] === "list" ? { name: "git worktree list", rest: tokens.slice(3) } : undefined;
546
- }
547
- if (first === "git" && GIT_READ_ONLY[`git ${second}`])
548
- return { name: `git ${second}`, rest: tokens.slice(2) };
549
- if (SUBCOMMAND_CONFIGS[twoWord])
550
- return { name: twoWord, rest: tokens.slice(2) };
551
- return { name: first, rest: tokens.slice(1) };
552
- }
553
- /** Commands whose output is exactly their own arguments, so they only read if
554
- * nothing in them can be expanded. */
555
- const ECHO_LIKE = new Set(["echo", "printf"]);
556
- /**
557
- * Is one segment read-only? The decision is per segment, so a pipe is fine as
558
- * long as both sides only read.
559
- */
560
- export function isReadOnlySegment(segment) {
561
- const tokens = tokenize(segment);
562
- const matched = matchCommand(tokens);
563
- if (!matched)
564
- return false;
565
- const { name, rest } = matched;
566
- // `echo $SECRET` prints whatever the variable holds — and this process holds
567
- // provider API keys in its environment. Claude Code removed `env`/`printenv`
568
- // and rejects `$` in `echo` for the same reason: printing is a read of the
569
- // process, not of the workspace.
570
- if (ECHO_LIKE.has(name) && rest.some((token) => token.includes("$") || token.includes("`")))
571
- return false;
572
- const config = GIT_READ_ONLY[name] ?? FLAGGED_READ_ONLY[name] ?? SUBCOMMAND_CONFIGS[name];
573
- if (config) {
574
- if (config.dangerous?.(rest))
575
- return false;
576
- return validateFlags(rest, 0, config);
577
- }
578
- if (READ_ONLY_COMMANDS.includes(name))
579
- return true;
580
- if (SHELL_BUILTINS.includes(name))
581
- return true;
582
- return false;
583
- }
584
- /**
585
- * The whole check: every segment of the compound must be read-only.
586
- *
587
- * `ls | head` → both segments read → true. `ls | tee out` → `tee` is not in any
588
- * list → false, so it prompts.
589
- */
590
- export function isReadOnlyCommand(command) {
591
- const segments = splitCompound(command);
592
- if (!segments || segments.length === 0)
593
- return false;
594
- if (cdWithGit(segments))
595
- return false;
596
- return segments.every(isReadOnlySegment);
597
- }