Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/__tests__/utils/web-defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
64 changes: 59 additions & 5 deletions src/utils/web-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,60 @@ async function writeText(filePath: string, content: string): Promise<void> {
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') {

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a settings file uses lone \r line endings, this scanner never terminates // comments and drops the rest of the JSON, causing Claude defaults setup to skip the file. Treat \r as a line-comment terminator as well as \n.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/utils/web-defaults.ts, line 52:

<comment>When a settings file uses lone `\r` line endings, this scanner never terminates `//` comments and drops the rest of the JSON, causing Claude defaults setup to skip the file. Treat `\r` as a line-comment terminator as well as `\n`.</comment>

<file context>
@@ -37,10 +37,60 @@ async function writeText(filePath: string, content: string): Promise<void> {
+    const nextChar = i + 1 < content.length ? content[i + 1] : '';
+
+    if (inLineComment) {
+      if (char === '\n') {
+        inLineComment = false;
+        result += char;
</file context>
Suggested change
if (char === '\n') {
if (char === '\n' || char === '\r') {
Fix with cubic

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(
Expand All @@ -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',
Expand Down