fix: allow claude backend to read env from setting.json while preventing recursion (#92)

* fix: allow claude backend to read env from setting.json while preventing recursion

Fixes #89

Problem:
- --setting-sources "" prevents claude from reading ~/.claude/setting.json env
- Removing it causes infinite recursion via skills/commands/agents loading

Solution:
- Keep --setting-sources "" to block all config sources
- Add loadMinimalEnvSettings() to extract only env from setting.json
- Pass env explicitly via --settings parameter
- Update tests to validate dynamic --settings parameter

Benefits:
- Claude backend can access ANTHROPIC_API_KEY and other env vars
- Skills/commands/agents remain blocked, preventing recursion
- Graceful degradation if setting.json doesn't exist

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>

* security: pass env via process environment instead of command line

Critical security fix for issue #89:
- Prevents ANTHROPIC_API_KEY leakage in process command line (ps)
- Prevents sensitive values from being logged in wrapper logs

Changes:
1. executor.go:
   - Add SetEnv() method to commandRunner interface
   - realCmd merges env with os.Environ() and sets to cmd.Env
   - All test mocks implement SetEnv()

2. backend.go:
   - Change loadMinimalEnvSettings() to return map[string]string
   - Use os.UserHomeDir() instead of os.Getenv("HOME")
   - Add 1MB file size limit check
   - Only accept string values in env (reject non-strings)
   - Remove --settings parameter (no longer in command line)

3. Tests:
   - Add loadMinimalEnvSettings() unit tests
   - Remove --settings validation (no longer in args)
   - All test mocks implement SetEnv()

Security improvements:
- No sensitive values in argv (safe from ps/logs)
- Type-safe env parsing (string-only)
- File size limit prevents memory issues
- Graceful degradation if setting.json missing

Tests: All pass (30.912s)

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>

---------

Co-authored-by: SWE-Agent.ai <noreply@swe-agent.ai>
This commit is contained in:
ben
2025-12-21 20:16:57 +08:00
committed by GitHub
parent eec844d850
commit 4d69c8aef1
5 changed files with 207 additions and 12 deletions

View File

@@ -1,7 +1,9 @@
package main
import (
"bytes"
"os"
"path/filepath"
"reflect"
"testing"
)
@@ -148,3 +150,64 @@ func TestClaudeBuildArgs_BackendMetadata(t *testing.T) {
}
}
}
func TestLoadMinimalEnvSettings(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Run("missing file returns empty", func(t *testing.T) {
if got := loadMinimalEnvSettings(); len(got) != 0 {
t.Fatalf("got %v, want empty", got)
}
})
t.Run("valid env returns string map", func(t *testing.T) {
dir := filepath.Join(home, ".claude")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
path := filepath.Join(dir, "setting.json")
data := []byte(`{"env":{"ANTHROPIC_API_KEY":"secret","FOO":"bar"}}`)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
got := loadMinimalEnvSettings()
if got["ANTHROPIC_API_KEY"] != "secret" || got["FOO"] != "bar" {
t.Fatalf("got %v, want keys present", got)
}
})
t.Run("non-string values are ignored", func(t *testing.T) {
dir := filepath.Join(home, ".claude")
path := filepath.Join(dir, "setting.json")
data := []byte(`{"env":{"GOOD":"ok","BAD":123,"ALSO_BAD":true}}`)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
got := loadMinimalEnvSettings()
if got["GOOD"] != "ok" {
t.Fatalf("got %v, want GOOD=ok", got)
}
if _, ok := got["BAD"]; ok {
t.Fatalf("got %v, want BAD omitted", got)
}
if _, ok := got["ALSO_BAD"]; ok {
t.Fatalf("got %v, want ALSO_BAD omitted", got)
}
})
t.Run("oversized file returns empty", func(t *testing.T) {
dir := filepath.Join(home, ".claude")
path := filepath.Join(dir, "setting.json")
data := bytes.Repeat([]byte("a"), maxClaudeSettingsBytes+1)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if got := loadMinimalEnvSettings(); len(got) != 0 {
t.Fatalf("got %v, want empty", got)
}
})
}