Compare commits

..

3 Commits

Author SHA1 Message Date
cexll
8252b67567 fix(executor): filter codex_core stderr noise 2026-02-18 16:32:59 +08:00
ben
207d3c5436 Merge pull request #147 from AsakiEmura/fix/unset-claudecode-env
fix(executor): unset CLAUDECODE env to prevent nested session rejection
2026-02-18 16:32:18 +08:00
lza
5fe8c24f55 fix(executor): unset CLAUDECODE env to prevent nested session rejection
Claude Code v2.1.41+ sets CLAUDECODE=1 in all child Bash processes and
rejects startup when the variable is present. When codeagent-wrapper
spawns `claude -p` as a subprocess, it inherits this variable and gets
blocked with "cannot be launched inside another Claude Code session".

Add UnsetEnv method to commandRunner interface and strip CLAUDECODE
before spawning the claude backend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:56:25 +08:00
6 changed files with 91 additions and 2 deletions

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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 != "" {

View File

@@ -20,8 +20,7 @@ var geminiNoisePatterns = []string{
// codexNoisePatterns contains stderr patterns to filter for codex backend
var codexNoisePatterns = []string{
"ERROR codex_core::codex: needs_follow_up:",
"ERROR codex_core::skills::loader:",
"ERROR codex_core::",
}
// filteringWriter wraps an io.Writer and filters out lines matching patterns

View File

@@ -71,3 +71,35 @@ func TestFilteringWriterPartialLines(t *testing.T) {
t.Errorf("got %q, want %q", got, "Hello World\n")
}
}
func TestFilteringWriterCodexNoise(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "filter all codex_core errors",
input: "ERROR codex_core::rollout::list: state db missing rollout path for thread 123\nERROR codex_core::skills::loader: missing skill\nVisible output\n",
want: "Visible output\n",
},
{
name: "keep non codex_core errors",
input: "ERROR another_module::state: real failure\nERROR codex_core::codex: needs_follow_up: true\nDone\n",
want: "ERROR another_module::state: real failure\nDone\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
fw := newFilteringWriter(&buf, codexNoisePatterns)
_, _ = fw.Write([]byte(tt.input))
fw.Flush()
if got := buf.String(); got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}