@@ -5,6 +5,42 @@ use std::fs;
55use crate :: config:: types:: { ClaudeSettings , Configuration , StorageMode } ;
66use crate :: utils:: get_claude_settings_path;
77
8+ /// Remove trailing commas from JSON content to make it more lenient
9+ ///
10+ /// Handles trailing commas before `}` and `]` characters, which are common
11+ /// in hand-edited JSON files but not valid in standard JSON.
12+ fn strip_trailing_commas ( json : & str ) -> String {
13+ // Simple approach: remove commas that appear before closing braces/brackets
14+ // This handles the most common case of trailing commas
15+ let mut result = String :: with_capacity ( json. len ( ) ) ;
16+ let chars: Vec < char > = json. chars ( ) . collect ( ) ;
17+ let mut i = 0 ;
18+
19+ while i < chars. len ( ) {
20+ let c = chars[ i] ;
21+
22+ // Check if this is a comma followed by optional whitespace and then } or ]
23+ if c == ',' {
24+ // Look ahead to see if the next non-whitespace char is } or ]
25+ let mut j = i + 1 ;
26+ while j < chars. len ( ) && chars[ j] . is_whitespace ( ) {
27+ j += 1 ;
28+ }
29+
30+ if j < chars. len ( ) && ( chars[ j] == '}' || chars[ j] == ']' ) {
31+ // Skip this trailing comma
32+ i += 1 ;
33+ continue ;
34+ }
35+ }
36+
37+ result. push ( c) ;
38+ i += 1 ;
39+ }
40+
41+ result
42+ }
43+
844impl ClaudeSettings {
945 /// Load Claude settings from disk
1046 ///
@@ -34,8 +70,28 @@ impl ClaudeSettings {
3470 let mut settings: ClaudeSettings = if content. trim ( ) . is_empty ( ) {
3571 ClaudeSettings :: default ( )
3672 } else {
37- serde_json:: from_str ( & content)
38- . with_context ( || "Failed to parse Claude settings JSON" ) ?
73+ // Strip trailing commas to handle lenient JSON
74+ let cleaned_content = strip_trailing_commas ( & content) ;
75+
76+ // Try to parse the cleaned content first
77+ match serde_json:: from_str ( & cleaned_content) {
78+ Ok ( s) => s,
79+ Err ( e) => {
80+ // Provide helpful error message with the actual parse error
81+ let error_msg = format ! (
82+ "Failed to parse Claude settings JSON at {}:\n {}\n \n \
83+ This usually means the JSON file has invalid syntax.\n \
84+ Common issues:\n \
85+ - Trailing commas (e.g., {{\" key\" : \" value\" ,}})\n \
86+ - Missing quotes around keys or values\n \
87+ - Unescaped special characters in strings\n \n \
88+ Please fix the JSON syntax in the file.",
89+ path. display( ) ,
90+ e
91+ ) ;
92+ return Err ( anyhow:: anyhow!( "{}" , error_msg) ) ;
93+ }
94+ }
3995 } ;
4096
4197 // Ensure env field exists (handle case where it might be missing from JSON)
@@ -320,3 +376,76 @@ impl ClaudeSettings {
320376 Ok ( ( ) )
321377 }
322378}
379+
380+ #[ cfg( test) ]
381+ mod tests {
382+ use super :: * ;
383+
384+ #[ test]
385+ fn test_strip_trailing_commas_simple ( ) {
386+ let input = r#"{"a": 1,}"# ;
387+ let expected = r#"{"a": 1}"# ;
388+ assert_eq ! ( strip_trailing_commas( input) , expected) ;
389+ }
390+
391+ #[ test]
392+ fn test_strip_trailing_commas_nested_object ( ) {
393+ let input = r#"{"env": {"KEY": "value",},}"# ;
394+ let expected = r#"{"env": {"KEY": "value"}}"# ;
395+ assert_eq ! ( strip_trailing_commas( input) , expected) ;
396+ }
397+
398+ #[ test]
399+ fn test_strip_trailing_commas_array ( ) {
400+ let input = r#"{"items": [1, 2, 3,],}"# ;
401+ let expected = r#"{"items": [1, 2, 3]}"# ;
402+ assert_eq ! ( strip_trailing_commas( input) , expected) ;
403+ }
404+
405+ #[ test]
406+ fn test_strip_trailing_commas_multiline ( ) {
407+ let input = r#"{
408+ "env": {
409+ "KEY": "value",
410+ },
411+ }"# ;
412+ let expected = r#"{
413+ "env": {
414+ "KEY": "value"
415+ }
416+ }"# ;
417+ assert_eq ! ( strip_trailing_commas( input) , expected) ;
418+ }
419+
420+ #[ test]
421+ fn test_strip_trailing_commas_no_trailing ( ) {
422+ let input = r#"{"a": 1, "b": 2}"# ;
423+ assert_eq ! ( strip_trailing_commas( input) , input) ;
424+ }
425+
426+ #[ test]
427+ fn test_strip_trailing_commas_complex ( ) {
428+ let input = r#"{
429+ "env": {
430+ "ANTHROPIC_AUTH_TOKEN": "token",
431+ "ANTHROPIC_BASE_URL": "https://api.example.com",
432+ },
433+ "model": "claude-3-opus",
434+ }"# ;
435+ let expected = r#"{
436+ "env": {
437+ "ANTHROPIC_AUTH_TOKEN": "token",
438+ "ANTHROPIC_BASE_URL": "https://api.example.com"
439+ },
440+ "model": "claude-3-opus"
441+ }"# ;
442+ assert_eq ! ( strip_trailing_commas( input) , expected) ;
443+ }
444+
445+ #[ test]
446+ fn test_strip_trailing_commas_preserves_inner_commas ( ) {
447+ let input = r#"{"a": 1, "b": 2, "c": 3,}"# ;
448+ let expected = r#"{"a": 1, "b": 2, "c": 3}"# ;
449+ assert_eq ! ( strip_trailing_commas( input) , expected) ;
450+ }
451+ }
0 commit comments