Skip to content

Commit 34dc00f

Browse files
authored
Merge pull request #76 from guyskk/fix/settings-env-handling
fix: preserve user-defined env in settings.json
2 parents 8f0bf0c + c71511a commit 34dc00f

5 files changed

Lines changed: 455 additions & 222 deletions

File tree

docs/settings-merge-strategy.md

Lines changed: 93 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,24 @@ ccc 不应该:
7171

7272
## 字段处理策略
7373

74-
### 1. env 字段 - 特殊处理
74+
### 1. env 字段 - 分离处理
7575

76-
**处理方式**清空特定键,避免配置冲突
76+
**处理方式**区分"用户 env"和"ccc env",分别写入 settings.json 和子进程
7777

78-
需要清空的键:
79-
1. 特定前缀:`ANTHROPIC_*``CLAUDE_*`
80-
2. 与 provider env 相同的 key
78+
**写入 settings.json 的 env**
79+
- 只保留用户在 settings.json 中定义的 env key
80+
- 排除与 base/provider env 冲突的 key
81+
- 排除 `ANTHROPIC_*`/`CLAUDE_*` 前缀的 key
82+
- 如果过滤后为空,不写 env 字段
83+
84+
**传递给子进程的 env**
85+
- 只包含 base + provider 的 env
86+
- 不包含用户 settings.json 的 env(Claude Code 自己从 settings.json 读取)
8187

8288
**原因**
8389
- provider 的环境变量通过命令行传递给 claude 子进程
84-
- 如果 settings.json 中保留这些键,会产生不确定性(不确定哪边生效)
85-
- 清空后确保 provider env 的行为可预测
90+
- 用户自定义的非冲突 env 需要保留在 settings.json 中供 Claude Code 使用
91+
- 子进程只需 base + provider env,避免重复
8692

8793
**示例**
8894

@@ -92,22 +98,39 @@ ccc 不应该:
9298
"env": {
9399
"ANTHROPIC_MODEL": "claude-3.7-sonnet",
94100
"MY_CUSTOM_VAR": "value",
95-
"ANTHROPIC_BASE_URL": "old-url"
101+
"DISABLE_TELEMETRY": "1"
96102
}
97103
}
98104

105+
// base env
106+
{
107+
"API_TIMEOUT": "30000",
108+
"DISABLE_TELEMETRY": "1"
109+
}
110+
99111
// provider env
100112
{
101113
"ANTHROPIC_BASE_URL": "https://open.bigmodel.cn/api/anthropic",
102114
"ANTHROPIC_AUTH_TOKEN": "token123",
103115
"ANTHROPIC_MODEL": "glm-4.7"
104116
}
105117

