Skip to content

Commit 35c22fe

Browse files
author
SqlRush
committed
Validate settings file edits
1 parent 2afc153 commit 35c22fe

6 files changed

Lines changed: 154 additions & 12 deletions

File tree

‎docs/cc-100-roadmap.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
| Anthropic API | 已有 streaming、retry、usage/cost、beta header、dump、prompt cache 基础 |
2828
| Conversation loop | 已有 tool loop、fallback、stream aggregation、transcript append |
2929
| Tool runtime | 已有 registry、executor、hooks 框架、并发分区、权限判定、结果截断 |
30-
| 文件工具 M5 初版 | 已完成文本版 `Read`、image Read 初版、notebook Read 初版、Read 大文本预算截断/落盘、`Write`、`Edit`,含读前写、mtime stale guard、`replace_all`、structured diff、Read 去重 |
30+
| 文件工具 M5 初版 | 已完成文本版 `Read`、image Read 初版、notebook Read 初版、Read 大文本预算截断/落盘、`Write`、`Edit`,含读前写、mtime stale guard、`replace_all`、structured diff、`.claude/settings*.json` 写前校验、team-memory secret guard、Read 去重 |
3131
| M6 初始上下文层 | 已完成 CLAUDE.md/memdir 扫描、memory manifest、team-memory secret guard、compact threshold/prompt/runner/boundary plan、conversation auto-compact 接入、失败熔断、microcompact/cache 初版、persistent cached microcompact 初版、cache digest structural/rich-content metadata 覆盖、cache version/TTL/prune、in-memory micro cache prune、memory-cache write-through 到磁盘、atomic cache write、坏缓存默认 fail-open、session memory summary/frontmatter aliases/recall 初版、model-ranked recall session-id selection 和 invalid-selection fallback 初版、recall agent alternate/camel response keys/fenced-prose JSON extraction/scalar id parsing/nested/wrapped/collection-alias selection parsing、resume context model-assisted recall 接入、session memory rollup/prune compaction、rollup archive exclusion/merge、rune-safe rollup truncation、resume context + session memory recall、conversation recall 注入、deterministic/model-backed memory extraction 初版、extraction agent fenced-prose JSON/wrapped facts/provider-style response wrapper/alternate/structured field/nested source object/nested response/fact kind alias parsing、turn-end memory extraction 落盘、prompt history lock/buffered flush/field aliases、official subagent transcript layout、agent metadata sidecar/field aliases、sidechain runtime start/append/finish/cancel/fail 和 parent-chain append/finish 初版、sidechain manager orchestration 初版、sidechain manifest 聚合、sidechain state/list/resume/content-field aliases 初版、sidechain resume context builder、sidechain conversation/content-replacement reconstruction、transcript tail/window/metadata/index loaders、byte-budget transcript tail loader、agent-scoped content replacement metadata/record field-alias loading、session-scoped metadata reappend including AI-title/last-prompt/task-summary、transcript line offset index/window/byte-budget-window/parent-chain/resume/tail/byte-budget-tail loaders、extended transcript metadata entries/type/field aliases、transcript message/session UUID field aliases including top-level `messageUuid`/`messageId`/`id` record IDs and `role`/`entry_type`/`messageType`/`createdAt` timestamp aliases、transcript tombstone metadata delete/relink、transcript resume conversation builder、index 文本预览和 AI-title/last-prompt/task metadata 字段、流式 transcript 搜索、session list pagination/search/title、remote history token refresh、remote history 全量分页抓取/page-field/event-list/records/entries/eventList/sessionEvents/last-id/cursor/event-id/has-next alias/wrapped-data/links/paging/bare-array/keyed event map/connection/eventConnection/sessionEventsConnection wrapper response/edge-cursor fallback/max-pages 截断状态/before_id 续抓、remote event transcript materialization/fallback field fill/去重追加/duplicate parent guard、remote history 一步 sync 到 transcript |
3232
| M7 初始 TUI 层 | 已完成轻量 terminal frame renderer、PromptInput 状态机、history 导航、ctrl-p/ctrl-n history navigation、shift-enter 多行输入、多行 prompt 行内 ctrl-a/ctrl-e/ctrl-u/ctrl-k 和 wrap/render/cursor、共享 kill ring、ctrl-b/ctrl-f/ctrl-u/ctrl-k/ctrl-w 行编辑、alt-b/alt-f/alt-d/alt-backspace word 编辑、ctrl-left/ctrl-right/alt-left/alt-right word motion、ctrl-y yank 和 alt-y yank-pop 初版、reverse-search cursor/word 编辑/kill/yank/yank-pop 初版、ctrl-c interrupt/双击退出事件、ctrl-d delete-forward/空输入双击退出事件、ctrl-l 重绘事件、ctrl-o/ctrl-t 全局切换事件、ctrl-g/ctrl-s/ctrl-x chord chat 事件、reverse-search 状态/渲染/脚本断言/空结果/选择回填/cursor 断言、paste/image hint 输入和 OSC ST/base64 filename 兼容、text/image pasted-content 引用/metadata 脚本断言/提交展开/history entry restoration、SGR mouse 解析、alternate terminal navigation key sequences including modified Home/End/Delete/PageUp/PageDown、滚轮滚动、修饰键滚轮/左键、左键拖动选择、viewport 半页/顶部/底部可配置滚动、viewport 点击选择和 dialog action 点击、focus/blur 事件、resize 视口保持、keybinding resolver/config/chord pending/null-unbind/key/action camelCase alias、JSON config loader 和 focus/mouse/paste/image key name 覆盖、vim insert/normal/j/k/word/WORD/ge/gE/line-local ^/$/0/|/I/A/D/quote/bracket text-object/yank/register/paste/delete/count/replace/undo/find/till/repeat/matching-pair %/dot-repeat/G/gg/toggle/join/open-line/indent/substitute 动作、normal-mode arrow/backspace/delete 映射和 operator linewise/字符范围、REPL screen、permission/task dialog builder、dialog kind/id routing/runtime/status line、runtime 到 REPL screen 的 dialog/status 同步、runtime-aware interaction script runner、prompt text/cursor/expanded/vim mode/register/task state/dialog result/runtime mutation/task bulk-cancel/permission cancel/keybinding mutation/status negative/snapshot negative/screen size/event-sequence/event-count/no-event/dialog-result-count/no-dialog-result 脚本断言、viewport 脚本断言、named-key 脚本输入、script JSON/JSONL/wrapper loader、script file runner 和 runtime/task camel field aliases、stale dialog race guard、cancel active、permission id/all cancellation、queued permission promotion、active task dialog refresh、task lifecycle/bulk-cancel 初版、idempotent alternate screen lifecycle/reset/reassert interactive 初版、mouse/focus/bracketed-paste terminal mode lifecycle/reconciliation、ANSI snapshot 基础、snapshot corpus write/compare/script-file compare/missing-baseline/diff/batch/strict unexpected-baseline 状态、scripted interaction runner/assertions/multi-key/text/paste/image/pasted-content metadata 初版、status/dialog/message components、viewport/selection |
3333
| 全量测试 | 当前 `go test ./...` 通过 |
@@ -510,7 +510,7 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
510510
当前状态:
511511

