praxis-agent 0.11.0 → 0.12.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 CHANGED
@@ -87,7 +87,7 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
87
87
  `/rewind`, runtime `/cd`, transcript-free
88
88
  `/btw` side questions with background-Agent handoff, interactive
89
89
  `/background` terminal handoff, unified `/status`/`/config`/`/usage` settings
90
- tabs, `/mcp`, `/memory` shared
90
+ tabs, `/sandbox` mode/dependency/override/config controls, `/mcp`, `/memory` shared
91
91
  instruction and auto-memory access, and live extension-reload controls,
92
92
  cursor/history composer, per-session model/effort/permission controls,
93
93
  context/status/skill/task dashboards, prompt stash and continuation shortcuts,
@@ -121,6 +121,10 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
121
121
  constraints, internal auto-memory/session/task path handling, compound `cd` +
122
122
  Git protection, mode-ordered `acceptEdits` handling, live raw/resolved-path
123
123
  grants for external directories,
124
+ Claude-compatible opt-in Bash sandboxing with filesystem and network
125
+ isolation, explicit ask/deny precedence, sandbox-only auto-allow,
126
+ write-allowlist/deny-within-allow enforcement, per-command overrides and
127
+ exclusions, violation reporting, and bare-repository control-file cleanup,
124
128
  safe-property Skill auto-allow, interactive
125
129
  workspace-directory add/remove controls, path confinement, credential
126
130
  redaction, and sanitized child processes.
@@ -18,6 +18,7 @@ import { loadTuiKeybindings, type TuiKeybindingsFile } from './tui/keybindings.j
18
18
  import { completeTuiWorkspaceDirectory, resolveTuiWorkspaceDirectory } from './tui/workspace-directories.js';
19
19
  import { type TuiThemeSettings } from './tui/theme.js';
20
20
  import { type CustomThemeBase, type CustomThemeToken, type TuiCustomTheme } from './tui/custom-themes.js';
21
+ import { type TuiSandboxStore } from './tui/sandbox-settings.js';
21
22
  import { type ConfigSettingsTarget } from './tui/config-settings.js';
22
23
  import { type PraxisRuntimeSettings } from './tui/runtime-settings.js';
23
24
  import { type TuiNotificationWriter } from './tui/terminal-notifications.js';
@@ -152,6 +153,7 @@ interface InteractiveAppProps {
152
153
  }): Promise<void>;
153
154
  remove?(rule: TuiPermissionRule): Promise<void>;
154
155
  };
156
+ sandboxStore?: TuiSandboxStore;
155
157
  recentlyDeniedStore?: RecentlyDeniedStore;
156
158
  themeStore?: {
157
159
  load(): Promise<TuiThemeSettings>;
@@ -174,7 +176,7 @@ interface InteractiveAppProps {
174
176
  notificationWriter?: TuiNotificationWriter;
175
177
  elicitationUrlOpener?: (url: string) => void | Promise<void>;
176
178
  }
177
- export declare function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener, }: InteractiveAppProps): import("react").JSX.Element;
179
+ export declare function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener, }: InteractiveAppProps): import("react").JSX.Element;
178
180
  export declare function runInteractive(options: {
179
181
  factory: InteractiveServiceFactory;
180
182
  initialPrompt?: string;
@@ -33,6 +33,8 @@ import { commitElicitationText, createTuiElicitationForm, elicitationFormIsValid
33
33
  import { openTuiUrl } from './tui/open-url.js';
34
34
  import { projectTuiToolPermission, ToolPermissionDialog, } from './tui/tool-permission.js';
35
35
  import { ConfigDashboard, projectConfigRows } from './tui/config-dashboard.js';
36
+ import { SandboxDashboard, tuiSandboxTabs } from './tui/sandbox-dashboard.js';
37
+ import { createTuiSandboxStore, } from './tui/sandbox-settings.js';
36
38
  import { loadConfigSettings, saveConfigSetting, configSettingDefinition, resolveConfigSettingsLocation, } from './tui/config-settings.js';
37
39
  import { projectRuntimeSettings, loadRuntimeSettings, } from './tui/runtime-settings.js';
38
40
  import { autoUpdateTarget, copyCandidates, externalEditorInitialContent, formatTurnDuration, questionTimeoutMilliseconds, sessionRecap, spinnerTip, shouldShowCopyPicker, } from './tui/runtime-interactions.js';
@@ -232,7 +234,18 @@ function ordinal(value) {
232
234
  return `${value}th`;
233
235
  }
234
236
  }
235
- export function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory = [], signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader = (configRoot, cwd) => loadTuiMemoryFiles({ configRoot, cwd }), memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener = openTuiUrl, }) {
237
+ const HIDDEN_TUI_SLASH_COMMANDS = new Set([
238
+ 'background',
239
+ 'new',
240
+ 'output-style',
241
+ 'reload-skills',
242
+ 'sessions',
243
+ 'skill',
244
+ 'terminal-setup',
245
+ 'update',
246
+ 'usage',
247
+ ]);
248
+ export function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory = [], signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader = (configRoot, cwd) => loadTuiMemoryFiles({ configRoot, cwd }), memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener = openTuiUrl, }) {
236
249
  const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
237
250
  const width = useTerminalWidth(terminalWidth);
238
251
  const keybindingsRoot = useMemo(() => resolve(keybindingsConfigRoot ??
@@ -369,6 +382,18 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
369
382
  permissionMode: permissionMode(display.permissionMode),
370
383
  additionalDirectories: [...new Set(additionalDirectories)],
371
384
  }));
