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.
- package/CONFIGURE.md +39 -39
- package/DECISIONS.md +107 -107
- package/README.md +199 -137
- package/SKILL.md +169 -169
- package/bin/code-warden.js +82 -0
- package/codewarden.json +14 -14
- package/examples/governed-session.md +132 -132
- package/install.js +399 -399
- package/install.ps1 +32 -32
- package/install.sh +33 -33
- package/package.json +45 -2
- package/references/anti-drift.md +55 -55
- package/references/architecture.md +26 -26
- package/references/cleanup.md +30 -30
- package/references/cognition.md +36 -36
- package/references/operations.md +45 -45
- package/references/planning-gates.md +83 -83
- package/references/research-and-fit.md +51 -51
- package/references/safety.md +31 -31
- package/templates/ci/github-actions.yml +83 -66
- package/tools/auto-detect.js +91 -91
- package/tools/auto-targets.js +104 -104
- package/tools/auto-windsurf-adapter.js +75 -75
- package/tools/get-context.js +50 -50
- package/tools/governance-report.js +302 -0
- package/tools/hooks/claude/install-hooks.js +112 -112
- package/tools/hooks/claude/uninstall-hooks.js +75 -75
- package/tools/hooks/claude/warden-lint-hook.js +106 -106
- package/tools/hooks/claude/warden-secrets-hook.js +73 -73
- package/tools/hooks/codex/install-hooks.js +100 -100
- package/tools/hooks/codex/uninstall-hooks.js +53 -53
- package/tools/hooks/codex/warden-apply-patch-hook.js +113 -113
- package/tools/hooks/codex/warden-bash-hook.js +51 -51
- package/tools/lib/config.js +49 -49
- package/tools/lib/file-collection.js +72 -72
- package/tools/lib/line-count.js +28 -28
- package/tools/lib/secret-patterns.js +57 -57
- package/tools/tests/fixtures/clean.js +9 -9
- package/tools/tests/run-tests.js +210 -210
- package/tools/verify-secrets.js +26 -26
- 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 };
|