From ef51dacefa4554ea751af0244e221a63f5ad09a6 Mon Sep 17 00:00:00 2001 From: Falko Kaethner Date: Fri, 25 Sep 2026 07:13:39 +0200 Subject: [PATCH 1/3] fix(codex): only pass --add-dir when the sandbox is writable Codex refuses to start when --add-dir is given without a writable sandbox, so a Codex agent spawned outside auto mode exited at once. Co-Authored-By: Claude Opus 5.5 --- src/main/hive.ts | 15 +++++++++------ src/main/index.ts | 1 + src/shared/agentProvider.ts | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/main/hive.ts b/src/main/hive.ts index 4537c2bf6..48d7f7d1f 100644 --- a/src/main/hive.ts +++ b/src/main/hive.ts @@ -35,6 +35,7 @@ import { canReceiveInbox, providerPreset, bridgeOf, + codexSandboxAllowsExtraRoots, type AgentProvider } from '../shared/agentProvider'; import { MCP_CATALOG } from '../shared/mcpCatalog'; @@ -695,6 +696,8 @@ export class HiveManager { * MemPalace dir, which `mempalace` mutates). Absolute paths; ignored * for providers without a sandbox. */ extraWritableDirs?: string[]; + /** Original CLI arguments used to determine the Codex sandbox posture. */ + launchArgs?: string[]; } = {} ): Promise { const root = this.root(); @@ -842,12 +845,12 @@ export class HiveManager { // that already vets hook sources"). Without it the hooks silently // never fire. Must precede the positional prompt. preArgs.push('--dangerously-bypass-hook-trust'); - // Auto mode keeps codex's OS sandbox (`-a never -s workspace-write`, - // agentProvider.ts). workspace-write only covers cwd, so the agent - // folder (inbox/.done, memory.md, outbox) and the shared hive root - // (research deliverables, the board for god) are added as extra - // writable roots. Harmless outside auto mode. - for (const d of this.sandboxWritableDirs(meta, dir, root, opts.extraWritableDirs)) preArgs.push('--add-dir', d); + // Codex exits when --add-dir is present without a writable sandbox. + // In read-only mode the agent starts without extra roots; writes to + // inbox/outbox instead go through the human approval flow. + if (codexSandboxAllowsExtraRoots(opts.launchArgs ?? [])) { + for (const d of this.sandboxWritableDirs(meta, dir, root, opts.extraWritableDirs)) preArgs.push('--add-dir', d); + } } else if (desc.shim === 'pi') { // Pi (earendil-works) has a rich pi.on(event) lifecycle. We drop a diff --git a/src/main/index.ts b/src/main/index.ts index e80298c63..d1a93be6b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2770,6 +2770,7 @@ async function spawnAgentCore(opts: AgentSpawnOptions, owner: Electron.WebConten const inj = await hive.ensureAgent( { ...opts.hive, cwd: opts.cwd, provider }, { + launchArgs: opts.args ?? [], semanticMemory: memory.active(), knowledgeGraph: knowledge.active(), // Bake the ABSOLUTE KG CLI path into the agent's prompt. The prompt used diff --git a/src/shared/agentProvider.ts b/src/shared/agentProvider.ts index 1e5048d32..baa6e7124 100644 --- a/src/shared/agentProvider.ts +++ b/src/shared/agentProvider.ts @@ -672,6 +672,20 @@ export function autoModeFlagForProvider(provider: AgentProvider): string { return providerPreset(provider).autoModeFlag ?? ''; } +/** Codex exits when --add-dir is used without a writable sandbox, so only add + * extra roots when argv explicitly enables one. */ +export function codexSandboxAllowsExtraRoots(args: string[]): boolean { + let sandbox: string | undefined; + let bypass = false; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--full-auto' || arg === '--dangerously-bypass-approvals-and-sandbox') bypass = true; + if (arg === '-s' || arg === '--sandbox') sandbox = args[i + 1]; + else if (arg.startsWith('--sandbox=')) sandbox = arg.slice('--sandbox='.length); + } + return bypass || sandbox === 'workspace-write' || sandbox === 'danger-full-access'; +} + /** Idempotently append a provider's auto-mode flag to an args array, honoring the * user's global autoMode toggle. The renderer's Add Agent flow bakes this same * flag into the command STRING before a GUI hire ever reaches the shared spawn From a551b5829deab99e798bb84bbaf5d2a57db66649 Mon Sep 17 00:00:00 2001 From: Falko Kaethner Date: Fri, 25 Sep 2026 07:13:40 +0200 Subject: [PATCH 2/3] test(codex): cover the sandbox gate for --add-dir Co-Authored-By: Claude Opus 5.5 --- test/codex-add-dir.test.cjs | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test/codex-add-dir.test.cjs diff --git a/test/codex-add-dir.test.cjs b/test/codex-add-dir.test.cjs new file mode 100644 index 000000000..cff29eb18 --- /dev/null +++ b/test/codex-add-dir.test.cjs @@ -0,0 +1,74 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const loadTs = require('./load-ts.cjs'); + +const electron = require.resolve('electron'); +require.cache[electron] = { + id: electron, filename: electron, loaded: true, + exports: { Notification: class { show() {} static isSupported() { return false; } } } +}; + +const { HiveManager } = loadTs('src/main/hive.ts'); +const { codexSandboxAllowsExtraRoots } = loadTs('src/shared/agentProvider.ts'); + +function tmpHome() { return fs.mkdtempSync(path.join(os.tmpdir(), 'md-codex-add-dir-')); } +function count(values, wanted) { return values.filter((value) => value === wanted).length; } + +const sandboxCases = [ + ['no flag', [], false], + ['-s read-only', ['-s', 'read-only'], false], + ['--sandbox read-only', ['--sandbox', 'read-only'], false], + ['-s workspace-write', ['-s', 'workspace-write'], true], + ['--sandbox workspace-write', ['--sandbox', 'workspace-write'], true], + ['--sandbox=workspace-write', ['--sandbox=workspace-write'], true], + ['danger-full-access', ['--sandbox', 'danger-full-access'], true], + ['--full-auto', ['--full-auto'], true], + ['full bypass', ['--dangerously-bypass-approvals-and-sandbox'], true], + ['dangling -s', ['-s'], false], + ['last sandbox read-only wins', ['-s', 'workspace-write', '-s', 'read-only'], false], + ['last sandbox workspace-write wins', ['-s', 'read-only', '-s', 'workspace-write'], true], + ['sandbox read-only overrides --full-auto', ['--full-auto', '-s', 'read-only'], false] +]; + +for (const [name, args, expected] of sandboxCases) { + test(`codexSandboxAllowsExtraRoots: ${name}`, () => { + assert.equal(codexSandboxAllowsExtraRoots(args), expected); + }); +} + +test('ensureAgent omits Codex --add-dir without a writable sandbox', async () => { + const home = tmpHome(); + const hive = new HiveManager(() => home); + const inj = await hive.ensureAgent( + { id: 'jim-read-only', name: 'Jim', provider: 'codex', cwd: home }, + { launchArgs: ['--model', 'x'] } + ); + + assert.equal(inj.args.includes('--add-dir'), false); + assert.equal(count(inj.args, '--dangerously-bypass-hook-trust'), 1); + assert.match(inj.args.at(-1), /^You are "Jim" \(jim-read-only\),/); +}); + +test('ensureAgent adds every Codex writable root once and keeps the prompt last', async () => { + const home = tmpHome(); + const hive = new HiveManager(() => home); + const palace = path.join(home, 'palace'); + const inj = await hive.ensureAgent( + { id: 'jim-write', name: 'Jim', provider: 'codex', cwd: home }, + { launchArgs: ['-s', 'workspace-write'], extraWritableDirs: [palace] } + ); + const agentDir = path.join(home, 'hive', 'agents', 'jim-write'); + const hiveRoot = path.join(home, 'hive'); + const addDirs = inj.args.flatMap((arg, i) => arg === '--add-dir' ? [inj.args[i + 1]] : []); + + assert.equal(count(addDirs, agentDir), 1); + assert.equal(count(addDirs, hiveRoot), 1); + assert.ok(addDirs.includes(palace)); + assert.equal(count(inj.args, '--dangerously-bypass-hook-trust'), 1); + assert.match(inj.args.at(-1), /^You are "Jim" \(jim-write\),/); +}); From 3be9dedf1760b35704b8a1345027a5bf8cfbf736 Mon Sep 17 00:00:00 2001 From: Falko Kaethner Date: Fri, 25 Sep 2026 07:16:26 +0200 Subject: [PATCH 3/3] fix(codex): let the last sandbox flag win over --full-auto Co-Authored-By: Claude Opus 5.5 --- src/shared/agentProvider.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/agentProvider.ts b/src/shared/agentProvider.ts index baa6e7124..5e70604e9 100644 --- a/src/shared/agentProvider.ts +++ b/src/shared/agentProvider.ts @@ -676,14 +676,14 @@ export function autoModeFlagForProvider(provider: AgentProvider): string { * extra roots when argv explicitly enables one. */ export function codexSandboxAllowsExtraRoots(args: string[]): boolean { let sandbox: string | undefined; - let bypass = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; - if (arg === '--full-auto' || arg === '--dangerously-bypass-approvals-and-sandbox') bypass = true; - if (arg === '-s' || arg === '--sandbox') sandbox = args[i + 1]; + if (arg === '--full-auto') sandbox = 'workspace-write'; + else if (arg === '--dangerously-bypass-approvals-and-sandbox') sandbox = 'danger-full-access'; + else if (arg === '-s' || arg === '--sandbox') sandbox = args[i + 1]; else if (arg.startsWith('--sandbox=')) sandbox = arg.slice('--sandbox='.length); } - return bypass || sandbox === 'workspace-write' || sandbox === 'danger-full-access'; + return sandbox === 'workspace-write' || sandbox === 'danger-full-access'; } /** Idempotently append a provider's auto-mode flag to an args array, honoring the