diff --git a/internal/cli/frontdoor.go b/internal/cli/frontdoor.go index 77b5081c..fb54be6e 100644 --- a/internal/cli/frontdoor.go +++ b/internal/cli/frontdoor.go @@ -301,6 +301,15 @@ func runClaude(ctx context.Context, args []string, dir string, ops hostOps, look inner = append(inner, "--dangerously-skip-permissions") } inner = append(inner, "--agent", "spacedock:first-officer") + // An unsandboxed launch has no safehouse isolation, so per-action permission + // prompting is friction without a matching safety gain: start the first officer + // in auto permission-mode. Suppressed when the operator already chose a mode and + // on a resume (which rides its own session intent, like the bootstrap prompt). + // The sandboxed arm's --dangerously-skip-permissions above already covers its + // posture, so this is the !wrap counterpart, not a replacement. + if !wrap && !resume && !passthroughHasFlag(fd.passthrough, "--permission-mode") { + inner = append(inner, "--permission-mode", "auto") + } inner = append(inner, fd.passthrough...) if !resume { inner = append(inner, launchPrompt(bootstrapPrompt, fd)) @@ -368,6 +377,22 @@ func hasPluginDir(passthrough []string) bool { return false } +// passthroughHasFlag reports whether the operator already supplied any of the +// named host flags in the passthrough, in either `--flag value` or `--flag=value` +// form. The unsandboxed launchers consult it before injecting their default +// permission/approval flag so an operator-supplied one is never duplicated +// (operator wins). Mirrors hasPluginDir, generalized over a flag set. +func passthroughHasFlag(passthrough []string, names ...string) bool { + for _, a := range passthrough { + for _, name := range names { + if a == name || strings.HasPrefix(a, name+"=") { + return true + } + } + } + return false +} + // containsResume reports whether the operator forwarded any of claude's // session-resume forms (which carry their own session intent, so the bootstrap // prompt is suppressed): `--resume`, `--resume=`, `-r`, `--continue`, `-c`. @@ -458,6 +483,15 @@ func runCodex(ctx context.Context, args []string, dir string, ops hostOps, lookP if wrap { inner = append(inner, "--dangerously-bypass-approvals-and-sandbox") } + // An unsandboxed launch has no safehouse isolation; codex has no single + // auto-mode flag, so its nearest analog to claude's auto permission-mode is + // `--ask-for-approval on-request` (the model decides when to escalate). + // Suppressed when the operator already chose an approval policy and on a resume + // (which rides its own session intent). The sandboxed arm's bypass flag above + // already covers its posture, so this is the !wrap counterpart. + if !wrap && !resume && !passthroughHasFlag(fd.passthrough, "--ask-for-approval", "-a") { + inner = append(inner, "--ask-for-approval", "on-request") + } inner = append(inner, fd.passthrough...) if !resume { inner = append(inner, launchPrompt(codexBootstrapPrompt, fd)) @@ -512,12 +546,14 @@ var valueTakingHostFlags = map[string]map[string]bool{ }, "codex": { "-m": true, "--model": true, - "--config": true, - "-c": true, - "--cd": true, - "--image": true, - "--sandbox": true, - "--profile": true, + "--config": true, + "-c": true, + "--cd": true, + "--image": true, + "--sandbox": true, + "--profile": true, + "--ask-for-approval": true, + "-a": true, }, } diff --git a/internal/cli/frontdoor_permission_mode_test.go b/internal/cli/frontdoor_permission_mode_test.go new file mode 100644 index 00000000..d254a475 --- /dev/null +++ b/internal/cli/frontdoor_permission_mode_test.go @@ -0,0 +1,234 @@ +// ABOUTME: AC-1..AC-4 oracles for the unsandboxed-launch permission posture: +// ABOUTME: claude --permission-mode auto / codex --ask-for-approval on-request injection. +package cli + +import ( + "bytes" + "context" + "testing" +) + +// argvHasFlagValue reports whether argv contains the space-form flag/value pair +// (a `flag` token immediately followed by `value`), and how many times `flag` +// appears in total. The count lets an oracle assert a single occurrence (operator +// override must not produce a duplicate). +func argvHasFlagValue(argv []string, flag, value string) (pair bool, count int) { + for i, tok := range argv { + if tok == flag { + count++ + if i+1 < len(argv) && argv[i+1] == value { + pair = true + } + } + } + return pair, count +} + +// AC-1: an unsandboxed `spacedock claude` launch injects `--permission-mode auto` +// and carries NO `--dangerously-skip-permissions`; the sandboxed launch is +// unchanged (`--dangerously-skip-permissions`, NO injected `--permission-mode`). +func TestClaudeUnsandboxedInjectsAutoPermissionMode(t *testing.T) { + t.Run("unsandboxed-injects-auto", func(t *testing.T) { + dir := t.TempDir() // no .safehouse + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runClaude(context.Background(), nil, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + if pair, _ := argvHasFlagValue(fake.launchedArg, "--permission-mode", "auto"); !pair { + t.Fatalf("unsandboxed claude launch missing --permission-mode auto: %v", fake.launchedArg) + } + for _, tok := range fake.launchedArg { + if tok == "--dangerously-skip-permissions" { + t.Fatalf("unsandboxed claude launch carried --dangerously-skip-permissions: %v", fake.launchedArg) + } + } + }) + t.Run("sandboxed-unchanged", func(t *testing.T) { + dir := safehouseFixtureDir(t) + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runClaude(context.Background(), nil, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + sawSkip := false + for _, tok := range fake.launchedArg { + if tok == "--dangerously-skip-permissions" { + sawSkip = true + } + if tok == "--permission-mode" { + t.Fatalf("sandboxed claude launch carried injected --permission-mode: %v", fake.launchedArg) + } + } + if !sawSkip { + t.Fatalf("sandboxed claude launch missing --dangerously-skip-permissions: %v", fake.launchedArg) + } + }) +} + +// AC-2 (captain option A): an unsandboxed `spacedock codex` launch injects +// `--ask-for-approval on-request` and carries NO bypass flag; the sandboxed +// launch is unchanged (`--dangerously-bypass-approvals-and-sandbox`, NO injected +// approval flag). +func TestCodexUnsandboxedInjectsOnRequestApproval(t *testing.T) { + t.Run("unsandboxed-injects-on-request", func(t *testing.T) { + dir := t.TempDir() // no .safehouse + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runCodex(context.Background(), nil, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + if pair, _ := argvHasFlagValue(fake.launchedArg, "--ask-for-approval", "on-request"); !pair { + t.Fatalf("unsandboxed codex launch missing --ask-for-approval on-request: %v", fake.launchedArg) + } + for _, tok := range fake.launchedArg { + if tok == "--dangerously-bypass-approvals-and-sandbox" { + t.Fatalf("unsandboxed codex launch carried the bypass flag: %v", fake.launchedArg) + } + } + }) + t.Run("sandboxed-unchanged", func(t *testing.T) { + dir := safehouseFixtureDir(t) + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runCodex(context.Background(), nil, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + sawBypass := false + for _, tok := range fake.launchedArg { + if tok == "--dangerously-bypass-approvals-and-sandbox" { + sawBypass = true + } + if tok == "--ask-for-approval" { + t.Fatalf("sandboxed codex launch carried injected --ask-for-approval: %v", fake.launchedArg) + } + } + if !sawBypass { + t.Fatalf("sandboxed codex launch missing --dangerously-bypass-approvals-and-sandbox: %v", fake.launchedArg) + } + }) +} + +// AC-3: an operator-supplied permission/approval flag in the passthrough +// suppresses the spacedock-injected one — exactly one occurrence, operator value +// wins (no duplicate). Covered for both space form and equals form. +func TestOperatorPermissionFlagSuppressesInjection(t *testing.T) { + t.Run("claude-space-form", func(t *testing.T) { + dir := t.TempDir() // no .safehouse + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runClaude(context.Background(), []string{"--", "--permission-mode", "plan"}, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + pair, count := argvHasFlagValue(fake.launchedArg, "--permission-mode", "plan") + if !pair { + t.Fatalf("operator --permission-mode plan not preserved: %v", fake.launchedArg) + } + if count != 1 { + t.Fatalf("--permission-mode appears %d times, want 1 (no injected duplicate): %v", count, fake.launchedArg) + } + }) + t.Run("claude-equals-form", func(t *testing.T) { + dir := t.TempDir() // no .safehouse + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runClaude(context.Background(), []string{"--", "--permission-mode=plan"}, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + for _, tok := range fake.launchedArg { + if tok == "--permission-mode" { + t.Fatalf("injected space-form --permission-mode despite operator equals-form: %v", fake.launchedArg) + } + } + }) + t.Run("codex-space-form", func(t *testing.T) { + dir := t.TempDir() // no .safehouse + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runCodex(context.Background(), []string{"--", "--ask-for-approval", "untrusted"}, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + pair, count := argvHasFlagValue(fake.launchedArg, "--ask-for-approval", "untrusted") + if !pair { + t.Fatalf("operator --ask-for-approval untrusted not preserved: %v", fake.launchedArg) + } + if count != 1 { + t.Fatalf("--ask-for-approval appears %d times, want 1 (no injected duplicate): %v", count, fake.launchedArg) + } + }) + t.Run("codex-short-form", func(t *testing.T) { + dir := t.TempDir() // no .safehouse + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runCodex(context.Background(), []string{"--", "-a", "untrusted"}, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + for _, tok := range fake.launchedArg { + if tok == "--ask-for-approval" { + t.Fatalf("injected --ask-for-approval despite operator short-form -a: %v", fake.launchedArg) + } + } + }) +} + +// AC-4: the injected flag rides the non-resume gate — a resumed unsandboxed +// launch is NOT forced into the auto/approval mode. Mirrors the resume-suppression +// oracle: the bootstrap prompt and the injected flag share the same gate. +func TestResumeUnsandboxedSuppressesInjection(t *testing.T) { + t.Run("claude-resume", func(t *testing.T) { + dir := t.TempDir() // no .safehouse + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runClaude(context.Background(), []string{"--", "--resume"}, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + for _, tok := range fake.launchedArg { + if tok == "--permission-mode" { + t.Fatalf("resumed claude launch carried injected --permission-mode: %v", fake.launchedArg) + } + } + }) + t.Run("codex-resume", func(t *testing.T) { + dir := t.TempDir() // no .safehouse + fake := &fakeHost{manifest: compatibleManifest(t)} + var stdout, stderr bytes.Buffer + + code := runCodex(context.Background(), []string{"--", "resume", "abc123"}, dir, fake, lookFound, &stdout, &stderr) + + if code != 0 { + t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) + } + for _, tok := range fake.launchedArg { + if tok == "--ask-for-approval" { + t.Fatalf("resumed codex launch carried injected --ask-for-approval: %v", fake.launchedArg) + } + } + }) +} diff --git a/internal/cli/frontdoor_stray_prompt_test.go b/internal/cli/frontdoor_stray_prompt_test.go index 27b01e1a..0a464bd5 100644 --- a/internal/cli/frontdoor_stray_prompt_test.go +++ b/internal/cli/frontdoor_stray_prompt_test.go @@ -36,7 +36,7 @@ func TestClaudeStrayPromptAfterDashWarns(t *testing.T) { if !strings.Contains(warn, "BEFORE") { t.Fatalf("warning does not name the corrected form (prompt BEFORE `--`): %q", warn) } - want := []string{"claude", "--agent", "spacedock:first-officer", "--model", "gpt-x", "@/tmp/handoff.md", wantBootstrapPrompt} + want := []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "--model", "gpt-x", "@/tmp/handoff.md", wantBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v (warn must not change the argv)", fake.launchedArg, want) } @@ -67,7 +67,7 @@ func TestClaudeStrayPromptSession12Shape(t *testing.T) { if strings.Contains(warn, "/co") { t.Fatalf("warning names the spacedock-injected --plugin-dir value (shadows the real prompt): %q", warn) } - want := []string{"claude", "--agent", "spacedock:first-officer", "--plugin-dir", "/co", "--model", "gpt-x", "@/tmp/handoff.md", wantBootstrapPrompt} + want := []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "--plugin-dir", "/co", "--model", "gpt-x", "@/tmp/handoff.md", wantBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v (warn must not change the argv)", fake.launchedArg, want) } @@ -120,7 +120,7 @@ func TestStrayPromptGuardNegatives(t *testing.T) { return runClaude(context.Background(), args, dir, fake, lookFound, &stdout, stderr) }, args: []string{"--", "-p", "do the thing"}, - want: []string{"claude", "--agent", "spacedock:first-officer", "-p", "do the thing", wantBootstrapPrompt}, + want: []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "-p", "do the thing", wantBootstrapPrompt}, }, { name: "codex exec subcommand-arg", @@ -129,7 +129,7 @@ func TestStrayPromptGuardNegatives(t *testing.T) { return runCodex(context.Background(), args, dir, fake, lookFound, &stdout, stderr) }, args: []string{"--", "exec", "do the thing"}, - want: []string{"codex", "exec", "do the thing", wantCodexBootstrapPrompt}, + want: []string{"codex", "--ask-for-approval", "on-request", "exec", "do the thing", wantCodexBootstrapPrompt}, }, { name: "claude hasTask short-circuit", @@ -138,7 +138,7 @@ func TestStrayPromptGuardNegatives(t *testing.T) { return runClaude(context.Background(), args, dir, fake, lookFound, &stdout, stderr) }, args: []string{"task before", "--", "@/tmp/handoff.md"}, - want: []string{"claude", "--agent", "spacedock:first-officer", "@/tmp/handoff.md", wantBootstrapPrompt + " task before"}, + want: []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "@/tmp/handoff.md", wantBootstrapPrompt + " task before"}, }, { // An unrecognized `-`-prefixed flag's value must NOT get the prescriptive @@ -150,7 +150,7 @@ func TestStrayPromptGuardNegatives(t *testing.T) { return runClaude(context.Background(), args, dir, fake, lookFound, &stdout, stderr) }, args: []string{"--", "--some-new-flag", "the-value"}, - want: []string{"claude", "--agent", "spacedock:first-officer", "--some-new-flag", "the-value", wantBootstrapPrompt}, + want: []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "--some-new-flag", "the-value", wantBootstrapPrompt}, }, { // The spacedock-injected `--plugin-dir ` prefix lands the `exec` @@ -164,7 +164,7 @@ func TestStrayPromptGuardNegatives(t *testing.T) { return runCodex(context.Background(), args, dir, fake, lookFound, &stdout, stderr) }, args: []string{"--plugin-dir", "/co", "--", "exec", "do the thing"}, - want: []string{"codex", "--plugin-dir", "/co", "exec", "do the thing", wantCodexBootstrapPrompt}, + want: []string{"codex", "--ask-for-approval", "on-request", "--plugin-dir", "/co", "exec", "do the thing", wantCodexBootstrapPrompt}, }, { // Same structural skip for the codex `resume` subcommand behind the @@ -175,7 +175,7 @@ func TestStrayPromptGuardNegatives(t *testing.T) { return runCodex(context.Background(), args, dir, fake, lookFound, &stdout, stderr) }, args: []string{"--plugin-dir", "/co", "--", "resume", "abc123"}, - want: []string{"codex", "--plugin-dir", "/co", "resume", "abc123", wantCodexBootstrapPrompt}, + want: []string{"codex", "--ask-for-approval", "on-request", "--plugin-dir", "/co", "resume", "abc123", wantCodexBootstrapPrompt}, }, } for _, tc := range cases { diff --git a/internal/cli/frontdoor_test.go b/internal/cli/frontdoor_test.go index 4ae272eb..eddbb34e 100644 --- a/internal/cli/frontdoor_test.go +++ b/internal/cli/frontdoor_test.go @@ -105,7 +105,7 @@ func TestClaudeFrontDoorLaunchesOnCompatible(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) } - want := []string{"claude", "--agent", "spacedock:first-officer", "-p", "do the thing", wantBootstrapPrompt} + want := []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "-p", "do the thing", wantBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v", fake.launchedArg, want) } @@ -485,7 +485,7 @@ func TestClaudeFrontDoorSkipContractCheckBootstrap(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 with --skip-contract-check (stderr=%q)", code, stderr.String()) } - want := []string{"claude", "--agent", "spacedock:first-officer", wantBootstrapPrompt} + want := []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", wantBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v (skip-check must not pass the flag through)", fake.launchedArg, want) } diff --git a/internal/cli/launch_parity_test.go b/internal/cli/launch_parity_test.go index 84030d9a..b7c201dc 100644 --- a/internal/cli/launch_parity_test.go +++ b/internal/cli/launch_parity_test.go @@ -164,7 +164,7 @@ func TestCodexPlainWhenNoTrigger(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) } - want := []string{"codex", wantCodexBootstrapPrompt} + want := []string{"codex", "--ask-for-approval", "on-request", wantCodexBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v", fake.launchedArg, want) } @@ -221,7 +221,7 @@ func TestFenceTaskPromptOverride(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) } - want := []string{"claude", "--agent", "spacedock:first-officer", "--model", "gpt-x", wantBootstrapPrompt + " do the thing"} + want := []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "--model", "gpt-x", wantBootstrapPrompt + " do the thing"} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v", fake.launchedArg, want) } @@ -283,7 +283,7 @@ func TestPluginDirRelaxesGate(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 (--plugin-dir relaxes the gate); stderr=%q", code, stderr.String()) } - want := []string{"claude", "--agent", "spacedock:first-officer", "--plugin-dir", "/a", "--plugin-dir", "/b", wantBootstrapPrompt} + want := []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "--plugin-dir", "/a", "--plugin-dir", "/b", wantBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v", fake.launchedArg, want) } @@ -303,7 +303,7 @@ func TestPluginDirRelaxesGate(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 (before-`--` --plugin-dir relaxes the gate); stderr=%q", code, stderr.String()) } - want := []string{"claude", "--agent", "spacedock:first-officer", "--plugin-dir", "/a", "--plugin-dir", "/b", wantBootstrapPrompt} + want := []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "--plugin-dir", "/a", "--plugin-dir", "/b", wantBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v", fake.launchedArg, want) } @@ -315,7 +315,7 @@ func TestPluginDirRelaxesGate(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 (before-`--` --plugin-dir relaxes the gate); stderr=%q", code, stderr.String()) } - want := []string{"codex", "--plugin-dir", "/a", wantCodexBootstrapPrompt} + want := []string{"codex", "--ask-for-approval", "on-request", "--plugin-dir", "/a", wantCodexBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v", fake.launchedArg, want) } @@ -327,7 +327,7 @@ func TestPluginDirRelaxesGate(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 (captain no-`--` form relaxes the gate); stderr=%q", code, stderr.String()) } - want := []string{"claude", "--agent", "spacedock:first-officer", "--plugin-dir", "/a", wantBootstrapPrompt + " review the PRs"} + want := []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "--plugin-dir", "/a", wantBootstrapPrompt + " review the PRs"} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v", fake.launchedArg, want) } diff --git a/internal/cli/plugin_dir_frontdoor_test.go b/internal/cli/plugin_dir_frontdoor_test.go index 93aa4a20..c42852ec 100644 --- a/internal/cli/plugin_dir_frontdoor_test.go +++ b/internal/cli/plugin_dir_frontdoor_test.go @@ -62,7 +62,7 @@ func TestDevLanePluginDirReachesLaunchSeam(t *testing.T) { t.Fatalf("launch seam not reached on the --plugin-dir dev lane") } want := []string{ - "claude", "--agent", "spacedock:first-officer", + "claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "--plugin-dir", repo, wantBootstrapPrompt + " do the thing", } diff --git a/internal/cli/safehouse_frontdoor_test.go b/internal/cli/safehouse_frontdoor_test.go index ec7e824e..4aa6c245 100644 --- a/internal/cli/safehouse_frontdoor_test.go +++ b/internal/cli/safehouse_frontdoor_test.go @@ -156,7 +156,7 @@ func TestClaudeNoSafehouseLaunchesPlain(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) } - want := []string{"claude", "--agent", "spacedock:first-officer", "--foo", wantBootstrapPrompt} + want := []string{"claude", "--agent", "spacedock:first-officer", "--permission-mode", "auto", "--foo", wantBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v", fake.launchedArg, want) } @@ -314,7 +314,7 @@ func TestCodexNoSafehouseLaunchesPlainNoBypass(t *testing.T) { if code != 0 { t.Fatalf("exit = %d, want 0 (stderr=%q)", code, stderr.String()) } - want := []string{"codex", "--foo", wantCodexBootstrapPrompt} + want := []string{"codex", "--ask-for-approval", "on-request", "--foo", wantCodexBootstrapPrompt} if !equalArgv(fake.launchedArg, want) { t.Fatalf("launch argv = %v, want %v", fake.launchedArg, want) }