106-
// 处理后
118+
// 写入 settings.json 的 env
107119
{
108120
"env": {
109-
"MY_CUSTOM_VAR": "value" // 保留(非 ANTHROPIC_* 且非 provider key
121+
"MY_CUSTOM_VAR": "value" // 保留(非冲突、非 ANTHROPIC_*/CLAUDE_*
110122
}
123+
// DISABLE_TELEMETRY 被过滤(与 base env 冲突)
124+
// ANTHROPIC_MODEL 被过滤(ANTHROPIC_* 前缀)
125+
}
126+
127+
// 传递给子进程的 env(base + provider)
128+
{
129+
"API_TIMEOUT": "30000",
130+
"DISABLE_TELEMETRY": "1",
131+
"ANTHROPIC_BASE_URL": "https://open.bigmodel.cn/api/anthropic",
132+
"ANTHROPIC_AUTH_TOKEN": "token123",
133+
"ANTHROPIC_MODEL": "glm-4.7"
111134
}
112135
```
113136

@@ -237,33 +260,44 @@ func LoadSettings() (map[string]interface{}, error)
237260

238261
---
239262

240-
### 2. CleanEnvInSettings()
263+
### 2. FilterUserEnvForSettings()
241264

242-
**描述**清空 settings.env 中的特定环境变量键
265+
**描述**过滤用户自定义 env,只保留安全的 key
243266

244267
**签名**
245268
```go
246-
// CleanEnvInSettings removes specific environment variable keys from settings.env.
247-
// It removes:
248-
// 1. Keys with specific prefixes (ANTHROPIC_*, CLAUDE_*)
249-
// 2. Keys that match provider env keys
250-
// Returns a new map without modifying the input.
251-
func CleanEnvInSettings(settings map[string]interface{}, providerEnvKeys []string) map[string]interface{}
269+
// FilterUserEnvForSettings filters user-defined env to only keep safe keys.
270+
// Removes keys in managedEnvKeys or with ANTHROPIC_*/CLAUDE_* prefix.
271+
// Returns nil if no keys remain.
272+
func FilterUserEnvForSettings(userEnv map[string]interface{}, managedEnvKeys map[string]bool) map[string]interface{}
252273
```
253274

254275
**逻辑**
255-
1. 深拷贝 settings(不修改输入)
256-
2. 获取 `env` map(不存在则跳过)
257-
3. 遍历每个 key
258-
4. 删除满足以下任一条件的 key:
259-
-`ANTHROPIC_` 开头
260-
-`CLAUDE_` 开头
261-
- 存在于 `providerEnvKeys` 列表中
262-
5. 返回新的 map
276+
1. 遍历 userEnv 的每个 key
277+
2. 跳过在 managedEnvKeys 中的 key(与 base/provider 冲突)
278+
3. 跳过 `ANTHROPIC_*`/`CLAUDE_*` 前缀的 key
279+
4. 如果过滤后为空,返回 nil
263280

264281
---
265282

266-
### 3. MergeWithPriority()
283+
### 3. MergeEnvMaps()
284+
285+
**描述**:合并多个 env map,后者覆盖前者。
286+
287+
**签名**
288+
```go
289+
// MergeEnvMaps merges multiple env maps. Later maps override earlier ones.
290+
func MergeEnvMaps(maps ...map[string]interface{}) map[string]interface{}
291+
```
292+
293+
**逻辑**
294+
1. 遍历所有 map,依次合并
295+
2. nil map 被跳过
296+
3. 如果结果为空,返回 nil
297+
298+
---
299+
300+
### 4. MergeWithPriority()
267301

268302
**描述**:按优先级深度合并多个配置源。
269303

@@ -289,7 +323,7 @@ func MergeWithPriority(baseSettings, providerSettings, userSettings map[string]i
289323

290324
---
291325

292-
### 4. EnsureStopHook()
326+
### 5. EnsureStopHook()
293327

294328
**描述**:确保 Supervisor Stop hook 存在于 settings 中。
295329

@@ -323,22 +357,30 @@ func EnsureStopHook(settings map[string]interface{}, hookCommand string) map[str
323357
├─→ baseSettings = cfg.Settings
324358
├─→ providerSettings = cfg.Providers[providerName]
325359
326-
├─→ 提取 provider env keys
360+
├─→ 提取各来源 env(合并前)
361+
│ ├─→ userEnvMap = GetEnv(userSettings)
362+
│ ├─→ baseEnvMap = GetEnv(cfg.Settings)
363+
│ └─→ providerEnvMap = GetEnv(providerSettings)
364+
365+
├─→ 构建 managedEnvKeys = base env keys + provider env keys
327366
328367
├─→ MergeWithPriority(baseSettings, providerSettings, userSettings)
329368
│ │
330369
│ └─→ merged = DeepMerge(DeepCopy(baseSettings), providerSettings)
331370
│ merged = DeepMerge(merged, userSettings) ← userSettings 优先
332371
333-
├─→ CleanEnvInSettings(merged, providerEnvKeys)
334-
│ └─→ 清空 ANTHROPIC_*, CLAUDE_*, provider env keys
335-
336372
├─→ EnsureStopHook(merged, hookCommand)
337373
│ └─→ 确保 Supervisor Stop hook 存在
338374
339-
├─→ 确保 hooks 可执行
340-
│ ├─→ merged["disableAllHooks"] = false
341-
│ └─→ merged["allowManagedHooksOnly"] = false
375+
├─→ delete(merged, "env")
376+
│ └─→ 移除合并后的 env
377+
378+
├─→ FilterUserEnvForSettings(userEnvMap, managedEnvKeys)
379+
│ └─→ 过滤用户 env,保留安全 key
380+
│ └─→ 如果有结果,写入 merged["env"]
381+
382+
├─→ MergeEnvMaps(baseEnvMap, providerEnvMap)
383+
│ └─→ 子进程 env = base + provider(不含用户 env)
342384
343385
└─→ 保存 merged 到 settings.json
344386
```
@@ -403,7 +445,7 @@ func EnsureStopHook(settings map[string]interface{}, hookCommand string) map[str
403445

404446
---
405447

406-
### 场景 3:env 字段清空
448+
### 场景 3:env 字段分离处理
407449

408450
```json
409451
// settings.json 初始内容
@@ -423,13 +465,25 @@ func EnsureStopHook(settings map[string]interface{}, hookCommand string) map[str
423465
}
424466
```
425467

426-
**处理后**
468+
**写入 settings.json**(只保留安全用户 env)
427469

428470
```json
429471
{
430472
"env": {
431-
"MY_CUSTOM_VAR": "value" // 保留(非 ANTHROPIC_* 且非 CLAUDE_* 且非 provider key
473+
"MY_CUSTOM_VAR": "value" // 保留(非冲突、非 ANTHROPIC_*/CLAUDE_*)
432474
}
475+
// ANTHROPIC_MODEL 被过滤(ANTHROPIC_* 前缀)
476+
// CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR 被过滤(CLAUDE_* 前缀)
477+
}
478+
```
479+
480+
**传递给子进程**(base + provider env):
481+
482+
```json
483+
{
484+
"ANTHROPIC_BASE_URL": "https://open.bigmodel.cn/api/anthropic",
485+
"ANTHROPIC_AUTH_TOKEN": "token123",
486+
"ANTHROPIC_MODEL": "glm-4.7"
433487
}
434488
```
435489

@@ -481,7 +535,7 @@ func EnsureStopHook(settings map[string]interface{}, hookCommand string) map[str
481535

482536
| 文件 | 修改内容 |
483537
|------|----------|
484-
| `internal/config/config.go` | 新增 LoadSettings、CleanEnvInSettings、MergeWithPriority、EnsureStopHook |
538+
| `internal/config/config.go` | 新增 LoadSettings、FilterUserEnvForSettings、MergeEnvMaps、MergeWithPriority、EnsureStopHook |
485539
| `internal/provider/provider.go` | 重写 SwitchWithHook() 函数逻辑 |
486540
| `internal/config/config_test.go` | 为新函数添加测试 |
487541

internal/config/config.go

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -250,57 +250,46 @@ func LoadSettings() (map[string]interface{}, error) {
250250
return settings, nil
251251
}
252252

253-
// CleanEnvInSettings removes specific environment variable keys from settings.env.
254-
// It removes:
255-
// 1. Keys with specific prefixes (ANTHROPIC_*, CLAUDE_*)
256-
// 2. Keys that match provider env keys
257-
//
258-
// Returns a new map without modifying the input.
259-
func CleanEnvInSettings(settings map[string]interface{}, providerEnvKeys []string) map[string]interface{} {
260-
// Deep copy to avoid modifying input
261-
result := deepCopy(settings)
262-
263-
// Get env map if it exists
264-
envVal, envExists := result["env"]
265-
if !envExists {
266-
// No env to clean
267-
return result
253+
// FilterUserEnvForSettings filters user-defined env to only keep safe keys.
254+
// It removes keys in managedEnvKeys or with ANTHROPIC_*/CLAUDE_* prefix.
255+
// Returns nil if no keys remain.
256+
func FilterUserEnvForSettings(userEnv map[string]interface{}, managedEnvKeys map[string]bool) map[string]interface{} {
257+
if userEnv == nil {
258+
return nil
268259
}
269260

270-
env, ok := envVal.(map[string]interface{})
271-
if !ok {
272-
// env is not a map, nothing to clean
273-
return result
261+
filtered := make(map[string]interface{})
262+
for key, value := range userEnv {
263+
if managedEnvKeys[key] {
264+
continue
265+
}
266+
if strings.HasPrefix(key, "ANTHROPIC_") || strings.HasPrefix(key, "CLAUDE_") {
267+
continue
268+
}
269+
filtered[key] = value
274270
}
275271

276-
// Build set of keys to remove for O(1) lookup
277-
keysToRemove := make(map[string]bool)
278-
for _, key := range providerEnvKeys {
279-
keysToRemove[key] = true
272+
if len(filtered) == 0 {
273+
return nil
280274
}
275+
return filtered
276+
}
281277

282-
// Remove keys from env
283-
for key := range env {
284-
shouldRemove := false
285-
286-
// Check for specific prefixes
287-
if strings.HasPrefix(key, "ANTHROPIC_") || strings.HasPrefix(key, "CLAUDE_") {
288-
shouldRemove = true
289-
}
290-
291-
// Check for provider env keys
292-
if keysToRemove[key] {
293-
shouldRemove = true
278+
// MergeEnvMaps merges multiple env maps. Later maps override earlier ones.
279+
// Returns nil if no maps have entries.
280+
func MergeEnvMaps(maps ...map[string]interface{}) map[string]interface{} {
281+
result := make(map[string]interface{})
282+
for _, m := range maps {
283+
if m == nil {
284+
continue
294285
}
295-
296-
if shouldRemove {
297-
delete(env, key)
286+
for k, v := range m {
287+
result[k] = v
298288
}
299289
}
300-
301-
// Update env in result (always update to preserve the structure)
302-
result["env"] = env
303-
290+
if len(result) == 0 {
291+
return nil
292+
}
304293
return result
305294
}
306295

0 commit comments

Comments
 (0)