mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(clone): route EVERY settings writer through one safe-write gate
Round 2 of the #251 review: settingsWriteBlocker covered only writeHooksConfig and updateCaseModel, while applyStatusLineConfig, stripCaseEnvKeys, updateCaseEnvVars, refreshStaleCodemanHooks and ensureCodemanHooks still wrote the same repository-controlled path unguarded (applyStatusLineConfig was demonstrated writing through a symlinked settings.local.json). All seven writers now go through withSafeSettingsWrite(), which runs the blocker check INSIDE the per-path settings lock and then hands the writer its claudeDir/settingsPath; none of them touch the settings path directly anymore. Test pins all seven against a symlinked settings.local.json at once (link target must stay byte-identical). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,16 @@ import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import {
|
||||
applyStatusLineConfig,
|
||||
ensureCodemanHooks,
|
||||
generateBackgroundWakeScript,
|
||||
generateHooksConfig,
|
||||
generateSubagentStopGuardScript,
|
||||
refreshStaleCodemanHooks,
|
||||
settingsWriteBlocker,
|
||||
stripCaseEnvKeys,
|
||||
updateCaseEnvVars,
|
||||
updateCaseModel,
|
||||
writeHooksConfig,
|
||||
} from '../src/hooks-config.js';
|
||||
|
||||
@@ -230,6 +234,31 @@ describe('writeHooksConfig', () => {
|
||||
expect(await settingsWriteBlocker(caseDir)).toBeNull();
|
||||
});
|
||||
|
||||
it('EVERY settings writer refuses a symlinked settings.local.json (#251 review round 2)', async () => {
|
||||
// Round 1 guarded only writeHooksConfig/updateCaseModel; the reviewer
|
||||
// demonstrated applyStatusLineConfig writing through the link. All
|
||||
// writers now share one safe-write gate, so pin all of them at once.
|
||||
const outsideFile = join(testDir, 'victim-all-writers.json');
|
||||
const precious =
|
||||
'{"env":{"CLAUDE_CODE_KEEP":"me"},"hooks":{"Stop":[{"hooks":[{"command":"curl /api/hook-event"}]}]}}\n';
|
||||
writeFileSync(outsideFile, precious);
|
||||
const caseDir = join(testDir, 'case-writers');
|
||||
mkdirSync(join(caseDir, '.claude'), { recursive: true });
|
||||
symlinkSync(outsideFile, join(caseDir, '.claude', 'settings.local.json'));
|
||||
|
||||
await writeHooksConfig(caseDir);
|
||||
await ensureCodemanHooks(caseDir);
|
||||
await refreshStaleCodemanHooks(caseDir);
|
||||
await updateCaseModel(caseDir, 'opus');
|
||||
await updateCaseEnvVars(caseDir, { CLAUDE_CODE_NEW: 'value' });
|
||||
await stripCaseEnvKeys(caseDir, ['CLAUDE_CODE_KEEP']);
|
||||
await applyStatusLineConfig(caseDir, true);
|
||||
await applyStatusLineConfig(caseDir, false);
|
||||
|
||||
// The link target is byte-identical: none of the writers went through it.
|
||||
expect(readFileSync(outsideFile, 'utf-8')).toBe(precious);
|
||||
});
|
||||
|
||||
it('should merge with existing settings.local.json', async () => {
|
||||
const claudeDir = join(testDir, '.claude');
|
||||
mkdirSync(claudeDir, { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user