512512
- 文本版 `Read`、PNG/JPEG/GIF/WebP image Read、Jupyter notebook cell 渲染初版、Read 大文本 tool-result budget 截断/落盘、`Write`、`Edit` 初版已完成。
513-
- 已覆盖读前写、mtime stale guard、唯一匹配、`replace_all`、Write/Edit structured diff hunks、Read 去重、跨 tool round read-state。
513+
- 已覆盖读前写、mtime stale guard、唯一匹配、`replace_all`、Write/Edit structured diff hunks、`.claude/settings.json`/`settings.local.json` 写前 JSON/语义校验、team-memory secret guard、Read 去重、跨 tool round read-state。
514514
- `Bash` 初版已完成,支持 command/timeout/description 输入校验、`/bin/sh -c` 执行、stdout/stderr/exit code/timeout 结构化结果、动态 read-only/concurrency-safe/destructive 分类、权限规则接入、后台启动、同会话 `BashOutput` 输出读取和 `KillBash` 取消。
515515
- `Glob`/`Grep` 纯 Go 初版已完成,支持 `**` 递归 glob、基础 ignored dirs、层级 `.gitignore`/`.ignore`、mtime/path 排序、Grep regex、glob/type 过滤、`files_with_matches`/`content`/`count` 输出模式、`context`/`before_context`/`after_context` 上下文行、`offset`/`head_limit` 分页和大小写不敏感搜索。
516516
- `TodoWrite` 会话内初版已完成,支持完整 todo list 写入、状态/优先级校验、重复 id 拒绝、单个 `in_progress` 约束、结构化结果和 tool metadata 状态保存。
@@ -520,7 +520,7 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
520520
仍需完成:
521521

