Skip to content

Commit 8c3d08c

Browse files
FelixIsaacclaude
andcommitted
fix: use JSON parsing for safe cross-platform path expansion
- Parse JSON properly instead of aggressive string replacement - Recursively expand placeholders in JSON structures - Convert backslashes only in path values, not JSON escapes - Fall back to safe string replacement if parsing fails - Fixes corruption of JSON escape sequences like \" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8ea5bf1 commit 8c3d08c

1 file changed

Lines changed: 62 additions & 18 deletions

File tree

internal/sync/paths.go

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sync
22

33
import (
4+
"encoding/json"
45
"path/filepath"
56
"strings"
67
)
@@ -29,33 +30,76 @@ func NormalizePathsInJSON(data []byte, claudeDir string) []byte {
2930

3031
// ExpandPathsInJSON replaces the cross-platform placeholder with the local ClaudeDir path.
3132
// The expanded path uses the native format for the current platform.
33+
// Uses JSON parsing to safely handle escape sequences.
3234
func ExpandPathsInJSON(data []byte, claudeDir string) []byte {
35+
// First, parse as JSON to get the structure
36+
var obj interface{}
37+
if err := json.Unmarshal(data, &obj); err != nil {
38+
// If not valid JSON, fall back to string replacement
39+
return fallbackExpandPaths(data, claudeDir)
40+
}
41+
42+
// Recursively replace placeholders in the parsed object
43+
expanded := expandInObject(obj, claudeDir)
44+
45+
// Marshal back to JSON with proper formatting
46+
result, err := json.MarshalIndent(expanded, "", " ")
47+
if err != nil {
48+
// If marshaling fails, fall back
49+
return fallbackExpandPaths(data, claudeDir)
50+
}
51+
52+
return result
53+
}
54+
55+
// expandInObject recursively expands placeholders in JSON objects
56+
func expandInObject(obj interface{}, claudeDir string) interface{} {
57+
switch v := obj.(type) {
58+
case map[string]interface{}:
59+
for k, val := range v {
60+
v[k] = expandInObject(val, claudeDir)
61+
}
62+
return v
63+
case []interface{}:
64+
for i, val := range v {
65+
v[i] = expandInObject(val, claudeDir)
66+
}
67+
return v
68+
case string:
69+
if strings.Contains(v, ClaudeDirPlaceholder) {
70+
// Replace placeholder with local path
71+
expanded := strings.ReplaceAll(v, ClaudeDirPlaceholder, claudeDir)
72+
73+
// On Unix systems, convert backslashes to forward slashes in paths
74+
if !strings.Contains(claudeDir, `\`) {
75+
// This is Unix - convert Windows-style backslashes to forward slashes
76+
// But only convert path separators, not escape sequences
77+
// In JSON strings, \\ is already unescaped to \, so we just replace \
78+
expanded = strings.ReplaceAll(expanded, `\`, `/`)
79+
}
80+
81+
return expanded
82+
}
83+
return v
84+
default:
85+
return v
86+
}
87+
}
88+
89+
// fallbackExpandPaths is a safe string-based fallback that only replaces in quoted strings
90+
func fallbackExpandPaths(data []byte, claudeDir string) []byte {
3391
content := string(data)
3492

3593
// For JSON files, we need to use escaped backslashes on Windows
36-
// Check if we're on Windows by looking for backslashes in claudeDir
3794
if strings.Contains(claudeDir, `\`) {
3895
// Windows: use escaped backslashes for JSON
3996
escapedClaudeDir := strings.ReplaceAll(claudeDir, `\`, `\\`)
4097
content = strings.ReplaceAll(content, ClaudeDirPlaceholder, escapedClaudeDir)
4198
} else {
42-
// Unix: replace placeholder first
43-
content = strings.ReplaceAll(content, ClaudeDirPlaceholder, claudeDir)
44-
45-
// Convert path separators: replace \\ with / on lines that now contain the expanded path
46-
lines := strings.Split(content, "\n")
47-
for i, line := range lines {
48-
if strings.Contains(line, claudeDir) {
49-
// This line now contains the expanded path with backslashes
50-
// Replace \\ (escaped backslash in JSON = one actual backslash) with /
51-
// Go string literal: `\\` is actually two characters (backslash, backslash) in the source
52-
// When applied to JSON content, "C:\\Users" becomes /home/ubuntu/.claude\plugins\...
53-
// And we want to convert \plugins to /plugins
54-
line = strings.ReplaceAll(line, `\`, `/`)
55-
lines[i] = line
56-
}
57-
}
58-
content = strings.Join(lines, "\n")
99+
// Unix: replace placeholder with forward-slash path
100+
// This is safer than replacing all backslashes
101+
normalizedPath := filepath.ToSlash(claudeDir) // ensure forward slashes
102+
content = strings.ReplaceAll(content, ClaudeDirPlaceholder, normalizedPath)
59103
}
60104

61105
return []byte(content)

0 commit comments

Comments
 (0)