diff --git a/src/__tests__/utils/web-defaults.test.ts b/src/__tests__/utils/web-defaults.test.ts index b2c2670249..4508b8478c 100644 --- a/src/__tests__/utils/web-defaults.test.ts +++ b/src/__tests__/utils/web-defaults.test.ts @@ -119,4 +119,38 @@ describe('configureWebDefaults', () => { 'model = "gpt-5"\n\n[profiles.research]\nweb_search = "disabled"\n' ); }); + + it('preserves valid JSON strings containing "//" in Claude settings', async () => { + await write( + '.claude/settings.json', + JSON.stringify({ + permissions: { + allow: ['Read(//c/Users/dev/repo)'], + }, + }) + ); + + const results = await configureWebDefaults(); + const claudeResult = results.find((r) => r.agent === 'Claude Code'); + expect(claudeResult?.skipped).toBeFalsy(); + expect(claudeResult?.changed).toBe(true); + + const saved = JSON.parse(await read('.claude/settings.json')); + expect(saved.permissions.allow).toEqual(['Read(//c/Users/dev/repo)']); + expect(saved.permissions.deny).toEqual(['WebSearch', 'WebFetch']); + }); + + it('parses Claude settings containing JSON comments without corrupting strings with "//"', async () => { + const jsonWithComments = `// Claude settings\n{\n /* custom permissions */\n "permissions": {\n "allow": ["Read(//c/Users/dev/repo)"]\n }\n}`; + await write('.claude/settings.json', jsonWithComments); + + const results = await configureWebDefaults(); + const claudeResult = results.find((r) => r.agent === 'Claude Code'); + expect(claudeResult?.skipped).toBeFalsy(); + expect(claudeResult?.changed).toBe(true); + + const saved = JSON.parse(await read('.claude/settings.json')); + expect(saved.permissions.allow).toEqual(['Read(//c/Users/dev/repo)']); + expect(saved.permissions.deny).toEqual(['WebSearch', 'WebFetch']); + }); }); diff --git a/src/utils/web-defaults.ts b/src/utils/web-defaults.ts index 1419acf6ac..0d870f9c5e 100644 --- a/src/utils/web-defaults.ts +++ b/src/utils/web-defaults.ts @@ -37,10 +37,60 @@ async function writeText(filePath: string, content: string): Promise { await fs.writeFile(filePath, content, 'utf8'); } -function removeJsonComments(content: string): string { - return content - .replace(/\/\*[\s\S]*?\*\//g, '') - .replace(/(^|[^:\\])\/\/.*$/gm, '$1'); +export function removeJsonComments(content: string): string { + let result = ''; + let inString = false; + let isEscaped = false; + let inLineComment = false; + let inBlockComment = false; + + for (let i = 0; i < content.length; i++) { + const char = content[i]; + const nextChar = i + 1 < content.length ? content[i + 1] : ''; + + if (inLineComment) { + if (char === '\n') { + inLineComment = false; + result += char; + } + continue; + } + + if (inBlockComment) { + if (char === '*' && nextChar === '/') { + inBlockComment = false; + i++; + } + continue; + } + + if (inString) { + result += char; + if (isEscaped) { + isEscaped = false; + } else if (char === '\\') { + isEscaped = true; + } else if (char === '"') { + inString = false; + } + continue; + } + + if (char === '"') { + inString = true; + result += char; + } else if (char === '/' && nextChar === '/') { + inLineComment = true; + i++; + } else if (char === '/' && nextChar === '*') { + inBlockComment = true; + i++; + } else { + result += char; + } + } + + return result; } async function configureClaudeDefaults( @@ -52,7 +102,11 @@ async function configureClaudeDefaults( if (existing && existing.trim()) { try { - config = JSON.parse(removeJsonComments(existing)); + try { + config = JSON.parse(existing); + } catch { + config = JSON.parse(removeJsonComments(existing)); + } } catch { return { agent: 'Claude Code',