522522
- `Read` 的 PDF、完整 notebook parity、完整 token-budget parity、full media parity、binary edge cases。
523-
- `Edit/Write` 的完整 git diff parity、LSP/IDE notify、file history、settings validation、secret guard。
523+
- `Edit/Write` 的完整 git diff parity、LSP/IDE notify、file history 和更广义 secret guard。
524524
- `Bash` 完整 shell parser、真实 sandbox、interrupt、后台任务完整生命周期、更细 read-only/destructive validation 和官方 golden 兼容。
525525
- `Glob/Grep` 完整 ripgrep parity 和剩余输出参数。
526526
- `TodoWrite` 跨会话恢复、TUI 同步和官方 golden 兼容。

‎docs/claude-code-go-rewrite-plan.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ test/parity/ # golden tests against TS/official behavior
166166

167167
当前进度:
168168

169-
- Read/Edit/Write 初版已落地,覆盖文本 Read、PNG/JPEG/GIF/WebP image Read、Jupyter notebook cell 渲染初版、Read 大文本 tool-result budget 截断/落盘、read-before-write、mtime stale guard、唯一匹配、`replace_all`、Write/Edit structured diff hunks、Read 去重和跨 tool round read-state。
169+
- Read/Edit/Write 初版已落地,覆盖文本 Read、PNG/JPEG/GIF/WebP image Read、Jupyter notebook cell 渲染初版、Read 大文本 tool-result budget 截断/落盘、read-before-write、mtime stale guard、唯一匹配、`replace_all`、Write/Edit structured diff hunks、`.claude/settings.json`/`settings.local.json` 写前 JSON/语义校验、team-memory secret guard、Read 去重和跨 tool round read-state。
170170
- Bash 初版已落地,覆盖 command/timeout/description 输入校验、`/bin/sh -c` 执行、stdout/stderr/exit code/timeout 结构化结果、动态 read-only/concurrency-safe/destructive 分类、权限规则接入、后台启动、同会话 `BashOutput` 输出读取和 `KillBash` 取消;完整 shell parser、真实 sandbox、interrupt、后台任务完整生命周期和官方 golden 仍需继续补。
171171
- Glob/Grep 纯 Go 初版已落地,覆盖 `**` 递归 glob、基础 ignored dirs、层级 `.gitignore`/`.ignore`、mtime/path 排序、Grep regex、glob/type 过滤、`files_with_matches`/`content`/`count` 输出模式、`context`/`before_context`/`after_context` 上下文行、`offset`/`head_limit` 分页和大小写不敏感搜索;完整 ripgrep parity 和剩余输出参数仍需继续补。
172172
- TodoWrite 会话内初版已落地,覆盖完整 todo list 写入、状态/优先级校验、重复 id 拒绝、单个 `in_progress` 约束、结构化结果和 tool metadata 状态保存;跨会话恢复、TUI 同步和官方 golden 仍需继续补。

