code-warden 3.3.0 → 3.3.1
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 +6 -0
- package/SKILL.md +169 -169
- package/bin/code-warden.js +82 -82
- 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 +62 -62
- 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/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 -302
- 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 +5 -2
- 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
|
@@ -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 };
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* warden-lint-hook.js
|
|
4
|
-
* PreToolUse Claude Code hook: blocks Write/Edit if the resulting file would
|
|
5
|
-
* exceed the configured line limit.
|
|
6
|
-
*
|
|
7
|
-
* Payload (stdin JSON): { tool_name, tool_input: { file_path, content|new_string, ... } }
|
|
8
|
-
* On violation: exit 2 + JSON deny response to stdout.
|
|
9
|
-
* On pass: exit 0 (no output).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
'use strict';
|
|
13
|
-
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
const { countLines } = require('../../lib/line-count');
|
|
17
|
-
const { loadConfig } = require('../../lib/config');
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Config — loaded via shared module; falls back to 400 if missing
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
const { maxFileLength: MAX_LINES } = loadConfig();
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Skip list — file types where line counting is meaningless
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
const SKIP_EXTS = new Set([
|
|
30
|
-
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg',
|
|
31
|
-
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
32
|
-
'.zip', '.tar', '.gz', '.rar', '.7z',
|
|
33
|
-
'.pdf', '.doc', '.docx', '.xls', '.xlsx',
|
|
34
|
-
'.mp3', '.mp4', '.avi', '.mov', '.wav',
|
|
35
|
-
'.map', '.lock',
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
const SKIP_NAMES = new Set(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']);
|
|
39
|
-
|
|
40
|
-
function shouldSkip(filePath) {
|
|
41
|
-
if (!filePath) return true;
|
|
42
|
-
if (SKIP_NAMES.has(path.basename(filePath))) return true;
|
|
43
|
-
return SKIP_EXTS.has(path.extname(filePath).toLowerCase());
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Response helpers
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
function deny(reason) {
|
|
51
|
-
process.stdout.write(JSON.stringify({
|
|
52
|
-
hookSpecificOutput: {
|
|
53
|
-
hookEventName: 'PreToolUse',
|
|
54
|
-
permissionDecision: 'deny',
|
|
55
|
-
permissionDecisionReason: reason,
|
|
56
|
-
},
|
|
57
|
-
}));
|
|
58
|
-
process.exit(2);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const allow = () => process.exit(0);
|
|
62
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Main
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
async function main() {
|
|
68
|
-
let payload;
|
|
69
|
-
try {
|
|
70
|
-
const chunks = [];
|
|
71
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
72
|
-
payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
73
|
-
} catch {
|
|
74
|
-
allow(); // parse failure is non-blocking
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const { tool_name, tool_input = {} } = payload;
|
|
78
|
-
const { file_path, content, old_string, new_string, replace_all } = tool_input;
|
|
79
|
-
|
|
80
|
-
if (tool_name === 'Write') {
|
|
81
|
-
if (shouldSkip(file_path)) allow();
|
|
82
|
-
const lines = countLines(content || '');
|
|
83
|
-
if (lines > MAX_LINES) {
|
|
84
|
-
deny(`[CodeWarden] File length gate: ${path.basename(file_path)} would be ${lines} lines (limit ${MAX_LINES}). Split into modules before writing.`);
|
|
85
|
-
}
|
|
86
|
-
allow();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (tool_name === 'Edit') {
|
|
90
|
-
if (shouldSkip(file_path)) allow();
|
|
91
|
-
if (!fs.existsSync(file_path)) allow(); // new file — Write hook will catch it
|
|
92
|
-
const current = fs.readFileSync(file_path, 'utf8');
|
|
93
|
-
const patched = replace_all
|
|
94
|
-
? current.split(old_string || '').join(new_string || '')
|
|
95
|
-
: current.replace(old_string || '', new_string || '');
|
|
96
|
-
const lines = countLines(patched);
|
|
97
|
-
if (lines > MAX_LINES) {
|
|
98
|
-
deny(`[CodeWarden] File length gate: ${path.basename(file_path)} would be ${lines} lines after edit (limit ${MAX_LINES}). Split into modules before editing.`);
|
|
99
|
-
}
|
|
100
|
-
allow();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
allow(); // unrecognised tool — pass through
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
main().catch(() => allow());
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* warden-lint-hook.js
|
|
4
|
+
* PreToolUse Claude Code hook: blocks Write/Edit if the resulting file would
|
|
5
|
+
* exceed the configured line limit.
|
|
6
|
+
*
|
|
7
|
+
* Payload (stdin JSON): { tool_name, tool_input: { file_path, content|new_string, ... } }
|
|
8
|
+
* On violation: exit 2 + JSON deny response to stdout.
|
|
9
|
+
* On pass: exit 0 (no output).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { countLines } = require('../../lib/line-count');
|
|
17
|
+
const { loadConfig } = require('../../lib/config');
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Config — loaded via shared module; falls back to 400 if missing
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const { maxFileLength: MAX_LINES } = loadConfig();
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Skip list — file types where line counting is meaningless
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const SKIP_EXTS = new Set([
|
|
30
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg',
|
|
31
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
32
|
+
'.zip', '.tar', '.gz', '.rar', '.7z',
|
|
33
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx',
|
|
34
|
+
'.mp3', '.mp4', '.avi', '.mov', '.wav',
|
|
35
|
+
'.map', '.lock',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const SKIP_NAMES = new Set(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']);
|
|
39
|
+
|
|
40
|
+
function shouldSkip(filePath) {
|
|
41
|
+
if (!filePath) return true;
|
|
42
|
+
if (SKIP_NAMES.has(path.basename(filePath))) return true;
|
|
43
|
+
return SKIP_EXTS.has(path.extname(filePath).toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Response helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function deny(reason) {
|
|
51
|
+
process.stdout.write(JSON.stringify({
|
|
52
|
+
hookSpecificOutput: {
|
|
53
|
+
hookEventName: 'PreToolUse',
|
|
54
|
+
permissionDecision: 'deny',
|
|
55
|
+
permissionDecisionReason: reason,
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const allow = () => process.exit(0);
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Main
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
async function main() {
|
|
68
|
+
let payload;
|
|
69
|
+
try {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
72
|
+
payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
73
|
+
} catch {
|
|
74
|
+
allow(); // parse failure is non-blocking
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { tool_name, tool_input = {} } = payload;
|
|
78
|
+
const { file_path, content, old_string, new_string, replace_all } = tool_input;
|
|
79
|
+
|
|
80
|
+
if (tool_name === 'Write') {
|
|
81
|
+
if (shouldSkip(file_path)) allow();
|
|
82
|
+
const lines = countLines(content || '');
|
|
83
|
+
if (lines > MAX_LINES) {
|
|
84
|
+
deny(`[CodeWarden] File length gate: ${path.basename(file_path)} would be ${lines} lines (limit ${MAX_LINES}). Split into modules before writing.`);
|
|
85
|
+
}
|
|
86
|
+
allow();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (tool_name === 'Edit') {
|
|
90
|
+
if (shouldSkip(file_path)) allow();
|
|
91
|
+
if (!fs.existsSync(file_path)) allow(); // new file — Write hook will catch it
|
|
92
|
+
const current = fs.readFileSync(file_path, 'utf8');
|
|
93
|
+
const patched = replace_all
|
|
94
|
+
? current.split(old_string || '').join(new_string || '')
|
|
95
|
+
: current.replace(old_string || '', new_string || '');
|
|
96
|
+
const lines = countLines(patched);
|
|
97
|
+
if (lines > MAX_LINES) {
|
|
98
|
+
deny(`[CodeWarden] File length gate: ${path.basename(file_path)} would be ${lines} lines after edit (limit ${MAX_LINES}). Split into modules before editing.`);
|
|
99
|
+
}
|
|
100
|
+
allow();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
allow(); // unrecognised tool — pass through
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
main().catch(() => allow());
|