From 952a46b9fd0e2e23ff56858464f5db08ec506dde Mon Sep 17 00:00:00 2001 From: CreedTech <76216787+CreedTech@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:12:14 +0100 Subject: [PATCH] fix(setup): preserve existing hookify guards on rerun --- packages/setup/README.md | 3 +++ packages/setup/index.js | 9 +++++-- packages/setup/tests/setup-contract.test.mjs | 27 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/setup/README.md b/packages/setup/README.md index 0316ba0e..bf742592 100644 --- a/packages/setup/README.md +++ b/packages/setup/README.md @@ -40,6 +40,9 @@ for that client. | `.claude/hookify.sysknife-schema-fetch.local.md` | Deferred-schema reminder | | `.claude/hookify.sysknife-bash-guard.local.md` | VM query guard | +On rerun, existing hookify rules are left intact and reported as skipped. Edit +them manually if you want to update them to the packaged defaults. + ### Cursor | File | Purpose | diff --git a/packages/setup/index.js b/packages/setup/index.js index 1d69eba0..fb9f888f 100755 --- a/packages/setup/index.js +++ b/packages/setup/index.js @@ -779,8 +779,13 @@ async function main() { for (const { file, content } of rules) { const hookPath = path.join('.claude', file); - fs.writeFileSync(hookPath, content); - ok(`Created ${hookPath}`); + try { + fs.writeFileSync(hookPath, content, { flag: 'wx' }); + ok(`Created ${hookPath}`); + } catch (error) { + if (error.code !== 'EEXIST') throw error; + warn(`Skipped ${hookPath} (already exists; edit manually to update)`); + } } } diff --git a/packages/setup/tests/setup-contract.test.mjs b/packages/setup/tests/setup-contract.test.mjs index 9ebfc1a7..d459aaac 100644 --- a/packages/setup/tests/setup-contract.test.mjs +++ b/packages/setup/tests/setup-contract.test.mjs @@ -69,6 +69,33 @@ test('MCP configs are merged, not overwritten (preserves other servers)', () => assert.doesNotMatch(source, /const cursorMcp = \{ mcpServers \}/); }); +test('rerunning setup preserves existing hookify guards and reports what it did', () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sysknife-setup-hookify-')); + const rulesDir = path.join(cwd, '.claude'); + const approvalPath = path.join(rulesDir, 'hookify.require-sysknife-approval.local.md'); + const schemaPath = path.join(rulesDir, 'hookify.sysknife-schema-fetch.local.md'); + const bashPath = path.join(rulesDir, 'hookify.sysknife-bash-guard.local.md'); + + try { + fs.mkdirSync(rulesDir); + fs.writeFileSync(approvalPath, 'project-specific approval guard\n'); + fs.writeFileSync(schemaPath, 'project-specific schema reminder\n'); + + const result = runWizard({ cwd }); + const output = `${result.stdout}${result.stderr}`; + + assert.equal(result.status, 3, output); + assert.equal(fs.readFileSync(approvalPath, 'utf8'), 'project-specific approval guard\n'); + assert.equal(fs.readFileSync(schemaPath, 'utf8'), 'project-specific schema reminder\n'); + assert.match(output, /Skipped \.claude\/hookify\.require-sysknife-approval\.local\.md.*already exists/); + assert.match(output, /Skipped \.claude\/hookify\.sysknife-schema-fetch\.local\.md.*already exists/); + assert.match(output, /Created \.claude\/hookify\.sysknife-bash-guard\.local\.md/); + assert.ok(fs.readFileSync(bashPath, 'utf8').length > 0); + } finally { + fs.rmSync(cwd, { recursive: true, force: true }); + } +}); + test('all selected MCP configs are validated before any integration file is written', () => { const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sysknife-setup-preflight-')); const claudePath = path.join(cwd, '.mcp.json');