fix(persist): accurate symlink guard message on backup read

The guard fires on the backup READ step; use a typed ConfigError whose message
matches the actual step (refusing to read), not a misleading 'refusing to write'.
This commit is contained in:
Tam Nhu Tran committed 2026-06-22 12:06:58 -04:00
1 parent 9d5b33382b
commit e4c91d2853
1 file changed
+9
@@ -11,6 +11,7 @@ import * as path from 'path';
import { initUI, header, color, dim, ok, fail, warn, info } from '../../utils/ui'; import { initUI, header, color, dim, ok, fail, warn, info } from '../../utils/ui';
import { InteractivePrompt } from '../../utils/prompt'; import { InteractivePrompt } from '../../utils/prompt';
import { getClaudeSettingsPath } from '../../utils/claude-config-path'; import { getClaudeSettingsPath } from '../../utils/claude-config-path';
import { ConfigError } from '../../errors';
import { import {
formatDisplayPath, formatDisplayPath,
getClaudeSettingsDisplayPath, getClaudeSettingsDisplayPath,
@@ -86,6 +87,14 @@ export async function createBackup(): Promise<string> {
throw new Error('No settings.json to backup'); throw new Error('No settings.json to backup');
} }
// Guard: refuse to read a symlinked settings.json (TOCTOU mitigation on backup read).
if (await isSymlinkAsync(settingsPath)) {
throw new ConfigError(
'settings.json is a symlink - refusing to read for backup for security',
settingsPath
);
}
const settingsContent = await readFileUtf8NoFollow(settingsPath); const settingsContent = await readFileUtf8NoFollow(settingsPath);
const now = new Date(); const now = new Date();