Files
myclaude/codeagent-wrapper/backend.go
ben 4d69c8aef1 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>
2025-12-21 20:16:57 +08:00

136 lines
3.1 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
)
// Backend defines the contract for invoking different AI CLI backends.
// Each backend is responsible for supplying the executable command and
// building the argument list based on the wrapper config.
type Backend interface {
Name() string
BuildArgs(cfg *Config, targetArg string) []string
Command() string
}
type CodexBackend struct{}
func (CodexBackend) Name() string { return "codex" }
func (CodexBackend) Command() string {
return "codex"
}
func (CodexBackend) BuildArgs(cfg *Config, targetArg string) []string {
return buildCodexArgs(cfg, targetArg)
}
type ClaudeBackend struct{}
func (ClaudeBackend) Name() string { return "claude" }
func (ClaudeBackend) Command() string {
return "claude"
}
func (ClaudeBackend) BuildArgs(cfg *Config, targetArg string) []string {
return buildClaudeArgs(cfg, targetArg)
}
const maxClaudeSettingsBytes = 1 << 20 // 1MB
// loadMinimalEnvSettings 从 ~/.claude/setting.json 只提取 env 配置。
// 只接受字符串类型的值;文件缺失/解析失败/超限都返回空。
func loadMinimalEnvSettings() map[string]string {
home, err := os.UserHomeDir()
if err != nil || home == "" {
return nil
}
settingPath := filepath.Join(home, ".claude", "setting.json")
info, err := os.Stat(settingPath)
if err != nil || info.Size() > maxClaudeSettingsBytes {
return nil
}
data, err := os.ReadFile(settingPath)
if err != nil {
return nil
}
var cfg struct {
Env map[string]any `json:"env"`
}
if err := json.Unmarshal(data, &cfg); err != nil {
return nil
}
if len(cfg.Env) == 0 {
return nil
}
env := make(map[string]string, len(cfg.Env))
for k, v := range cfg.Env {
s, ok := v.(string)
if !ok {
continue
}
env[k] = s
}
if len(env) == 0 {
return nil
}
return env
}
func buildClaudeArgs(cfg *Config, targetArg string) []string {
if cfg == nil {
return nil
}
args := []string{"-p"}
if cfg.SkipPermissions {
args = append(args, "--dangerously-skip-permissions")
}
// Prevent infinite recursion: disable all setting sources (user, project, local)
// This ensures a clean execution environment without CLAUDE.md or skills that would trigger codeagent
args = append(args, "--setting-sources", "")
if cfg.Mode == "resume" {
if cfg.SessionID != "" {
// Claude CLI uses -r <session_id> for resume.
args = append(args, "-r", cfg.SessionID)
}
}
// Note: claude CLI doesn't support -C flag; workdir set via cmd.Dir
args = append(args, "--output-format", "stream-json", "--verbose", targetArg)
return args
}
type GeminiBackend struct{}
func (GeminiBackend) Name() string { return "gemini" }
func (GeminiBackend) Command() string {
return "gemini"
}
func (GeminiBackend) BuildArgs(cfg *Config, targetArg string) []string {
return buildGeminiArgs(cfg, targetArg)
}
func buildGeminiArgs(cfg *Config, targetArg string) []string {
if cfg == nil {
return nil
}
args := []string{"-o", "stream-json", "-y"}
if cfg.Mode == "resume" {
if cfg.SessionID != "" {
args = append(args, "-r", cfg.SessionID)
}
}
// Note: gemini CLI doesn't support -C flag; workdir set via cmd.Dir
args = append(args, "-p", targetArg)
return args
}