Skip to content

Commit caddd38

Browse files
authored
Merge pull request #86 from guyskk/fix/stale-env-override
fix: override stale ANTHROPIC_*/CLAUDE_* keys in settings.json via --settings
2 parents dde884c + 64c9f63 commit caddd38

2 files changed

Lines changed: 136 additions & 5 deletions

File tree

internal/cli/exec.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,21 +144,47 @@ func filterEnvVars(env []string, shouldKeep func(string) bool) []string {
144144

145145
// buildProviderSettingsJSON serializes provider env into a JSON string
146146
// suitable for passing to claude --settings.
147+
//
148+
// It also loads settings.json to detect ANTHROPIC_*/CLAUDE_* keys that the
149+
// provider does not define, and sets them to empty string in the --settings JSON.
150+
// This prevents stale values in settings.json (e.g. ANTHROPIC_MODEL from a
151+
// previous provider) from leaking into the current session.
152+
//
147153
// Environment variable references like ${VAR} are expanded before serialization.
148-
// Returns empty string if providerEnv is nil or empty.
154+
// Returns empty string if the resulting env map is empty.
149155
func buildProviderSettingsJSON(providerEnv map[string]interface{}) (string, error) {
150156
if len(providerEnv) == 0 {
151157
return "", nil
152158
}
153159

154-
// Expand env var references
155-
expanded := make(map[string]interface{}, len(providerEnv))
160+
// Start with provider env, expanding ${VAR} references
161+
settingsEnv := make(map[string]interface{}, len(providerEnv))
156162
for k, v := range providerEnv {
157-
expanded[k] = os.ExpandEnv(fmt.Sprintf("%v", v))
163+
settingsEnv[k] = os.ExpandEnv(fmt.Sprintf("%v", v))
164+
}
165+
166+
// Load settings.json to detect potential conflict keys.
167+
// For any ANTHROPIC_*/CLAUDE_* key in settings.json that the provider does NOT
168+
// define, set it to empty string in --settings to prevent stale values from
169+
// leaking into the session (e.g. ANTHROPIC_MODEL from a previous provider).
170+
userSettings, err := config.LoadSettings()
171+
if err == nil && userSettings != nil {
172+
userEnvMap := config.GetEnv(userSettings)
173+
for key := range userEnvMap {
174+
if strings.HasPrefix(key, "ANTHROPIC_") || strings.HasPrefix(key, "CLAUDE_") {
175+
if _, providerHasKey := settingsEnv[key]; !providerHasKey {
176+
settingsEnv[key] = ""
177+
}
178+
}
179+
}
180+
}
181+
182+
if len(settingsEnv) == 0 {
183+
return "", nil
158184
}
159185

160186
settings := map[string]interface{}{
161-
"env": expanded,
187+
"env": settingsEnv,
162188
}
163189
data, err := json.Marshal(settings)
164190
if err != nil {

internal/cli/exec_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ func TestBuildProviderSettingsJSON(t *testing.T) {
101101
}
102102

103103
func TestBuildProviderSettingsJSON_ExpandsEnvVars(t *testing.T) {
104+
cleanup := setupTestDir(t)
105+
defer cleanup()
106+
104107
// Set a test env var
105108
t.Setenv("CCC_TEST_BASE_URL", "https://expanded.example.com")
106109

@@ -124,3 +127,105 @@ func TestBuildProviderSettingsJSON_ExpandsEnvVars(t *testing.T) {
124127
t.Errorf("expected env var to be expanded, got: %s", got)
125128
}
126129
}
130+
131+
func TestBuildProviderSettingsJSON_OverridesStaleKeys(t *testing.T) {
132+
cleanup := setupTestDir(t)
133+
defer cleanup()
134+
135+
// settings.json has model keys from a previous provider
136+
writeSettingsJSON(t, `{
137+
"env": {
138+
"ANTHROPIC_MODEL": "mimo-v2.5-pro[1m]",
139+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "mimo-v2.5[1m]",
140+
"ANTHROPIC_DEFAULT_OPUS_MODEL": "mimo-v2.5-pro[1m]",
141+
"ANTHROPIC_DEFAULT_SONNET_MODEL": "mimo-v2.5-pro[1m]",
142+
"ANTHROPIC_AUTH_TOKEN": "old-token",
143+
"MY_CUSTOM_VAR": "keep-me"
144+
}
145+
}`)
146+
147+
// Provider only defines AUTH_TOKEN and BASE_URL (like tokenswitch)
148+
providerEnv := map[string]interface{}{
149+
"ANTHROPIC_AUTH_TOKEN": "new-token",
150+
"ANTHROPIC_BASE_URL": "https://example.com",
151+
}
152+
153+
result, err := buildProviderSettingsJSON(providerEnv)
154+
if err != nil {
155+
t.Fatalf("unexpected error: %v", err)
156+
}
157+
158+
var parsed map[string]interface{}
159+
if err := json.Unmarshal([]byte(result), &parsed); err != nil {
160+
t.Fatalf("result is not valid JSON: %v", err)
161+
}
162+
163+
envMap := parsed["env"].(map[string]interface{})
164+
165+
// Provider keys should have provider values
166+
if envMap["ANTHROPIC_AUTH_TOKEN"] != "new-token" {
167+
t.Errorf("ANTHROPIC_AUTH_TOKEN = %v, want new-token", envMap["ANTHROPIC_AUTH_TOKEN"])
168+
}
169+
if envMap["ANTHROPIC_BASE_URL"] != "https://example.com" {
170+
t.Errorf("ANTHROPIC_BASE_URL = %v, want https://example.com", envMap["ANTHROPIC_BASE_URL"])
171+
}
172+
173+
// Stale model keys should be overridden to empty string
174+
if envMap["ANTHROPIC_MODEL"] != "" {
175+
t.Errorf("ANTHROPIC_MODEL = %v, want empty string", envMap["ANTHROPIC_MODEL"])
176+
}
177+
if envMap["ANTHROPIC_DEFAULT_HAIKU_MODEL"] != "" {
178+
t.Errorf("ANTHROPIC_DEFAULT_HAIKU_MODEL = %v, want empty string", envMap["ANTHROPIC_DEFAULT_HAIKU_MODEL"])
179+
}
180+
if envMap["ANTHROPIC_DEFAULT_OPUS_MODEL"] != "" {
181+
t.Errorf("ANTHROPIC_DEFAULT_OPUS_MODEL = %v, want empty string", envMap["ANTHROPIC_DEFAULT_OPUS_MODEL"])
182+
}
183+
if envMap["ANTHROPIC_DEFAULT_SONNET_MODEL"] != "" {
184+
t.Errorf("ANTHROPIC_DEFAULT_SONNET_MODEL = %v, want empty string", envMap["ANTHROPIC_DEFAULT_SONNET_MODEL"])
185+
}
186+
187+
// Non-ANTHROPIC/CLAUDE keys should NOT be in --settings
188+
if _, exists := envMap["MY_CUSTOM_VAR"]; exists {
189+
t.Error("MY_CUSTOM_VAR should not be in --settings (not ANTHROPIC_*/CLAUDE_*)")
190+
}
191+
}
192+
193+
func TestBuildProviderSettingsJSON_ProviderOverridesNotNulled(t *testing.T) {
194+
cleanup := setupTestDir(t)
195+
defer cleanup()
196+
197+
// settings.json has model keys
198+
writeSettingsJSON(t, `{
199+
"env": {
200+
"ANTHROPIC_MODEL": "old-model",
201+
"ANTHROPIC_BASE_URL": "http://old"
202+
}
203+
}`)
204+
205+
// Provider defines ALL keys — should NOT be set to empty
206+
providerEnv := map[string]interface{}{
207+
"ANTHROPIC_AUTH_TOKEN": "new-token",
208+
"ANTHROPIC_BASE_URL": "https://new.example.com",
209+
"ANTHROPIC_MODEL": "new-model",
210+
}
211+
212+
result, err := buildProviderSettingsJSON(providerEnv)
213+
if err != nil {
214+
t.Fatalf("unexpected error: %v", err)
215+
}
216+
217+
var parsed map[string]interface{}
218+
if err := json.Unmarshal([]byte(result), &parsed); err != nil {
219+
t.Fatalf("result is not valid JSON: %v", err)
220+
}
221+
222+
envMap := parsed["env"].(map[string]interface{})
223+
224+
// Provider values should win, not empty string
225+
if envMap["ANTHROPIC_MODEL"] != "new-model" {
226+
t.Errorf("ANTHROPIC_MODEL = %v, want new-model", envMap["ANTHROPIC_MODEL"])
227+
}
228+
if envMap["ANTHROPIC_BASE_URL"] != "https://new.example.com" {
229+
t.Errorf("ANTHROPIC_BASE_URL = %v, want https://new.example.com", envMap["ANTHROPIC_BASE_URL"])
230+
}
231+
}

0 commit comments

Comments
 (0)