From 7a5105d61c94e8968dd38fbe702752163d27d1e1 Mon Sep 17 00:00:00 2001 From: dyoshikawa Date: Mon, 6 Jul 2026 01:57:15 -0700 Subject: [PATCH] refactor(ignore): guard ignore-side Claude settings JSON.parse with try/catch The Claude Code ignore feature parsed an existing `.claude/settings.json` with a bare `JSON.parse`, so a corrupted settings file surfaced an unformatted error. The permissions side (`claudecode-permissions.ts`) already wraps this in a try/catch that rethrows a `Failed to parse existing Claude settings at ` message. Mirror that here for consistency, and add a test covering the malformed input path. Closes #2121 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/features/ignore/claudecode-ignore.test.ts | 20 +++++++++++++++++++ src/features/ignore/claudecode-ignore.ts | 11 +++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/features/ignore/claudecode-ignore.test.ts b/src/features/ignore/claudecode-ignore.test.ts index 88290752f..a288c80a8 100644 --- a/src/features/ignore/claudecode-ignore.test.ts +++ b/src/features/ignore/claudecode-ignore.test.ts @@ -216,6 +216,26 @@ describe("ClaudecodeIgnore", () => { // existing shared settings.json untouched on disk. expect(claudecodeIgnore.getRelativeFilePath()).toBe("settings.local.json"); }); + + it("should throw a formatted error when the existing settings.json is malformed", async () => { + const claudeDir = join(testDir, ".claude"); + await ensureDir(claudeDir); + await writeFileContent(join(claudeDir, "settings.json"), "{ not valid json"); + + const rulesyncIgnore = new RulesyncIgnore({ + relativeDirPath: RULESYNC_RELATIVE_DIR_PATH, + relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH, + fileContent: "*.log", + }); + + await expect( + ClaudecodeIgnore.fromRulesyncIgnore({ + outputRoot: testDir, + rulesyncIgnore, + options: { fileMode: "shared" }, + }), + ).rejects.toThrow(/Failed to parse existing Claude settings at .*settings\.json/); + }); }); describe("isDeletable", () => { diff --git a/src/features/ignore/claudecode-ignore.ts b/src/features/ignore/claudecode-ignore.ts index f4d29d685..2f38bf7da 100644 --- a/src/features/ignore/claudecode-ignore.ts +++ b/src/features/ignore/claudecode-ignore.ts @@ -9,6 +9,7 @@ import { } from "../../constants/claudecode-paths.js"; import type { ClaudeSettingsJson } from "../../types/claude-settings.js"; import { FeatureOptions } from "../../types/features.js"; +import { formatError } from "../../utils/error.js"; import { fileExists, readFileContent } from "../../utils/file.js"; import { applyIgnoreReadDenies, @@ -143,7 +144,15 @@ export class ClaudecodeIgnore extends ToolIgnore { const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath); const exists = await fileExists(filePath); const existingFileContent = exists ? await readFileContent(filePath) : "{}"; - const existingJsonValue: ClaudeSettingsJson = JSON.parse(existingFileContent); + let existingJsonValue: ClaudeSettingsJson; + try { + existingJsonValue = JSON.parse(existingFileContent); + } catch (error) { + throw new Error( + `Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, + { cause: error }, + ); + } // The gateway owns the `permissions.deny` merge shared with the permissions // feature; here we only state the intent (deny these Read patterns).