‎docs/first-second-parity-audit.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,15 @@ Covered behavior:
111111
- `WebFetch` initial URL fetch behavior, including URL/timeout/max-byte validation, HTTP GET, text/binary detection, truncation, non-2xx error marking, structured result payloads, and `WebFetch(domain:...)` permission-rule adaptation.
112112
- `WebSearch` initial HTML-search adapter, including query/max-result/timeout/domain-filter validation, injectable search endpoint, DuckDuckGo HTML link parsing, domain allow/block filtering, structured result payloads, and query-based permission-rule matching.
113113
- `Read` line-number formatting, offset/limit slicing, mtime-based same-range dedup, text/binary/device guards, PNG/JPEG/GIF/WebP image content-block reads, Jupyter notebook cell rendering, large text tool-result truncation/persistence, and read-state recording.
114-
- `Write` create/update behavior, read-before-write validation for existing files, mtime stale detection, structured diff hunks, and post-write read-state refresh.
115-
- `Edit` exact replacement, nonexistent-file creation with empty `old_string`, unique-match enforcement, `replace_all`, quote-style preservation for curly quotes, CRLF preservation, structured diff hunks, and post-edit read-state refresh.
114+
- `Write` create/update behavior, read-before-write validation for existing files, mtime stale detection, `.claude/settings.json` and `settings.local.json` JSON/semantic validation before writes, structured diff hunks, and post-write read-state refresh.
115+
- `Edit` exact replacement, nonexistent-file creation with empty `old_string`, unique-match enforcement, `replace_all`, quote-style preservation for curly quotes, CRLF preservation, `.claude/settings.json` and `settings.local.json` final-content validation before writes, structured diff hunks, and post-edit read-state refresh.
116116
- `conversation.Runner` now preserves tool metadata across tool rounds so a `Read` in one tool round can authorize a later `Edit`/`Write`.
117117

118118
Still missing from full M5 parity:
119119

120120
- PDF, fuller notebook parity, fuller token-budget parity, and fuller media parity in `Read`.
121121
- Fuller git diff parity plus LSP/IDE notifications/file-history integration for `Write`/`Edit`.
122-
- Settings-file validation, team-memory secret guard, skill activation, and full permission prompt rendering.
122+
- Skill activation, full permission prompt rendering, and broader secret guard parity beyond the current team-memory write guard.
123123
- Complete `Bash` parser/sandbox/interrupt/background lifecycle/golden parity, `WebFetch` prompt-aware/browser/preflight/golden parity, `WebSearch` official backend/ranking/snippet/golden parity, notebook, PowerShell, and MCP concrete tool semantics, plus remaining ripgrep parity/output behavior for `Glob`/`Grep` and cross-session/TUI/golden compatibility for `TodoWrite`.
124124

125125
## M6/M7 Initial Progress