385
+ const sandboxStore = useMemo(() => suppliedSandboxStore ??
386
+ createTuiSandboxStore({
387
+ configRoot: keybindingsRoot,
388
+ cwd: runtimeCwd,
389
+ homeDirectory: homedir(),
390
+ additionalDirectories: runtimePreferences.additionalDirectories,
391
+ }), [
392
+ keybindingsRoot,
393
+ runtimeCwd,
394
+ runtimePreferences.additionalDirectories,
395
+ suppliedSandboxStore,
396
+ ]);
372
397
  const runtimePreferencesRef = useRef(runtimePreferences);
373
398
  const [permission, setPermission] = useState(null);
374
399
  const permissionRef = useRef(null);
@@ -3157,6 +3182,62 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
3157
3182
  }
3158
3183
  return;
3159
3184
  }
3185
+ if (earlyMenu?.kind === 'sandbox') {
3186
+ if (key.escape || value === '\u001B') {
3187
+ updateMenu(null);
3188
+ return;
3189
+ }
3190
+ const tabs = tuiSandboxTabs(earlyMenu.snapshot);
3191
+ if (key.leftArrow || key.rightArrow || key.tab) {
3192
+ const index = tabs.indexOf(earlyMenu.tab);
3193
+ const direction = key.leftArrow || (key.tab && key.shift) ? -1 : 1;
3194
+ const tab = tabs[Math.max(0, Math.min(tabs.length - 1, index + direction))] ??
3195
+ earlyMenu.tab;
3196
+ updateMenu({ ...earlyMenu, tab, selectedIndex: 0 });
3197
+ return;
3198
+ }
3199
+ const rowCount = earlyMenu.tab === 'mode'
3200
+ ? 3
3201
+ : earlyMenu.tab === 'overrides' && earlyMenu.snapshot.settings.enabled
3202
+ ? 2
3203
+ : 0;
3204
+ if (rowCount > 0 && (key.upArrow || key.downArrow)) {
3205
+ updateMenu({
3206
+ ...earlyMenu,
3207
+ selectedIndex: Math.max(0, Math.min(rowCount - 1, earlyMenu.selectedIndex + (key.upArrow ? -1 : 1))),
3208
+ });
3209
+ return;
3210
+ }
3211
+ if (rowCount > 0 && key.return) {
3212
+ const saving = (async () => {
3213
+ setBusy(true);
3214
+ try {
3215
+ const snapshot = earlyMenu.tab === 'mode'
3216
+ ? await sandboxStore.setMode(['auto-allow', 'regular', 'disabled'][earlyMenu.selectedIndex] ?? 'auto-allow')
3217
+ : await sandboxStore.setAllowUnsandboxedCommands(earlyMenu.selectedIndex === 0);
3218
+ await retireService();
3219
+ const tabs = tuiSandboxTabs(snapshot);
3220
+ updateMenu({
3221
+ kind: 'sandbox',
3222
+ tab: tabs.includes(earlyMenu.tab)
3223
+ ? earlyMenu.tab
3224
+ : (tabs[0] ?? 'dependencies'),
3225
+ selectedIndex: earlyMenu.selectedIndex,
3226
+ snapshot,
3227
+ });
3228
+ }
3229
+ catch (error) {
3230
+ warn(error);
3231
+ }
3232
+ finally {
3233
+ setBusy(false);
3234
+ }
3235
+ })();
3236
+ onTurnChange?.(saving);
3237
+ void saving.finally(() => onTurnChange?.(null));
3238
+ }
3239
+ return;
3240
+ }
3160
3241
  if (earlyMenu?.kind === 'config') {
3161
3242
  if (key.escape || value === '\u001B') {
3162
3243
  if (earlyMenu.searchFocused && earlyMenu.query) {
@@ -4673,9 +4754,13 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
4673
4754
  commandNameQuery === selectedCommand.name.toLocaleLowerCase();
4674
4755
  const selectedCommandMatchesName = selectedCommand !== undefined &&
4675
4756
  selectedCommand.name.toLocaleLowerCase().startsWith(commandNameQuery);
4757
+ const exactHiddenCommand = HIDDEN_TUI_SLASH_COMMANDS.has(commandNameQuery);
4676
4758
  if (selectedCommand &&
4677
4759
  (key.tab ||
4678
- (key.return && !exactSelectedCommand && selectedCommandMatchesName))) {
4760
+ (key.return &&
4761
+ !exactSelectedCommand &&
4762
+ !exactHiddenCommand &&
4763
+ selectedCommandMatchesName))) {
4679
4764
  updateComposerInput(`/${selectedCommand.name} `);
4680
4765
  setCommandPaletteOpen(false);
4681
4766
  return;
@@ -4702,6 +4787,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
4702
4787
  if (!prompt || prompt === '!')
4703
4788
  return;
4704
4789
  const copyCommand = /^\/copy(?:\s+(\d+))?$/u.exec(prompt);
4790
+ const sandboxCommand = /^\/sandbox(?:\s+([\s\S]+))?$/u.exec(prompt);
4705
4791
  const tuiCommand = /^\/tui(?:\s+(default|fullscreen))?$/u.exec(prompt);
4706
4792
  const renameCommand = /^\/rename(?:\s+(.+))?$/u.exec(prompt);
4707
4793
  const cdCommand = /^\/cd(?:\s+(.+))?$/u.exec(prompt);
@@ -4883,6 +4969,52 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
4883
4969
  append({ kind: 'user', text: prompt });
4884
4970
  updateMenu({ kind: 'export', selectedIndex: 0 });
4885
4971
  }
4972
+ else if (sandboxCommand) {
4973
+ const operation = (async () => {
4974
+ setBusy(true);
4975
+ try {
4976
+ const args = sandboxCommand[1]?.trim();
4977
+ if (args) {
4978
+ const exclude = /^exclude(?:\s+([\s\S]+))?$/u.exec(args);
4979
+ if (!exclude) {
4980
+ throw new Error(`Unknown subcommand "${args.split(/\s+/u)[0] ?? args}". Available subcommand: exclude`);
4981
+ }
4982
+ if (!exclude[1]?.trim()) {
4983
+ throw new Error('Please provide a command pattern to exclude (e.g. /sandbox exclude "npm run test:*")');
4984
+ }
4985
+ const result = await sandboxStore.exclude(exclude[1]);
4986
+ await retireService();
4987
+ append({
4988
+ kind: 'local-result',
4989
+ text: `Added "${result.pattern}" to excluded commands in ${result.settingsPath}`,
4990
+ });
4991
+ return;
4992
+ }
4993
+ const snapshot = await sandboxStore.load();
4994
+ if (!snapshot.supported) {
4995
+ throw new Error('Sandboxing is currently only supported on macOS, Linux, and WSL2.');
4996
+ }
4997
+ if (snapshot.unavailableReason?.includes('not in sandbox.enabledPlatforms')) {
4998
+ throw new Error(snapshot.unavailableReason);
4999
+ }
5000
+ const tabs = tuiSandboxTabs(snapshot);
5001
+ updateMenu({
5002
+ kind: 'sandbox',
5003
+ tab: tabs[0] ?? 'dependencies',
5004
+ selectedIndex: 0,
5005
+ snapshot,
5006
+ });
5007
+ }
5008
+ catch (error) {
5009
+ warn(error);
5010
+ }
5011
+ finally {
5012
+ setBusy(false);
5013
+ }
5014
+ })();
5015
+ onTurnChange?.(operation);
5016
+ void operation.finally(() => onTurnChange?.(null));
5017
+ }
4886
5018
  else if (prompt === '/permissions') {
4887
5019
  const loading = (async () => {
4888
5020
  setBusy(true);
@@ -5197,7 +5329,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
5197
5329
  ? `restore ${menu.point.fileChanges.join(', ')}.`
5198
5330
  : 'be unchanged.'] }), _jsx(Text, { children: " " }), rewindActions(menu.point).map((option, index) => (_jsxs(Text, { inverse: menu.selectedIndex === index, children: [menu.selectedIndex === index ? '❯ ' : ' ', index + 1, ". ", option.label] }, option.action))), _jsx(Text, { children: " " }), _jsx(Text, { color: activePalette.warning, children: "\u26A0 Rewinding does not affect files edited manually or via bash." })] })) : menu?.kind === 'rewind-context' ? (_jsxs(DialogFrame, { title: `Summarize ${menu.direction === 'from' ? 'from' : 'up to'} here`, screenReader: axScreenReader, children: [_jsx(Text, { children: menu.direction === 'from'
5199
5331
  ? 'Messages after this point will be summarized.'
5200
- : 'Messages up to this point will be summarized.' }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["add context (optional): ", input] }), _jsx(Text, { dimColor: true, children: "Enter to summarize \u00B7 Esc to go back" })] })) : menu?.kind === 'permission-rule-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Add ", menu.behavior, " permission rule"] }), _jsx(Text, { children: "Permission rules are a tool name, optionally followed by a specifier in parentheses." }), _jsx(Text, { children: "e.g., WebFetch or Bash(ls *)" }), _jsx(Text, { children: " " }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Enter permission rule…' }) }), _jsx(Text, { dimColor: true, children: "Enter to submit \u00B7 Esc to cancel" })] })) : menu?.kind === 'workspace-directory-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Add directory to workspace" }), _jsx(Text, { children: "Praxis Code will be able to read files in this directory and make edits when auto-accept edits is on." }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter the path to the directory:" }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Directory path…' }) }), _jsx(Text, { dimColor: true, children: "Tab to complete \u00B7 Enter to add \u00B7 Esc to cancel" })] })) : menu?.kind === 'permission-delete' ? (_jsxs(DialogFrame, { title: permissionDeleteTitle(menu.rule.behavior), screenReader: axScreenReader, children: [_jsx(Text, { children: menu.rule.rule }), permissionRuleDescription(menu.rule.rule) ? (_jsx(Text, { dimColor: true, children: permissionRuleDescription(menu.rule.rule) })) : null, _jsx(Text, { dimColor: true, children: permissionScopeLabel(menu.rule.scope) }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Are you sure you want to delete this permission rule?" }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Esc to cancel" })] })) : menu?.kind === 'workspace-directory-delete' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Remove directory from workspace?" }), _jsxs(Text, { children: [" ", menu.path] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Praxis Code will no longer have access to files in this directory." }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to cancel" })] })) : menu ? (menu.kind === 'help' ? (_jsx(HelpMenu, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, builtinCommands: builtinSlashCommands, customCommands: customSlashCommands, width: width, screenReader: axScreenReader })) : menu.kind === 'diff' ? (_jsx(DiffDashboard, { snapshots: menu.snapshots, sourceIndex: menu.sourceIndex, selectedIndex: menu.selectedIndex, viewingFile: menu.viewingFile, scrollOffset: menu.scrollOffset, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-dashboard' ? (_jsx(PermissionDashboard, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, query: menu.query, rules: menu.rules, recentDenied: recentDenied, retryingDeniedId: retryingDeniedId, workspaceDirectories: workspaceDirectories, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-scope' ? (_jsx(SelectionMenu, { title: "Where should this rule be saved?", description: `${menu.rule}${permissionRuleDescription(menu.rule)
5332
+ : 'Messages up to this point will be summarized.' }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["add context (optional): ", input] }), _jsx(Text, { dimColor: true, children: "Enter to summarize \u00B7 Esc to go back" })] })) : menu?.kind === 'permission-rule-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Add ", menu.behavior, " permission rule"] }), _jsx(Text, { children: "Permission rules are a tool name, optionally followed by a specifier in parentheses." }), _jsx(Text, { children: "e.g., WebFetch or Bash(ls *)" }), _jsx(Text, { children: " " }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Enter permission rule…' }) }), _jsx(Text, { dimColor: true, children: "Enter to submit \u00B7 Esc to cancel" })] })) : menu?.kind === 'workspace-directory-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Add directory to workspace" }), _jsx(Text, { children: "Praxis Code will be able to read files in this directory and make edits when auto-accept edits is on." }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter the path to the directory:" }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Directory path…' }) }), _jsx(Text, { dimColor: true, children: "Tab to complete \u00B7 Enter to add \u00B7 Esc to cancel" })] })) : menu?.kind === 'permission-delete' ? (_jsxs(DialogFrame, { title: permissionDeleteTitle(menu.rule.behavior), screenReader: axScreenReader, children: [_jsx(Text, { children: menu.rule.rule }), permissionRuleDescription(menu.rule.rule) ? (_jsx(Text, { dimColor: true, children: permissionRuleDescription(menu.rule.rule) })) : null, _jsx(Text, { dimColor: true, children: permissionScopeLabel(menu.rule.scope) }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Are you sure you want to delete this permission rule?" }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Esc to cancel" })] })) : menu?.kind === 'workspace-directory-delete' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Remove directory from workspace?" }), _jsxs(Text, { children: [" ", menu.path] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Praxis Code will no longer have access to files in this directory." }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to cancel" })] })) : menu ? (menu.kind === 'help' ? (_jsx(HelpMenu, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, builtinCommands: builtinSlashCommands, customCommands: customSlashCommands, width: width, screenReader: axScreenReader })) : menu.kind === 'diff' ? (_jsx(DiffDashboard, { snapshots: menu.snapshots, sourceIndex: menu.sourceIndex, selectedIndex: menu.selectedIndex, viewingFile: menu.viewingFile, scrollOffset: menu.scrollOffset, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-dashboard' ? (_jsx(PermissionDashboard, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, query: menu.query, rules: menu.rules, recentDenied: recentDenied, retryingDeniedId: retryingDeniedId, workspaceDirectories: workspaceDirectories, width: width, screenReader: axScreenReader })) : menu.kind === 'sandbox' ? (_jsx(SandboxDashboard, { snapshot: menu.snapshot, tab: menu.tab, selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-scope' ? (_jsx(SelectionMenu, { title: "Where should this rule be saved?", description: `${menu.rule}${permissionRuleDescription(menu.rule)
5201
5333
  ? ` · ${permissionRuleDescription(menu.rule)}`
