code-warden 3.1.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CONFIGURE.md +39 -39
  2. package/DECISIONS.md +107 -107
  3. package/README.md +199 -137
  4. package/SKILL.md +169 -169
  5. package/bin/code-warden.js +82 -0
  6. package/codewarden.json +14 -14
  7. package/examples/governed-session.md +132 -132
  8. package/install.js +399 -399
  9. package/install.ps1 +32 -32
  10. package/install.sh +33 -33
  11. package/package.json +45 -2
  12. package/references/anti-drift.md +55 -55
  13. package/references/architecture.md +26 -26
  14. package/references/cleanup.md +30 -30
  15. package/references/cognition.md +36 -36
  16. package/references/operations.md +45 -45
  17. package/references/planning-gates.md +83 -83
  18. package/references/research-and-fit.md +51 -51
  19. package/references/safety.md +31 -31
  20. package/templates/ci/github-actions.yml +83 -66
  21. package/tools/auto-detect.js +91 -91
  22. package/tools/auto-targets.js +104 -104
  23. package/tools/auto-windsurf-adapter.js +75 -75
  24. package/tools/get-context.js +50 -50
  25. package/tools/governance-report.js +302 -0
  26. package/tools/hooks/claude/install-hooks.js +112 -112
  27. package/tools/hooks/claude/uninstall-hooks.js +75 -75
  28. package/tools/hooks/claude/warden-lint-hook.js +106 -106
  29. package/tools/hooks/claude/warden-secrets-hook.js +73 -73
  30. package/tools/hooks/codex/install-hooks.js +100 -100
  31. package/tools/hooks/codex/uninstall-hooks.js +53 -53
  32. package/tools/hooks/codex/warden-apply-patch-hook.js +113 -113
  33. package/tools/hooks/codex/warden-bash-hook.js +51 -51
  34. package/tools/lib/config.js +49 -49
  35. package/tools/lib/file-collection.js +72 -72
  36. package/tools/lib/line-count.js +28 -28
  37. package/tools/lib/secret-patterns.js +57 -57
  38. package/tools/tests/fixtures/clean.js +9 -9
  39. package/tools/tests/run-tests.js +210 -210
  40. package/tools/verify-secrets.js +26 -26
  41. package/tools/warden-lint.js +27 -27
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { spawnSync } = require('child_process');
8
+ const { countLines } = require('./lib/line-count');
9
+ const { collectFiles } = require('./lib/file-collection');
10
+ const { scanForSecrets } = require('./lib/secret-patterns');
11
+ const { loadConfig } = require('./lib/config');
12
+
13
+ const ROOT = path.join(__dirname, '..');
14
+ const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
15
+ const VERSION = PKG.version;
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // CLI
19
+ // ---------------------------------------------------------------------------
20
+
21
+ function parseArgs(argv) {
22
+ const args = argv.slice(2);
23
+ const formatArg = args.find(a => a.startsWith('--format='));
24
+ const format = formatArg ? formatArg.split('=')[1] : null;
25
+ const scanPath = args.find(a => !a.startsWith('--')) || '.';
26
+ return { format, scanPath };
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Git metadata
31
+ // ---------------------------------------------------------------------------
32
+
33
+ function gitInfo() {
34
+ const run = (gitArgs) => {
35
+ const r = spawnSync('git', gitArgs, { encoding: 'utf8', timeout: 5000 });
36
+ return r.status === 0 ? r.stdout.trim() : null;
37
+ };
38
+ return {
39
+ branch: run(['rev-parse', '--abbrev-ref', 'HEAD']),
40
+ commit: run(['rev-parse', '--short', 'HEAD']),
41
+ };
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // File length + secrets (single pass over all files)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function runScans(scanPath) {
49
+ const { maxFileLength } = loadConfig();
50
+ const resolved = path.resolve(scanPath);
51
+
52
+ if (!fs.existsSync(resolved)) {
53
+ console.error(`[CodeWarden] Error: scan path not found: ${scanPath}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ const files = [];
58
+ if (fs.statSync(resolved).isDirectory()) {
59
+ collectFiles(resolved, files);
60
+ } else {
61
+ files.push(resolved);
62
+ }
63
+
64
+ const lengthViolations = [];
65
+ const secretViolations = [];
66
+
67
+ for (const f of files) {
68
+ let content;
69
+ try { content = fs.readFileSync(f, 'utf8'); } catch { continue; }
70
+
71
+ const rel = path.relative(resolved, f);
72
+
73
+ const lineCount = countLines(content);
74
+ if (lineCount > maxFileLength) {
75
+ lengthViolations.push({ file: rel, lines: lineCount, limit: maxFileLength });
76
+ }
77
+
78
+ const hit = scanForSecrets(content);
79
+ if (hit) {
80
+ secretViolations.push({ file: rel, pattern: hit.label });
81
+ }
82
+ }
83
+
84
+ return {
85
+ fileLength: {
86
+ status: lengthViolations.length === 0 ? 'pass' : 'fail',
87
+ filesScanned: files.length,
88
+ violations: lengthViolations.length,
89
+ details: lengthViolations.length > 0 ? lengthViolations : undefined,
90
+ },
91
+ secrets: {
92
+ status: secretViolations.length === 0 ? 'pass' : 'fail',
93
+ filesScanned: files.length,
94
+ violations: secretViolations.length,
95
+ details: secretViolations.length > 0 ? secretViolations : undefined,
96
+ },
97
+ };
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Behavioral tests
102
+ // ---------------------------------------------------------------------------
103
+
104
+ function checkTests() {
105
+ const testScript = path.join(__dirname, 'tests', 'run-tests.js');
106
+ if (!fs.existsSync(testScript)) {
107
+ return { status: 'skip', tests: 0, failures: 0 };
108
+ }
109
+
110
+ const r = spawnSync(process.execPath, [testScript], {
111
+ encoding: 'utf8',
112
+ timeout: 30000,
113
+ cwd: ROOT,
114
+ });
115
+
116
+ const out = (r.stdout || '') + (r.stderr || '');
117
+ const passMatch = out.match(/pass\s+(\d+)/);
118
+ const failMatch = out.match(/fail\s+(\d+)/);
119
+
120
+ let passed, failed;
121
+ if (passMatch || failMatch) {
122
+ passed = parseInt(passMatch?.[1] || '0', 10);
123
+ failed = parseInt(failMatch?.[1] || '0', 10);
124
+ } else {
125
+ passed = (out.match(/^(?:ok \d+|✔)/gm) || []).length;
126
+ failed = (out.match(/^(?:not ok \d+|✖)/gm) || []).length;
127
+ }
128
+
129
+ return {
130
+ status: r.status === 0 ? 'pass' : 'fail',
131
+ tests: passed + failed,
132
+ failures: failed,
133
+ };
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Source integrity
138
+ // ---------------------------------------------------------------------------
139
+
140
+ function checkInstallHealth() {
141
+ const required = [
142
+ 'SKILL.md',
143
+ 'references',
144
+ 'tools/warden-lint.js',
145
+ 'tools/verify-secrets.js',
146
+ 'tools/get-context.js',
147
+ ];
148
+ const missing = required.filter(f => !fs.existsSync(path.join(ROOT, f)));
149
+ return {
150
+ status: missing.length === 0 ? 'pass' : 'fail',
151
+ missing: missing.length > 0 ? missing : undefined,
152
+ };
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Runtime hook detection
157
+ // ---------------------------------------------------------------------------
158
+
159
+ function checkRuntimeHooks() {
160
+ const home = os.homedir();
161
+ const result = {};
162
+
163
+ const claudeSettings = path.join(home, '.claude', 'settings.json');
164
+ if (fs.existsSync(claudeSettings)) {
165
+ try {
166
+ const s = JSON.parse(fs.readFileSync(claudeSettings, 'utf8'));
167
+ const hooks = (s?.hooks?.PreToolUse || [])
168
+ .flatMap(m => m.hooks || [])
169
+ .filter(h => String(h.description || '').startsWith('code-warden:'));
170
+ if (hooks.length > 0) {
171
+ const valid = hooks.every(h => h.args?.[0] && fs.existsSync(h.args[0]));
172
+ result.claude = valid ? 'registered' : 'registered_broken';
173
+ } else {
174
+ result.claude = 'not_registered';
175
+ }
176
+ } catch { result.claude = 'error'; }
177
+ } else {
178
+ result.claude = 'not_configured';
179
+ }
180
+
181
+ const codexHooksPath = path.join(home, '.codex', 'hooks.json');
182
+ if (fs.existsSync(codexHooksPath)) {
183
+ try {
184
+ const h = JSON.parse(fs.readFileSync(codexHooksPath, 'utf8'));
185
+ const cw = (h?.PreToolUse || [])
186
+ .filter(e => String(e.description || '').startsWith('code-warden:'));
187
+ if (cw.length > 0) {
188
+ const valid = cw.every(e => e.args?.[0] && fs.existsSync(e.args[0]));
189
+ result.codex = valid ? 'registered' : 'registered_broken';
190
+ } else {
191
+ result.codex = 'not_registered';
192
+ }
193
+ } catch { result.codex = 'error'; }
194
+ } else {
195
+ result.codex = 'not_configured';
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Report assembly
203
+ // ---------------------------------------------------------------------------
204
+
205
+ function generateReport(scanPath) {
206
+ const repo = gitInfo();
207
+ const { fileLength, secrets } = runScans(scanPath);
208
+ const behavioralTests = checkTests();
209
+ const installHealth = checkInstallHealth();
210
+ const runtimeHooks = checkRuntimeHooks();
211
+
212
+ const checks = { fileLength, secrets, behavioralTests, installHealth };
213
+ const result = Object.values(checks).every(c => c.status === 'pass' || c.status === 'skip')
214
+ ? 'pass' : 'fail';
215
+
216
+ return {
217
+ tool: 'code-warden',
218
+ version: VERSION,
219
+ timestamp: new Date().toISOString(),
220
+ repository: { branch: repo.branch, commit: repo.commit },
221
+ checks,
222
+ governance: {
223
+ scopeGate: 'session_only',
224
+ planGate: 'session_only',
225
+ runtimeHooks,
226
+ },
227
+ result,
228
+ };
229
+ }
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Markdown formatter
233
+ // ---------------------------------------------------------------------------
234
+
235
+ function formatMarkdown(report) {
236
+ const badge = s => s === 'pass' ? 'PASS' : s === 'skip' ? 'SKIP' : 'FAIL';
237
+ const hookLabel = (id) => {
238
+ const s = report.governance.runtimeHooks[id];
239
+ if (s === 'registered') return 'verified';
240
+ if (s === 'registered_broken') return 'broken';
241
+ if (s === 'not_registered') return 'none';
242
+ return 'n/a';
243
+ };
244
+
245
+ const healthDetail = report.checks.installHealth.missing
246
+ ? 'Missing: ' + report.checks.installHealth.missing.join(', ')
247
+ : 'All source files present';
248
+
249
+ const lines = [
250
+ '## Code-Warden Governance Report',
251
+ '',
252
+ '| Check | Result | Details |',
253
+ '|-------|--------|---------|',
254
+ `| File length | ${badge(report.checks.fileLength.status)} | ${report.checks.fileLength.filesScanned} files scanned, ${report.checks.fileLength.violations} violations |`,
255
+ `| Hardcoded credentials | ${badge(report.checks.secrets.status)} | ${report.checks.secrets.filesScanned} files scanned, ${report.checks.secrets.violations} violations |`,
256
+ `| Behavioral tests | ${badge(report.checks.behavioralTests.status)} | ${report.checks.behavioralTests.tests} tests, ${report.checks.behavioralTests.failures} failures |`,
257
+ `| Install health | ${badge(report.checks.installHealth.status)} | ${healthDetail} |`,
258
+ `| Runtime hooks | — | Claude: ${hookLabel('claude')} / Codex: ${hookLabel('codex')} |`,
259
+ '',
260
+ `**Result:** ${report.result === 'pass' ? 'All governed checks passed.' : 'One or more checks failed.'}`,
261
+ '',
262
+ `> Generated by Code-Warden v${report.version} at ${report.timestamp}`,
263
+ ];
264
+
265
+ return lines.join('\n');
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // One-line summary (default mode stdout)
270
+ // ---------------------------------------------------------------------------
271
+
272
+ function formatSummary(report) {
273
+ const c = report.checks;
274
+ const parts = [
275
+ `lint:${c.fileLength.status}`,
276
+ `secrets:${c.secrets.status}`,
277
+ `tests:${c.behavioralTests.status}`,
278
+ `health:${c.installHealth.status}`,
279
+ ];
280
+ return `[CodeWarden] Governance report: ${report.result.toUpperCase()} (${parts.join(', ')})`;
281
+ }
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Main
285
+ // ---------------------------------------------------------------------------
286
+
287
+ const { format, scanPath } = parseArgs(process.argv);
288
+ const report = generateReport(scanPath);
289
+
290
+ if (format === 'md') {
291
+ console.log(formatMarkdown(report));
292
+ } else if (format === 'json') {
293
+ console.log(JSON.stringify(report, null, 2));
294
+ } else {
295
+ const json = JSON.stringify(report, null, 2);
296
+ const outPath = path.resolve('.code-warden-report.json');
297
+ fs.writeFileSync(outPath, json, 'utf8');
298
+ console.log(formatSummary(report));
299
+ console.log(`[CodeWarden] Report written to ${outPath}`);
300
+ }
301
+
302
+ process.exit(report.result === 'pass' ? 0 : 1);
@@ -1,112 +1,112 @@
1
- #!/usr/bin/env node
2
- /**
3
- * install-hooks.js
4
- * Merges code-warden PreToolUse hook entries into ~/.claude/settings.json.
5
- *
6
- * Idempotent: removes any existing code-warden entries by description marker,
7
- * then inserts current entries. Replace, not skip — ensures paths stay current
8
- * after reinstalls or version moves.
9
- *
10
- * Requires the skill to already be installed at skillDir before writing
11
- * settings — avoids dangling settings pointing at a missing skill directory.
12
- */
13
-
14
- 'use strict';
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
- const os = require('os');
19
-
20
- const MARKER_PREFIX = 'code-warden:';
21
- const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
22
-
23
- // ---------------------------------------------------------------------------
24
- // Settings I/O
25
- // ---------------------------------------------------------------------------
26
-
27
- function readSettings() {
28
- if (!fs.existsSync(SETTINGS_PATH)) return {};
29
- try {
30
- return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
31
- } catch (err) {
32
- throw new Error(`settings.json exists but could not be parsed (${SETTINGS_PATH}): ${err.message}`);
33
- }
34
- }
35
-
36
- function writeSettings(settings) {
37
- const tmp = `${SETTINGS_PATH}.tmp`;
38
- fs.mkdirSync(path.dirname(SETTINGS_PATH), { recursive: true });
39
- fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n', 'utf8');
40
- fs.renameSync(tmp, SETTINGS_PATH);
41
- }
42
-
43
- // ---------------------------------------------------------------------------
44
- // Hook entry helpers
45
- // ---------------------------------------------------------------------------
46
-
47
- function stripCodeWardenHooks(preToolUse) {
48
- return preToolUse
49
- .map(matcher => ({
50
- ...matcher,
51
- hooks: (matcher.hooks || []).filter(
52
- h => !String(h.description || '').startsWith(MARKER_PREFIX)
53
- ),
54
- }))
55
- .filter(matcher => (matcher.hooks || []).length > 0);
56
- }
57
-
58
- function buildEntries(skillDir) {
59
- return [
60
- {
61
- type: 'command',
62
- command: 'node',
63
- args: [path.join(skillDir, 'tools', 'hooks', 'claude', 'warden-lint-hook.js')],
64
- description: 'code-warden: file length gate',
65
- timeout: 30,
66
- },
67
- {
68
- type: 'command',
69
- command: 'node',
70
- args: [path.join(skillDir, 'tools', 'hooks', 'claude', 'warden-secrets-hook.js')],
71
- description: 'code-warden: zero-trust secrets gate',
72
- timeout: 30,
73
- },
74
- ];
75
- }
76
-
77
- // ---------------------------------------------------------------------------
78
- // Install
79
- // ---------------------------------------------------------------------------
80
-
81
- function installHooks(skillDir) {
82
- // Guard: skill must be installed before settings are written
83
- const required = [
84
- path.join(skillDir, 'SKILL.md'),
85
- path.join(skillDir, 'tools', 'hooks', 'claude', 'warden-lint-hook.js'),
86
- path.join(skillDir, 'tools', 'hooks', 'claude', 'warden-secrets-hook.js'),
87
- ];
88
- for (const p of required) {
89
- if (!fs.existsSync(p)) {
90
- console.error('[CodeWarden] Hooks require an installed Claude target.');
91
- console.error(`[CodeWarden] Missing: ${p}`);
92
- console.error('[CodeWarden] Run: node install.js --target=claude --all');
93
- process.exit(1);
94
- }
95
- }
96
-
97
- const settings = readSettings();
98
- settings.hooks = settings.hooks || {};
99
- const existing = settings.hooks.PreToolUse || [];
100
-
101
- // Remove stale code-warden entries, then append fresh block
102
- const cleaned = stripCodeWardenHooks(existing);
103
- settings.hooks.PreToolUse = [
104
- ...cleaned,
105
- { matcher: 'Write|Edit', hooks: buildEntries(skillDir) },
106
- ];
107
-
108
- writeSettings(settings);
109
- return SETTINGS_PATH;
110
- }
111
-
112
- module.exports = { installHooks };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install-hooks.js
4
+ * Merges code-warden PreToolUse hook entries into ~/.claude/settings.json.
5
+ *
6
+ * Idempotent: removes any existing code-warden entries by description marker,
7
+ * then inserts current entries. Replace, not skip — ensures paths stay current
8
+ * after reinstalls or version moves.
9
+ *
10
+ * Requires the skill to already be installed at skillDir before writing
11
+ * settings — avoids dangling settings pointing at a missing skill directory.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+
20
+ const MARKER_PREFIX = 'code-warden:';
21
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Settings I/O
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function readSettings() {
28
+ if (!fs.existsSync(SETTINGS_PATH)) return {};
29
+ try {
30
+ return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
31
+ } catch (err) {
32
+ throw new Error(`settings.json exists but could not be parsed (${SETTINGS_PATH}): ${err.message}`);
33
+ }
34
+ }
35
+
36
+ function writeSettings(settings) {
37
+ const tmp = `${SETTINGS_PATH}.tmp`;
38
+ fs.mkdirSync(path.dirname(SETTINGS_PATH), { recursive: true });
39
+ fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n', 'utf8');
40
+ fs.renameSync(tmp, SETTINGS_PATH);
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Hook entry helpers
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function stripCodeWardenHooks(preToolUse) {
48
+ return preToolUse
49
+ .map(matcher => ({
50
+ ...matcher,
51
+ hooks: (matcher.hooks || []).filter(
52
+ h => !String(h.description || '').startsWith(MARKER_PREFIX)
53
+ ),
54
+ }))
55
+ .filter(matcher => (matcher.hooks || []).length > 0);
56
+ }
57
+
58
+ function buildEntries(skillDir) {
59
+ return [
60
+ {
61
+ type: 'command',
62
+ command: 'node',
63
+ args: [path.join(skillDir, 'tools', 'hooks', 'claude', 'warden-lint-hook.js')],
64
+ description: 'code-warden: file length gate',
65
+ timeout: 30,
66
+ },
67
+ {
68
+ type: 'command',
69
+ command: 'node',
70
+ args: [path.join(skillDir, 'tools', 'hooks', 'claude', 'warden-secrets-hook.js')],
71
+ description: 'code-warden: zero-trust secrets gate',
72
+ timeout: 30,
73
+ },
74
+ ];
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Install
79
+ // ---------------------------------------------------------------------------
80
+
81
+ function installHooks(skillDir) {
82
+ // Guard: skill must be installed before settings are written
83
+ const required = [
84
+ path.join(skillDir, 'SKILL.md'),
85
+ path.join(skillDir, 'tools', 'hooks', 'claude', 'warden-lint-hook.js'),
86
+ path.join(skillDir, 'tools', 'hooks', 'claude', 'warden-secrets-hook.js'),
87
+ ];
88
+ for (const p of required) {
89
+ if (!fs.existsSync(p)) {
90
+ console.error('[CodeWarden] Hooks require an installed Claude target.');
91
+ console.error(`[CodeWarden] Missing: ${p}`);
92
+ console.error('[CodeWarden] Run: node install.js --target=claude --all');
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ const settings = readSettings();
98
+ settings.hooks = settings.hooks || {};
99
+ const existing = settings.hooks.PreToolUse || [];
100
+
101
+ // Remove stale code-warden entries, then append fresh block
102
+ const cleaned = stripCodeWardenHooks(existing);
103
+ settings.hooks.PreToolUse = [
104
+ ...cleaned,
105
+ { matcher: 'Write|Edit', hooks: buildEntries(skillDir) },
106
+ ];
107
+
108
+ writeSettings(settings);
109
+ return SETTINGS_PATH;
110
+ }
111
+
112
+ module.exports = { installHooks };
@@ -1,75 +1,75 @@
1
- #!/usr/bin/env node
2
- /**
3
- * uninstall-hooks.js
4
- * Removes all code-warden hook entries from ~/.claude/settings.json.
5
- * Identified by description prefix "code-warden:".
6
- * Cleans up empty PreToolUse arrays and empty hooks objects after removal.
7
- */
8
-
9
- 'use strict';
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const os = require('os');
14
-
15
- const MARKER_PREFIX = 'code-warden:';
16
- const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
17
-
18
- function readSettings() {
19
- if (!fs.existsSync(SETTINGS_PATH)) return null;
20
- try {
21
- return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
22
- } catch (err) {
23
- throw new Error(`settings.json could not be parsed (${SETTINGS_PATH}): ${err.message}`);
24
- }
25
- }
26
-
27
- function writeSettings(settings) {
28
- const tmp = `${SETTINGS_PATH}.tmp`;
29
- fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n', 'utf8');
30
- fs.renameSync(tmp, SETTINGS_PATH);
31
- }
32
-
33
- function uninstallHooks() {
34
- const settings = readSettings();
35
-
36
- if (!settings) {
37
- console.log('[CodeWarden] No settings.json found — nothing to remove.');
38
- return false;
39
- }
40
-
41
- if (!settings.hooks || !settings.hooks.PreToolUse) {
42
- console.log('[CodeWarden] No PreToolUse hooks in settings.json — nothing to remove.');
43
- return false;
44
- }
45
-
46
- const before = settings.hooks.PreToolUse;
47
- const cleaned = before
48
- .map(matcher => ({
49
- ...matcher,
50
- hooks: (matcher.hooks || []).filter(
51
- h => !String(h.description || '').startsWith(MARKER_PREFIX)
52
- ),
53
- }))
54
- .filter(matcher => (matcher.hooks || []).length > 0);
55
-
56
- const removedMatchers = before.length - cleaned.length;
57
- const removedHooks = before.flatMap(m => m.hooks || []).filter(
58
- h => String(h.description || '').startsWith(MARKER_PREFIX)
59
- ).length;
60
-
61
- if (removedHooks === 0) {
62
- console.log('[CodeWarden] No code-warden hook entries found — nothing to remove.');
63
- return false;
64
- }
65
-
66
- settings.hooks.PreToolUse = cleaned;
67
- if (cleaned.length === 0) delete settings.hooks.PreToolUse;
68
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
69
-
70
- writeSettings(settings);
71
- console.log(`[CodeWarden] Removed ${removedHooks} hook entry(ies) from settings.json.`);
72
- return true;
73
- }
74
-
75
- module.exports = { uninstallHooks };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * uninstall-hooks.js
4
+ * Removes all code-warden hook entries from ~/.claude/settings.json.
5
+ * Identified by description prefix "code-warden:".
6
+ * Cleans up empty PreToolUse arrays and empty hooks objects after removal.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ const MARKER_PREFIX = 'code-warden:';
16
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
17
+
18
+ function readSettings() {
19
+ if (!fs.existsSync(SETTINGS_PATH)) return null;
20
+ try {
21
+ return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
22
+ } catch (err) {
23
+ throw new Error(`settings.json could not be parsed (${SETTINGS_PATH}): ${err.message}`);
24
+ }
25
+ }
26
+
27
+ function writeSettings(settings) {
28
+ const tmp = `${SETTINGS_PATH}.tmp`;
29
+ fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n', 'utf8');
30
+ fs.renameSync(tmp, SETTINGS_PATH);
31
+ }
32
+
33
+ function uninstallHooks() {
34
+ const settings = readSettings();
35
+
36
+ if (!settings) {
37
+ console.log('[CodeWarden] No settings.json found — nothing to remove.');
38
+ return false;
39
+ }
40
+
41
+ if (!settings.hooks || !settings.hooks.PreToolUse) {
42
+ console.log('[CodeWarden] No PreToolUse hooks in settings.json — nothing to remove.');
43
+ return false;
44
+ }
45
+
46
+ const before = settings.hooks.PreToolUse;
47
+ const cleaned = before
48
+ .map(matcher => ({
49
+ ...matcher,
50
+ hooks: (matcher.hooks || []).filter(
51
+ h => !String(h.description || '').startsWith(MARKER_PREFIX)
52
+ ),
53
+ }))
54
+ .filter(matcher => (matcher.hooks || []).length > 0);
55
+
56
+ const removedMatchers = before.length - cleaned.length;
57
+ const removedHooks = before.flatMap(m => m.hooks || []).filter(
58
+ h => String(h.description || '').startsWith(MARKER_PREFIX)
59
+ ).length;
60
+
61
+ if (removedHooks === 0) {
62
+ console.log('[CodeWarden] No code-warden hook entries found — nothing to remove.');
63
+ return false;
64
+ }
65
+
66
+ settings.hooks.PreToolUse = cleaned;
67
+ if (cleaned.length === 0) delete settings.hooks.PreToolUse;
68
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
69
+
70
+ writeSettings(settings);
71
+ console.log(`[CodeWarden] Removed ${removedHooks} hook entry(ies) from settings.json.`);
72
+ return true;
73
+ }
74
+
75
+ module.exports = { uninstallHooks };