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.
Files changed (40) hide show
  1. package/CONFIGURE.md +39 -39
  2. package/DECISIONS.md +107 -107
  3. package/README.md +6 -0
  4. package/SKILL.md +169 -169
  5. package/bin/code-warden.js +82 -82
  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 +62 -62
  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/tools/auto-detect.js +91 -91
  21. package/tools/auto-targets.js +104 -104
  22. package/tools/auto-windsurf-adapter.js +75 -75
  23. package/tools/get-context.js +50 -50
  24. package/tools/governance-report.js +302 -302
  25. package/tools/hooks/claude/install-hooks.js +112 -112
  26. package/tools/hooks/claude/uninstall-hooks.js +75 -75
  27. package/tools/hooks/claude/warden-lint-hook.js +106 -106
  28. package/tools/hooks/claude/warden-secrets-hook.js +73 -73
  29. package/tools/hooks/codex/install-hooks.js +100 -100
  30. package/tools/hooks/codex/uninstall-hooks.js +53 -53
  31. package/tools/hooks/codex/warden-apply-patch-hook.js +113 -113
  32. package/tools/hooks/codex/warden-bash-hook.js +51 -51
  33. package/tools/lib/config.js +49 -49
  34. package/tools/lib/file-collection.js +5 -2
  35. package/tools/lib/line-count.js +28 -28
  36. package/tools/lib/secret-patterns.js +57 -57
  37. package/tools/tests/fixtures/clean.js +9 -9
  38. package/tools/tests/run-tests.js +210 -210
  39. package/tools/verify-secrets.js +26 -26
  40. 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());