5202
5334
  : ''}`, options: [
5203
5335
  {
@@ -0,0 +1,10 @@
1
+ import type { TuiSandboxSnapshot, TuiSandboxTab } from './sandbox-settings.js';
2
+ export declare function tuiSandboxTabs(snapshot: TuiSandboxSnapshot): readonly TuiSandboxTab[];
3
+ export declare function SandboxDashboard({ snapshot, tab, selectedIndex, width, screenReader, }: {
4
+ snapshot: TuiSandboxSnapshot;
5
+ tab: TuiSandboxTab;
6
+ selectedIndex: number;
7
+ width: number;
8
+ screenReader: boolean;
9
+ }): import("react").JSX.Element;
10
+ //# sourceMappingURL=sandbox-dashboard.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const TABS = {
4
+ mode: 'Mode',
5
+ dependencies: 'Dependencies',
6
+ overrides: 'Overrides',
7
+ config: 'Config',
8
+ };
9
+ export function tuiSandboxTabs(snapshot) {
10
+ if (snapshot.dependencies.errors.length > 0)
11
+ return ['dependencies'];
12
+ return [
13
+ 'mode',
14
+ ...(snapshot.dependencies.warnings.length > 0
15
+ ? ['dependencies']
16
+ : []),
17
+ 'overrides',
18
+ 'config',
19
+ ];
20
+ }
21
+ function prefix(selected, screenReader) {
22
+ if (screenReader)
23
+ return selected ? 'Selected: ' : '';
24
+ return selected ? '❯ ' : ' ';
25
+ }
26
+ export function SandboxDashboard({ snapshot, tab, selectedIndex, width, screenReader, }) {
27
+ const tabs = tuiSandboxTabs(snapshot);
28
+ const mode = !snapshot.settings.enabled
29
+ ? 'disabled'
30
+ : snapshot.settings.autoAllowBashIfSandboxed
31
+ ? 'auto-allow'
32
+ : 'regular';
33
+ const modeRows = [
34
+ {
35
+ value: 'auto-allow',
36
+ label: 'Sandbox BashTool, with auto-allow',
37
+ },
38
+ {
39
+ value: 'regular',
40
+ label: 'Sandbox BashTool, with regular permissions',
41
+ },
42
+ { value: 'disabled', label: 'No Sandbox' },
43
+ ];
44
+ const overrideRows = [
45
+ {
46
+ value: true,
47
+ label: 'Allow unsandboxed fallback',
48
+ },
49
+ {
50
+ value: false,
51
+ label: 'Strict sandbox mode',
52
+ },
53
+ ];
54
+ const filesystem = snapshot.settings.runtimeConfig.filesystem;
55
+ const network = snapshot.settings.runtimeConfig.network;
56
+ return (_jsxs(Box, { flexDirection: "column", width: Math.min(100, width), children: [!screenReader ? (_jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(100, width)) })) : null, _jsx(Text, { bold: true, children: " Sandbox:" }), _jsxs(Text, { children: [' ', tabs.map((candidate) => (_jsxs(Text, { inverse: !screenReader && candidate === tab, children: [screenReader && candidate === tab ? 'Current tab: ' : ' ', TABS[candidate], ' '] }, candidate)))] }), _jsx(Text, { children: " " }), tab === 'mode' ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: " Configure Mode:" }), _jsx(Text, { children: " " }), modeRows.map((row, index) => (_jsxs(Text, { inverse: !screenReader && selectedIndex === index, children: [prefix(selectedIndex === index, screenReader), index + 1, ". ", row.label, row.value === mode ? ' (current)' : ''] }, row.value))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Auto-allow mode runs Bash in the sandbox automatically. Explicit ask and deny rules are always respected." })] })) : tab === 'dependencies' ? (_jsxs(Box, { flexDirection: "column", children: [snapshot.platform === 'macos' ? (_jsxs(Text, { children: ["seatbelt: ", _jsx(Text, { color: "green", children: "built-in (macOS)" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["bubblewrap (bwrap):", ' ', _jsx(Text, { color: snapshot.dependencies.errors.some((error) => error.includes('bwrap'))
57
+ ? 'red'
58
+ : 'green', children: snapshot.dependencies.errors.some((error) => error.includes('bwrap'))
59
+ ? 'not installed'
60
+ : 'installed' })] }), _jsxs(Text, { children: ["socat:", ' ', _jsx(Text, { color: snapshot.dependencies.errors.some((error) => error.includes('socat'))
61
+ ? 'red'
62
+ : 'green', children: snapshot.dependencies.errors.some((error) => error.includes('socat'))
63
+ ? 'not installed'
64
+ : 'installed' })] }), _jsxs(Text, { children: ["seccomp filter:", ' ', _jsx(Text, { color: snapshot.dependencies.warnings.length > 0
65
+ ? 'yellow'
66
+ : 'green', children: snapshot.dependencies.warnings.length > 0
67
+ ? 'not installed'
68
+ : 'installed' })] })] })), _jsxs(Text, { children: ["ripgrep (rg):", ' ', _jsx(Text, { color: snapshot.dependencies.errors.some((error) => error.includes('ripgrep'))
69
+ ? 'red'
70
+ : 'green', children: snapshot.dependencies.errors.some((error) => error.includes('ripgrep'))
71
+ ? 'not found'
72
+ : 'found' })] }), snapshot.dependencies.errors.map((error) => (_jsx(Text, { color: "red", children: error }, error))), snapshot.dependencies.warnings.map((warning) => (_jsx(Text, { color: "yellow", children: warning }, warning))), snapshot.unavailableReason ? (_jsx(Text, { color: "red", children: snapshot.unavailableReason })) : null] })) : tab === 'overrides' ? (_jsxs(_Fragment, { children: [!snapshot.settings.enabled ? (_jsx(Text, { dimColor: true, children: "Sandbox is not enabled" })) : (overrideRows.map((row, index) => (_jsxs(Text, { inverse: !screenReader && selectedIndex === index, children: [prefix(selectedIndex === index, screenReader), index + 1, ". ", row.label, row.value === snapshot.settings.allowUnsandboxedCommands
73
+ ? ' (current)'
74
+ : ''] }, String(row.value))))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Closed mode ignores dangerouslyDisableSandbox and requires commands to remain sandboxed." })] })) : !snapshot.settings.enabled ? (_jsx(Text, { dimColor: true, children: "Sandbox is not enabled" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Excluded Commands:" }), _jsx(Text, { dimColor: true, children: snapshot.settings.excludedCommands.join(', ') || 'None' }), _jsx(Text, { bold: true, children: "Filesystem Read Restrictions:" }), _jsxs(Text, { dimColor: true, children: ["Denied: ", filesystem?.denyRead?.join(', ') || 'None'] }), _jsxs(Text, { dimColor: true, children: ["Allowed within denied: ", filesystem?.allowRead?.join(', ') || 'None'] }), _jsx(Text, { bold: true, children: "Filesystem Write Restrictions:" }), _jsxs(Text, { dimColor: true, children: ["Allowed: ", filesystem?.allowWrite?.join(', ') || 'None'] }), _jsxs(Text, { dimColor: true, children: ["Denied within allowed: ", filesystem?.denyWrite?.join(', ') || 'None'] }), _jsx(Text, { bold: true, children: "Network Restrictions:" }), _jsxs(Text, { dimColor: true, children: ["Allowed: ", network?.allowedDomains?.join(', ') || 'None'] }), _jsxs(Text, { dimColor: true, children: ["Denied: ", network?.deniedDomains?.join(', ') || 'None'] }), _jsx(Text, { bold: true, children: "Allowed Unix Sockets:" }), _jsx(Text, { dimColor: true, children: network?.allowUnixSockets?.join(', ') || 'None' }), (snapshot.globPatternWarnings?.length ?? 0) > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Warning: Glob patterns not fully supported on Linux" }), _jsx(Text, { dimColor: true, children: snapshot.globPatternWarnings?.slice(0, 3).join(', ') })] })) : null] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: tab === 'mode' || (tab === 'overrides' && snapshot.settings.enabled)
75
+ ? '↑/↓ navigate · Enter to select · ←/→ switch tabs · Esc to cancel'
76
+ : '←/→ switch tabs · Esc to cancel' })] }));
77
+ }
78
+ //# sourceMappingURL=sandbox-dashboard.js.map
@@ -0,0 +1,32 @@
1
+ import type { SandboxDependencyCheck } from '@anthropic-ai/sandbox-runtime';
2
+ import { loadClaudeSettings } from '../../compatibility/claude/shared-resources.js';
3
+ import { type ClaudeSandboxSettings } from '../../sandbox/claude-sandbox-settings.js';
4
+ export type TuiSandboxMode = 'auto-allow' | 'regular' | 'disabled';
5
+ export type TuiSandboxTab = 'mode' | 'dependencies' | 'overrides' | 'config';
6
+ export interface TuiSandboxSnapshot {
7
+ settings: ClaudeSandboxSettings;
8
+ dependencies: SandboxDependencyCheck;
9
+ supported: boolean;
10
+ platform: 'macos' | 'linux' | 'windows' | 'wsl';
11
+ globPatternWarnings?: readonly string[];
12
+ unavailableReason?: string;
13
+ }
14
+ export interface TuiSandboxStore {
15
+ load(): Promise<TuiSandboxSnapshot>;
16
+ setMode(mode: TuiSandboxMode): Promise<TuiSandboxSnapshot>;
17
+ setAllowUnsandboxedCommands(allow: boolean): Promise<TuiSandboxSnapshot>;
18
+ exclude(pattern: string): Promise<{
19
+ pattern: string;
20
+ settingsPath: string;
21
+ snapshot: TuiSandboxSnapshot;
22
+ }>;
23
+ }
24
+ export declare function linuxGlobPatternWarnings(resources: Awaited<ReturnType<typeof loadClaudeSettings>>, platform: 'macos' | 'linux' | 'windows' | 'wsl'): string[];
25
+ export declare function createTuiSandboxStore({ configRoot, cwd, homeDirectory, additionalDirectories, environment, }: {
26
+ configRoot: string;
27
+ cwd: string;
28
+ homeDirectory: string;
29
+ additionalDirectories?: readonly string[];
30
+ environment?: NodeJS.ProcessEnv;
31
+ }): TuiSandboxStore;
32
+ //# sourceMappingURL=sandbox-settings.d.ts.map
@@ -0,0 +1,146 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ import { loadClaudeSettings } from '../../compatibility/claude/shared-resources.js';
4
+ import { writeFileAtomically } from '../../platform/atomic-write.js';
5
+ import { claudeSandboxRuntime } from '../../sandbox/claude-sandbox-runtime.js';
6
+ import { claudeSandboxTempDirectory, loadClaudeSandboxSettings, } from '../../sandbox/claude-sandbox-settings.js';
7
+ function isRecord(value) {
8
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
9
+ }
10
+ async function readLocalSettings(path) {
11
+ try {
12
+ const value = JSON.parse(await readFile(path, 'utf8'));
13
+ if (!isRecord(value))
14
+ throw new Error(`Settings must be an object: ${path}`);
15
+ return value;
16
+ }
17
+ catch (error) {
18
+ if (error.code === 'ENOENT')
19
+ return {};
20
+ throw error;
21
+ }
22
+ }
23
+ async function updateLocalSandbox(path, update) {
24
+ for (let attempt = 0; attempt < 3; attempt += 1) {
25
+ let source;
26
+ try {
27
+ source = await readFile(path, 'utf8');
28
+ }
29
+ catch (error) {
30
+ if (error.code !== 'ENOENT')
31
+ throw error;
32
+ }
33
+ const settings = await readLocalSettings(path);
34
+ const current = settings.sandbox;
35
+ if (current !== undefined && !isRecord(current)) {
36
+ throw new Error(`sandbox must be an object: ${path}`);
37
+ }
38
+ const committed = await writeFileAtomically(path, `${JSON.stringify({ ...settings, sandbox: update(current ?? {}) }, null, 2)}\n`, {
39
+ beforeCommit: async () => {
40
+ try {
41
+ return (await readFile(path, 'utf8')) === source;
42
+ }
43
+ catch (error) {
44
+ if (error.code === 'ENOENT')
45
+ return source === undefined;
46
+ throw error;
47
+ }
48
+ },
49
+ });
50
+ if (committed)
51
+ return;
52
+ }
53
+ throw new Error(`Settings changed concurrently: ${path}`);
54
+ }
55
+ export function linuxGlobPatternWarnings(resources, platform) {
56
+ if (platform !== 'linux' && platform !== 'wsl')
57
+ return [];
58
+ const warnings = [];
59
+ for (const resource of resources) {
60
+ if (!isRecord(resource.value) || !isRecord(resource.value.permissions))
61
+ continue;
62
+ for (const behavior of ['allow', 'deny']) {
63
+ const rules = resource.value.permissions[behavior];
64
+ if (!Array.isArray(rules))
65
+ continue;
66
+ for (const rule of rules) {
67
+ if (typeof rule !== 'string')
68
+ continue;
69
+ const match = /^(?:Edit|Read)\((.*)\)$/u.exec(rule);
70
+ const pattern = match?.[1]?.replace(/\/\*\*$/u, '');
71
+ if (pattern && /[*?[\]]/u.test(pattern))
72
+ warnings.push(rule);
73
+ }
74
+ }
75
+ }
76
+ return [...new Set(warnings)];
77
+ }
78
+ export function createTuiSandboxStore({ configRoot, cwd, homeDirectory, additionalDirectories = [], environment = process.env, }) {
79
+ const localSettingsPath = join(cwd, '.claude', 'settings.local.json');
80
+ const load = async () => {
81
+ const resources = await loadClaudeSettings({ configRoot, cwd });
82
+ const settings = loadClaudeSandboxSettings({
83
+ resources,
84
+ cwd,
85
+ configRoot,
86
+ homeDirectory,
87
+ additionalDirectories,
88
+ tempDirectory: claudeSandboxTempDirectory(environment),
89
+ });
90
+ await claudeSandboxRuntime.initialize(settings);
91
+ const unavailableReason = claudeSandboxRuntime.unavailableReason(settings);
92
+ const platform = claudeSandboxRuntime.platformName();
93
+ return {
94
+ settings,
95
+ dependencies: claudeSandboxRuntime.dependencyCheck(settings),
96
+ supported: claudeSandboxRuntime.isSupportedPlatform(),
97
+ platform,
98
+ globPatternWarnings: linuxGlobPatternWarnings(resources, platform),
99
+ ...(unavailableReason ? { unavailableReason } : {}),
100
+ };
101
+ };
102
+ return {
103
+ load,
104
+ async setMode(mode) {
105
+ await updateLocalSandbox(localSettingsPath, (sandbox) => ({
106
+ ...sandbox,
107
+ enabled: mode !== 'disabled',
108
+ autoAllowBashIfSandboxed: mode === 'auto-allow',
109
+ }));
110
+ return load();
111
+ },
112
+ async setAllowUnsandboxedCommands(allow) {
113
+ await updateLocalSandbox(localSettingsPath, (sandbox) => ({
114
+ ...sandbox,
115
+ allowUnsandboxedCommands: allow,
116
+ }));
117
+ return load();
118
+ },
119
+ async exclude(pattern) {
120
+ const cleanPattern = pattern.trim().replace(/^["']|["']$/gu, '');
121
+ if (!cleanPattern) {
122
+ throw new Error('Please provide a command pattern to exclude');
123
+ }
124
+ await updateLocalSandbox(localSettingsPath, (sandbox) => {
125
+ const excluded = sandbox.excludedCommands;
126
+ if (excluded !== undefined &&
127
+ (!Array.isArray(excluded) ||
128
+ excluded.some((value) => typeof value !== 'string'))) {
129
+ throw new Error(`sandbox.excludedCommands must be an array of strings: ${localSettingsPath}`);
130
+ }
131
+ return {
132
+ ...sandbox,
133
+ excludedCommands: [
134
+ ...new Set([...(excluded ?? []), cleanPattern]),
135
+ ],
136
+ };
137
+ });
138
+ return {
139
+ pattern: cleanPattern,
140
+ settingsPath: relative(cwd, localSettingsPath),
141
+ snapshot: await load(),
142
+ };
143
+ },
144
+ };
145
+ }
146
+ //# sourceMappingURL=sandbox-settings.js.map
@@ -64,6 +64,11 @@ export const BUILTIN_TUI_SLASH_COMMANDS = [
64
64
  description: 'Manage allow and deny tool permission rules',
65
65
  source: 'builtin',
66
66
  },
67
+ {
68
+ name: 'sandbox',
69
+ description: 'Configure sandbox mode and command isolation',
70
+ source: 'builtin',
71
+ },
67
72
  {
68
73
  name: 'resume',
69
74
  description: 'Resume a previous conversation',
package/dist/cli.d.ts CHANGED
@@ -114,6 +114,7 @@ export interface CliDependencies extends InteractiveServiceFactory {
114
114
  exposeToolRegistry?: boolean;
115
115
  emitToolUseSummaries?: boolean;
116
116
  cwd?: string;
117
+ sandboxOriginalCwd?: string;
117
118
  configRoot?: string;
118
119
  environment?: Readonly<Record<string, string>>;
119
120
  providerEnvironment?: NodeJS.ProcessEnv;
package/dist/cli.js CHANGED
@@ -43,6 +43,8 @@ import { FilteredToolRegistry } from './tools/filtered-tool-registry.js';
43
43
  import { WebToolRegistry } from './tools/web.js';
44
44
  import { WorkspaceContext } from './application/session-worktree.js';
45
45
  import { launchTmuxWorktree } from './platform/tmux-worktree.js';
46
+ import { claudeSandboxRuntime } from './sandbox/claude-sandbox-runtime.js';
47
+ import { claudeSandboxTempDirectory, loadClaudeSandboxSettings, } from './sandbox/claude-sandbox-settings.js';
46
48
  import { createErrorResult, createSuccessResult, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
47
49
  import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
48
50
  import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
@@ -688,8 +690,9 @@ const consoleIO = {
688
690
  isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
689
691
  readStdinLines: () => process.stdin,
690
692
  };
691
- const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, emitToolUseSummaries = false, cwd: requestedCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
693
+ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
692
694
  const runtimeEnvironment = requestedProviderEnvironment ?? process.env;
695
+ const sandboxEnvironment = { ...runtimeEnvironment, ...environment };
693
696
  const claudeVersion = await detectInstalledClaudeVersion();
694
697
  const cwd = requestedCwd ?? process.cwd();
695
698
  const workspace = new WorkspaceContext(cwd);
@@ -948,9 +951,26 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
948
951
  ...cli.additionalDirectories,
949
952
  ...(exposePlanDirectory ? [resolve(configRoot, 'plans')] : []),
950
953
  ];
954
+ const sandboxSettings = loadClaudeSandboxSettings({
955
+ resources: settings.filter((resource) => resource.plugin !== true),
956
+ cwd: workspace.cwd(),
957
+ originalCwd: sandboxOriginalCwd ?? workspace.cwd(),
958
+ configRoot,
959
+ homeDirectory: sandboxEnvironment.HOME ?? sandboxEnvironment.USERPROFILE ?? homedir(),
960
+ additionalDirectories: initialAdditionalDirectories,
961
+ tempDirectory: claudeSandboxTempDirectory(sandboxEnvironment),
962
+ });
963
+ await claudeSandboxRuntime.initialize(sandboxSettings);
964
+ const sandboxUnavailableReason = claudeSandboxRuntime.unavailableReason();
965
+ if (sandboxUnavailableReason && !sandboxSettings.failIfUnavailable) {
966
+ runtimeEventSink({ type: 'warning', message: sandboxUnavailableReason });
967
+ }
951
968
  const permissionAdditionalDirectories = [
952
- ...initialAdditionalDirectories,
953
- ...(memoryDirectory ? [memoryDirectory] : []),
969
+ ...new Set([
970
+ ...initialAdditionalDirectories,
971
+ ...controls.addDirectories.map((directory) => resolve(cwd, directory)),
972
+ ...(memoryDirectory ? [memoryDirectory] : []),
973
+ ]),
954
974
  ];
955
975
  const initialAdditionalReadDirectories = [claudeBackgroundTaskParent(cwd)];
956
976
  const permissionResolverForMode = (permissionMode) => new ClaudeExtensionPermissionResolver(new ClaudePermissionResolver({
@@ -962,6 +982,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
962
982
  disallowedTools: cli.disallowedTools,
963
983
  additionalDirectories: permissionAdditionalDirectories,
964
984
  additionalReadDirectories: initialAdditionalReadDirectories,
985
+ sandbox: claudeSandboxRuntime,
965
986
  permissionMode,
966
987
  ...(isSessionActionApproved ? { isSessionActionApproved } : {}),
967
988
  ...(permissionMode === 'auto'
@@ -978,6 +999,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
978
999
  ...(memoryDirectory ? { sharedMemoryDirectory: memoryDirectory } : {}),
979
1000
  additionalDirectories: initialAdditionalDirectories,
980
1001
  additionalReadDirectories: initialAdditionalReadDirectories,
1002
+ sandbox: claudeSandboxRuntime,
981
1003
  ...(environment ? { environment } : {}),
982
1004
  });
983
1005
  const runtimeMcpResources = async (candidates) => {
@@ -1463,6 +1485,7 @@ const defaultPluginEvalRuntimeFactory = {
1463
1485
  USERPROFILE: options.home,
1464
1486
  },
1465
1487
  providerEnvironment: process.env,
1488
+ isSessionActionApproved: (call) => options.allowedTools.includes(call.name),
1466
1489
  controls: {
1467
1490
  ...DEFAULT_CLI_CONTROLS,
1468
1491
  sessionPersistence: false,
@@ -1568,6 +1591,7 @@ const defaultDependencies = {
1568
1591
  createService: ({ additionalDirectories, cwd, ...options }) => createDefaultService({
1569
1592
  ...options,
1570
1593
  ...(cwd === undefined ? {} : { cwd }),
1594
+ sandboxOriginalCwd: process.cwd(),
1571
1595
  ...(agent === undefined ? {} : { agent }),
1572
1596
  controls: {
1573
1597
  ...interactiveControls,