‎internal/config/validation.go‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,18 @@ func LoadSettingsFileWithWarnings(path string) (contracts.Settings, []Validation
2828
if err != nil {
2929
return contracts.Settings{}, nil, err
3030
}
31+
return ParseSettingsJSON(data, path)
32+
}
33+
34+
func ParseSettingsJSON(data []byte, filePath string) (contracts.Settings, []ValidationError, error) {
3135
if len(strings.TrimSpace(string(data))) == 0 {
3236
return contracts.Settings{}, nil, nil
3337
}
3438
var raw map[string]any
3539
if err := json.Unmarshal(data, &raw); err != nil {
3640
return contracts.Settings{}, nil, err
3741
}
38-
warnings := FilterInvalidPermissionRules(raw, path)
42+
warnings := FilterInvalidPermissionRules(raw, filePath)
3943
normalized, err := json.Marshal(raw)
4044
if err != nil {
4145
return contracts.Settings{}, warnings, err
@@ -44,10 +48,15 @@ func LoadSettingsFileWithWarnings(path string) (contracts.Settings, []Validation
4448
if err := json.Unmarshal(normalized, &settings); err != nil {
4549
return contracts.Settings{}, warnings, err
4650
}
47-
warnings = append(warnings, ValidateSettings(settings, path)...)
51+
warnings = append(warnings, ValidateSettings(settings, filePath)...)
4852
return settings, warnings, nil
4953
}
5054

55+
func ValidateSettingsJSON(data []byte, filePath string) ([]ValidationError, error) {
56+
_, warnings, err := ParseSettingsJSON(data, filePath)
57+
return warnings, err
58+
}
59+
5160
func FilterInvalidPermissionRules(data map[string]any, filePath string) []ValidationError {
5261
if data == nil {
5362
return nil

‎internal/tools/file/tools.go‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
"ccgo/internal/config"
1314
"ccgo/internal/contracts"
1415
"ccgo/internal/memory"
1516
"ccgo/internal/permissions"
@@ -447,6 +448,9 @@ func validateWrite(ctx tool.Context, raw json.RawMessage) error {
447448
if err := memory.GuardTeamMemoryWrite(path, input.Content); err != nil {
448449
return err
449450
}
451+
if err := validateSettingsFileContent(path, input.Content); err != nil {
452+
return err
453+
}
450454
if info, err := os.Stat(path); err == nil {
451455
if info.IsDir() {
452456
return fmt.Errorf("cannot write directory: %s", input.FilePath)
@@ -458,6 +462,44 @@ func validateWrite(ctx tool.Context, raw json.RawMessage) error {
458462
return nil
459463
}
460464

465+
func validateSettingsFileContent(path string, content string) error {
466+
if !isClaudeSettingsPath(path) {
467+
return nil
468+
}
469+
warnings, err := config.ValidateSettingsJSON([]byte(content), path)
470+
if err != nil {
471+
return fmt.Errorf("invalid settings file: %w", err)
472+
}
473+
if len(warnings) > 0 {
474+
return fmt.Errorf("invalid settings file: %s", formatSettingsValidationWarning(warnings[0], len(warnings)))
475+
}
476+
return nil
477+
}
478+
479+
func isClaudeSettingsPath(path string) bool {
480+
clean := filepath.Clean(path)
481+
name := filepath.Base(clean)
482+
if name != "settings.json" && name != "settings.local.json" {
483+
return false
484+
}
485+
return filepath.Base(filepath.Dir(clean)) == ".claude"
486+
}
487+
488+
func formatSettingsValidationWarning(warning config.ValidationError, total int) string {
489+
path := warning.Path
490+
if path == "" {
491+
path = filepath.Base(warning.File)
492+
}
493+
message := warning.Message
494+
if message == "" {
495+
message = "settings validation failed"
496+
}
497+
if total > 1 {
498+
return fmt.Sprintf("%s: %s (and %d more)", path, message, total-1)
499+
}
500+
return fmt.Sprintf("%s: %s", path, message)
501+
}
502+
461503
func callWrite(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contracts.ToolResult, error) {
462504
input, err := decodeWrite(raw)
463505
if err != nil {
@@ -530,15 +572,15 @@ func validateEdit(ctx tool.Context, raw json.RawMessage) error {
530572
}
531573
if !existed {
532574
if input.OldString == "" {
533-
return nil
575+
return validateSettingsFileContent(path, input.NewString)
534576
}
535577
return fmt.Errorf("File does not exist.")
536578
}
537579
if input.OldString == "" {
538580
if strings.TrimSpace(content) != "" {
539581
return fmt.Errorf("Cannot create new file - file already exists.")
540582
}
541-
return nil
583+
return validateSettingsFileContent(path, input.NewString)
542584
}
543585
if err := validateFreshFullReadWithContent(ctx, path, content); err != nil {
544586
return err
@@ -551,7 +593,9 @@ func validateEdit(ctx tool.Context, raw json.RawMessage) error {
551593
if matches > 1 && !input.ReplaceAll {
552594
return fmt.Errorf("Found %d matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, please provide more context to uniquely identify the instance.\nString: %s", matches, input.OldString)
553595
}
554-
return nil
596+
actualNew := preserveQuoteStyle(input.OldString, actualOld, input.NewString)
597+
updated := applyEdit(content, actualOld, actualNew, input.ReplaceAll)
598+
return validateSettingsFileContent(path, updated)
555599
}
556600

557601
func callEdit(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contracts.ToolResult, error) {

0 commit comments

Comments
 (0)