diff --git a/codeagent-wrapper/internal/app/executor_concurrent_test.go b/codeagent-wrapper/internal/app/executor_concurrent_test.go index f6c7e8b..ed5f5cf 100644 --- a/codeagent-wrapper/internal/app/executor_concurrent_test.go +++ b/codeagent-wrapper/internal/app/executor_concurrent_test.go @@ -169,6 +169,12 @@ func (f *execFakeRunner) Process() executor.ProcessHandle { return &execFakeProcess{pid: 1} } +func (f *execFakeRunner) UnsetEnv(keys ...string) { + for _, k := range keys { + delete(f.env, k) + } +} + func TestExecutorRunCodexTaskWithContext(t *testing.T) { defer resetTestHooks() diff --git a/codeagent-wrapper/internal/app/main_test.go b/codeagent-wrapper/internal/app/main_test.go index 45e67b4..3a58bdb 100644 --- a/codeagent-wrapper/internal/app/main_test.go +++ b/codeagent-wrapper/internal/app/main_test.go @@ -274,6 +274,10 @@ func (d *drainBlockingCmd) Process() executor.ProcessHandle { return d.inner.Process() } +func (d *drainBlockingCmd) UnsetEnv(keys ...string) { + d.inner.UnsetEnv(keys...) +} + type bufferWriteCloser struct { buf bytes.Buffer mu sync.Mutex @@ -568,6 +572,14 @@ func (f *fakeCmd) Process() executor.ProcessHandle { return f.process } +func (f *fakeCmd) UnsetEnv(keys ...string) { + f.mu.Lock() + defer f.mu.Unlock() + for _, k := range keys { + delete(f.env, k) + } +} + func (f *fakeCmd) runStdoutScript() { if len(f.stdoutPlan) == 0 { if !f.keepStdoutOpen { diff --git a/codeagent-wrapper/internal/executor/env_stderr_test.go b/codeagent-wrapper/internal/executor/env_stderr_test.go index 693237c..2af7439 100644 --- a/codeagent-wrapper/internal/executor/env_stderr_test.go +++ b/codeagent-wrapper/internal/executor/env_stderr_test.go @@ -41,6 +41,11 @@ func (f *fakeCmd) SetEnv(env map[string]string) { } } func (f *fakeCmd) Process() processHandle { return nil } +func (f *fakeCmd) UnsetEnv(keys ...string) { + for _, k := range keys { + delete(f.env, k) + } +} func TestEnvInjection_LogsToStderrAndMasksKey(t *testing.T) { // Arrange ~/.codeagent/models.json via HOME override. diff --git a/codeagent-wrapper/internal/executor/executor.go b/codeagent-wrapper/internal/executor/executor.go index b1dcfb6..1243b21 100644 --- a/codeagent-wrapper/internal/executor/executor.go +++ b/codeagent-wrapper/internal/executor/executor.go @@ -113,6 +113,7 @@ type commandRunner interface { SetStderr(io.Writer) SetDir(string) SetEnv(env map[string]string) + UnsetEnv(keys ...string) Process() processHandle } @@ -221,6 +222,33 @@ func (r *realCmd) SetEnv(env map[string]string) { r.cmd.Env = out } +func (r *realCmd) UnsetEnv(keys ...string) { + if r == nil || r.cmd == nil || len(keys) == 0 { + return + } + // If cmd.Env is nil, Go inherits all parent env vars. + // Populate explicitly so we can selectively remove keys. + if r.cmd.Env == nil { + r.cmd.Env = os.Environ() + } + drop := make(map[string]struct{}, len(keys)) + for _, k := range keys { + drop[k] = struct{}{} + } + filtered := make([]string, 0, len(r.cmd.Env)) + for _, kv := range r.cmd.Env { + idx := strings.IndexByte(kv, '=') + name := kv + if idx >= 0 { + name = kv[:idx] + } + if _, ok := drop[name]; !ok { + filtered = append(filtered, kv) + } + } + r.cmd.Env = filtered +} + func (r *realCmd) Process() processHandle { if r == nil || r.cmd == nil || r.cmd.Process == nil { return nil @@ -1126,6 +1154,13 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe injectTempEnv(cmd) + // Claude Code sets CLAUDECODE=1 in its child processes. If we don't + // remove it, the spawned `claude -p` detects the variable and refuses + // to start ("cannot be launched inside another Claude Code session"). + if commandName == "claude" { + cmd.UnsetEnv("CLAUDECODE") + } + // For backends that don't support -C flag (claude, gemini), set working directory via cmd.Dir // Codex passes workdir via -C flag, so we skip setting Dir for it to avoid conflicts if cfg.Mode != "resume" && commandName != "codex" && cfg.WorkDir != "" {