code-warden 3.1.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 -0
- package/DECISIONS.md +107 -0
- package/README.md +137 -0
- package/SKILL.md +169 -0
- package/codewarden.json +14 -0
- package/examples/governed-session.md +132 -0
- package/install.js +399 -0
- package/install.ps1 +32 -0
- package/install.sh +33 -0
- package/package.json +19 -0
- package/references/anti-drift.md +55 -0
- package/references/architecture.md +26 -0
- package/references/cleanup.md +30 -0
- package/references/cognition.md +36 -0
- package/references/operations.md +45 -0
- package/references/planning-gates.md +83 -0
- package/references/research-and-fit.md +51 -0
- package/references/safety.md +31 -0
- package/templates/ci/github-actions.yml +66 -0
- package/tools/auto-detect.js +91 -0
- package/tools/auto-targets.js +104 -0
- package/tools/auto-windsurf-adapter.js +75 -0
- package/tools/get-context.js +50 -0
- package/tools/hooks/claude/install-hooks.js +112 -0
- package/tools/hooks/claude/uninstall-hooks.js +75 -0
- package/tools/hooks/claude/warden-lint-hook.js +106 -0
- package/tools/hooks/claude/warden-secrets-hook.js +73 -0
- package/tools/hooks/codex/install-hooks.js +100 -0
- package/tools/hooks/codex/uninstall-hooks.js +53 -0
- package/tools/hooks/codex/warden-apply-patch-hook.js +113 -0
- package/tools/hooks/codex/warden-bash-hook.js +51 -0
- package/tools/lib/config.js +49 -0
- package/tools/lib/file-collection.js +72 -0
- package/tools/lib/line-count.js +28 -0
- package/tools/lib/secret-patterns.js +57 -0
- package/tools/tests/fixtures/clean.js +9 -0
- package/tools/tests/run-tests.js +210 -0
- package/tools/verify-secrets.js +26 -0
- package/tools/warden-lint.js +27 -0
|
@@ -0,0 +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 };
|
|
@@ -0,0 +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());
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* warden-secrets-hook.js
|
|
4
|
+
* PreToolUse Claude Code hook: blocks Write/Edit if content contains
|
|
5
|
+
* hardcoded credentials matching zero-trust secret patterns.
|
|
6
|
+
*
|
|
7
|
+
* Write: scans full content.
|
|
8
|
+
* Edit: scans new_string only (old content was already committed).
|
|
9
|
+
*
|
|
10
|
+
* On violation: exit 2 + JSON deny response to stdout.
|
|
11
|
+
* On pass: exit 0 (no output).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { scanForSecrets } = require('../../lib/secret-patterns');
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Response helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function deny(reason) {
|
|
24
|
+
process.stdout.write(JSON.stringify({
|
|
25
|
+
hookSpecificOutput: {
|
|
26
|
+
hookEventName: 'PreToolUse',
|
|
27
|
+
permissionDecision: 'deny',
|
|
28
|
+
permissionDecisionReason: reason,
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const allow = () => process.exit(0);
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Main
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
let payload;
|
|
42
|
+
try {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
45
|
+
payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
46
|
+
} catch {
|
|
47
|
+
allow();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { tool_name, tool_input = {} } = payload;
|
|
51
|
+
|
|
52
|
+
if (tool_name === 'Write') {
|
|
53
|
+
const hit = scanForSecrets(tool_input.content || '');
|
|
54
|
+
if (hit) {
|
|
55
|
+
const file = path.basename(tool_input.file_path || 'file');
|
|
56
|
+
deny(`[CodeWarden] Hardcoded credential scanner: ${hit.label} detected in ${file}. Use environment variables — no hardcoded credentials.`);
|
|
57
|
+
}
|
|
58
|
+
allow();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (tool_name === 'Edit') {
|
|
62
|
+
const hit = scanForSecrets(tool_input.new_string || '');
|
|
63
|
+
if (hit) {
|
|
64
|
+
const file = path.basename(tool_input.file_path || 'file');
|
|
65
|
+
deny(`[CodeWarden] Hardcoded credential scanner: ${hit.label} detected in replacement for ${file}. Use environment variables — no hardcoded credentials.`);
|
|
66
|
+
}
|
|
67
|
+
allow();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
allow();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main().catch(() => allow());
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* install-hooks.js — Codex PreToolUse hook installer
|
|
4
|
+
*
|
|
5
|
+
* Merges code-warden hook entries into ~/.codex/hooks.json.
|
|
6
|
+
*
|
|
7
|
+
* Codex hooks.json schema:
|
|
8
|
+
* { "PreToolUse": [ { "matcher": "...", "description": "...", "command": "...", "args": [...] } ] }
|
|
9
|
+
*
|
|
10
|
+
* Idempotent: strips existing code-warden entries by description marker,
|
|
11
|
+
* then appends current entries. Ensures paths stay current after reinstalls.
|
|
12
|
+
*
|
|
13
|
+
* Requires the skill to already be installed at skillDir.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
const MARKER_PREFIX = 'code-warden:';
|
|
23
|
+
const HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Hooks file I/O
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function readHooks() {
|
|
30
|
+
if (!fs.existsSync(HOOKS_PATH)) return {};
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(HOOKS_PATH, 'utf8'));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
throw new Error(`hooks.json exists but could not be parsed (${HOOKS_PATH}): ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeHooks(hooks) {
|
|
39
|
+
const tmp = `${HOOKS_PATH}.tmp`;
|
|
40
|
+
fs.mkdirSync(path.dirname(HOOKS_PATH), { recursive: true });
|
|
41
|
+
fs.writeFileSync(tmp, JSON.stringify(hooks, null, 2) + '\n', 'utf8');
|
|
42
|
+
fs.renameSync(tmp, HOOKS_PATH);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Hook entry helpers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
function stripCodeWardenHooks(entries) {
|
|
50
|
+
return (entries || []).filter(
|
|
51
|
+
e => !String(e.description || '').startsWith(MARKER_PREFIX)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildEntries(skillDir) {
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
matcher: 'apply_patch',
|
|
59
|
+
command: 'node',
|
|
60
|
+
args: [path.join(skillDir, 'tools', 'hooks', 'codex', 'warden-apply-patch-hook.js')],
|
|
61
|
+
description: 'code-warden: apply_patch secrets + size gate',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
matcher: 'Bash',
|
|
65
|
+
command: 'node',
|
|
66
|
+
args: [path.join(skillDir, 'tools', 'hooks', 'codex', 'warden-bash-hook.js')],
|
|
67
|
+
description: 'code-warden: bash secrets gate',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Install
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function installHooks(skillDir) {
|
|
77
|
+
// Guard: skill must be installed before hooks are written
|
|
78
|
+
const required = [
|
|
79
|
+
path.join(skillDir, 'SKILL.md'),
|
|
80
|
+
path.join(skillDir, 'tools', 'hooks', 'codex', 'warden-apply-patch-hook.js'),
|
|
81
|
+
path.join(skillDir, 'tools', 'hooks', 'codex', 'warden-bash-hook.js'),
|
|
82
|
+
];
|
|
83
|
+
for (const p of required) {
|
|
84
|
+
if (!fs.existsSync(p)) {
|
|
85
|
+
console.error('[CodeWarden] Hooks require an installed Codex target.');
|
|
86
|
+
console.error(`[CodeWarden] Missing: ${p}`);
|
|
87
|
+
console.error('[CodeWarden] Run: node install.js --target=codex --all');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const hooks = readHooks();
|
|
93
|
+
const cleaned = stripCodeWardenHooks(hooks.PreToolUse);
|
|
94
|
+
hooks.PreToolUse = [...cleaned, ...buildEntries(skillDir)];
|
|
95
|
+
|
|
96
|
+
writeHooks(hooks);
|
|
97
|
+
return HOOKS_PATH;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { installHooks };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* uninstall-hooks.js — Codex PreToolUse hook remover
|
|
4
|
+
*
|
|
5
|
+
* Removes all code-warden entries from ~/.codex/hooks.json.
|
|
6
|
+
* Cleans up empty PreToolUse array and empty top-level object if nothing remains.
|
|
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 HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
|
|
17
|
+
|
|
18
|
+
function uninstallHooks() {
|
|
19
|
+
if (!fs.existsSync(HOOKS_PATH)) {
|
|
20
|
+
console.log('[CodeWarden] ~/.codex/hooks.json not found — nothing to remove.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let hooks;
|
|
25
|
+
try {
|
|
26
|
+
hooks = JSON.parse(fs.readFileSync(HOOKS_PATH, 'utf8'));
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(`[CodeWarden] hooks.json could not be parsed: ${err.message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const before = (hooks.PreToolUse || []).length;
|
|
33
|
+
hooks.PreToolUse = (hooks.PreToolUse || []).filter(
|
|
34
|
+
e => !String(e.description || '').startsWith(MARKER_PREFIX)
|
|
35
|
+
);
|
|
36
|
+
const removed = before - hooks.PreToolUse.length;
|
|
37
|
+
|
|
38
|
+
// Clean empty array
|
|
39
|
+
if (hooks.PreToolUse.length === 0) delete hooks.PreToolUse;
|
|
40
|
+
|
|
41
|
+
// Write back (or remove file if completely empty)
|
|
42
|
+
if (Object.keys(hooks).length === 0) {
|
|
43
|
+
fs.unlinkSync(HOOKS_PATH);
|
|
44
|
+
console.log(`[CodeWarden] Removed ${removed} hook(s) — hooks.json is now empty, file removed.`);
|
|
45
|
+
} else {
|
|
46
|
+
const tmp = `${HOOKS_PATH}.tmp`;
|
|
47
|
+
fs.writeFileSync(tmp, JSON.stringify(hooks, null, 2) + '\n', 'utf8');
|
|
48
|
+
fs.renameSync(tmp, HOOKS_PATH);
|
|
49
|
+
console.log(`[CodeWarden] Removed ${removed} code-warden hook(s) from ~/.codex/hooks.json.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { uninstallHooks };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* warden-apply-patch-hook.js — Codex PreToolUse hook
|
|
4
|
+
*
|
|
5
|
+
* Fires on apply_patch tool calls. Scans added lines for hardcoded credentials
|
|
6
|
+
* and estimates resulting file size where the target path can be extracted.
|
|
7
|
+
*
|
|
8
|
+
* Codex hook payload (stdin, JSON):
|
|
9
|
+
* { tool: "apply_patch", toolInput: { patch: "<patch text>" } }
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — proceed
|
|
13
|
+
* 2 — block (writes JSON deny to stdout)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { scanForSecrets } = require('../../lib/secret-patterns');
|
|
21
|
+
const { countLines } = require('../../lib/line-count');
|
|
22
|
+
const { loadConfig } = require('../../lib/config');
|
|
23
|
+
|
|
24
|
+
const { maxFileLength: maxLines } = loadConfig();
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
function deny(reason) {
|
|
28
|
+
process.stdout.write(JSON.stringify({ deny: true, message: `[CodeWarden] ${reason}` }) + '\n');
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Patch parsing helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract lines added by the patch (lines starting with '+', not '+++').
|
|
38
|
+
*/
|
|
39
|
+
function extractAddedLines(patch) {
|
|
40
|
+
return patch.split('\n')
|
|
41
|
+
.filter(l => l.startsWith('+') && !l.startsWith('+++'))
|
|
42
|
+
.map(l => l.slice(1));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract target file path from patch header (*** path or +++ path or --- path).
|
|
47
|
+
* Returns null if not detectable.
|
|
48
|
+
*/
|
|
49
|
+
function extractTargetPath(patch) {
|
|
50
|
+
for (const line of patch.split('\n')) {
|
|
51
|
+
const m = line.match(/^\+{3}\s+(.+?)(\s+\d{4}-\d{2}-\d{2}.*)?$/) ||
|
|
52
|
+
line.match(/^\*{3}\s+(.+?)(\s+\d{4}-\d{2}-\d{2}.*)?$/);
|
|
53
|
+
if (m) {
|
|
54
|
+
const p = m[1].trim();
|
|
55
|
+
if (p !== '/dev/null') return p;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Estimate resulting line count: base file lines + added lines - removed lines.
|
|
63
|
+
* Returns null if file not readable.
|
|
64
|
+
*/
|
|
65
|
+
function estimateResultLines(patch, targetPath) {
|
|
66
|
+
let baseLines = 0;
|
|
67
|
+
if (targetPath && fs.existsSync(targetPath)) {
|
|
68
|
+
try {
|
|
69
|
+
baseLines = countLines(fs.readFileSync(targetPath, 'utf8'));
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
} else if (!targetPath) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// Count added and removed lines
|
|
75
|
+
let added = 0;
|
|
76
|
+
let removed = 0;
|
|
77
|
+
for (const line of patch.split('\n')) {
|
|
78
|
+
if (line.startsWith('+') && !line.startsWith('+++')) added++;
|
|
79
|
+
else if (line.startsWith('-') && !line.startsWith('---')) removed++;
|
|
80
|
+
}
|
|
81
|
+
return baseLines + added - removed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Main
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
let raw = '';
|
|
89
|
+
process.stdin.setEncoding('utf8');
|
|
90
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
91
|
+
process.stdin.on('end', () => {
|
|
92
|
+
let payload;
|
|
93
|
+
try { payload = JSON.parse(raw); } catch { process.exit(0); }
|
|
94
|
+
|
|
95
|
+
const input = payload.toolInput || payload.tool_input || {};
|
|
96
|
+
const patch = String(input.patch || '');
|
|
97
|
+
if (!patch) process.exit(0);
|
|
98
|
+
|
|
99
|
+
// --- Secrets check on added lines ---
|
|
100
|
+
const added = extractAddedLines(patch);
|
|
101
|
+
const addedText = added.join('\n');
|
|
102
|
+
const hit = scanForSecrets(addedText);
|
|
103
|
+
if (hit) deny(`Blocked apply_patch — hardcoded credential detected (${hit.label}). Remove before patching.`);
|
|
104
|
+
|
|
105
|
+
// --- File length check ---
|
|
106
|
+
const targetPath = extractTargetPath(patch);
|
|
107
|
+
const estimated = estimateResultLines(patch, targetPath);
|
|
108
|
+
if (estimated !== null && estimated > maxLines) {
|
|
109
|
+
deny(`Blocked apply_patch — resulting file would be ~${estimated} lines (limit ${maxLines}). Break it up first.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
process.exit(0);
|
|
113
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* warden-bash-hook.js — Codex PreToolUse hook
|
|
4
|
+
*
|
|
5
|
+
* Fires on Bash tool calls. Scans the command string for patterns that
|
|
6
|
+
* would embed hardcoded credentials into files (e.g. echo/printf/cat with
|
|
7
|
+
* secret values, curl with Authorization headers, env assignments, etc.).
|
|
8
|
+
*
|
|
9
|
+
* This is a best-effort surface: Bash is intentionally wide. The hook catches
|
|
10
|
+
* the most common accidental secret exposure patterns; it does not attempt to
|
|
11
|
+
* sandbox arbitrary shell execution.
|
|
12
|
+
*
|
|
13
|
+
* Codex hook payload (stdin, JSON):
|
|
14
|
+
* { tool: "Bash", toolInput: { command: "<shell command>" } }
|
|
15
|
+
*
|
|
16
|
+
* Exit codes:
|
|
17
|
+
* 0 — proceed
|
|
18
|
+
* 2 — block (writes JSON deny to stdout)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const { scanForSecrets } = require('../../lib/secret-patterns');
|
|
24
|
+
|
|
25
|
+
function deny(reason) {
|
|
26
|
+
process.stdout.write(JSON.stringify({ deny: true, message: `[CodeWarden] ${reason}` }) + '\n');
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Main
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
let raw = '';
|
|
35
|
+
process.stdin.setEncoding('utf8');
|
|
36
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
37
|
+
process.stdin.on('end', () => {
|
|
38
|
+
let payload;
|
|
39
|
+
try { payload = JSON.parse(raw); } catch { process.exit(0); }
|
|
40
|
+
|
|
41
|
+
const input = payload.toolInput || payload.tool_input || {};
|
|
42
|
+
const command = String(input.command || '');
|
|
43
|
+
if (!command) process.exit(0);
|
|
44
|
+
|
|
45
|
+
const hit = scanForSecrets(command);
|
|
46
|
+
if (hit) {
|
|
47
|
+
deny(`Blocked Bash command — hardcoded credential detected (${hit.label}). Use environment variables or a secrets manager instead.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
process.exit(0);
|
|
51
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* config.js
|
|
6
|
+
* Shared codewarden.json loader.
|
|
7
|
+
*
|
|
8
|
+
* Previously warden-lint.js, warden-lint-hook.js, and warden-apply-patch-hook.js
|
|
9
|
+
* each parsed codewarden.json independently with slightly different fallback paths.
|
|
10
|
+
* This module centralises config loading with a single documented precedence.
|
|
11
|
+
*
|
|
12
|
+
* Resolution order for config file:
|
|
13
|
+
* 1. Explicit path passed to loadConfig(configPath)
|
|
14
|
+
* 2. <__dirname>/../../codewarden.json (relative to tools/lib/ → skill root)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'codewarden.json');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load and parse codewarden.json, returning a merged config object.
|
|
24
|
+
* Falls back to defaults silently if the file is missing or unparseable.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} [configPath] - Override the config file location
|
|
27
|
+
* @returns {{ maxFileLength: number }}
|
|
28
|
+
*/
|
|
29
|
+
function loadConfig(configPath) {
|
|
30
|
+
const target = configPath || DEFAULT_CONFIG_PATH;
|
|
31
|
+
let maxFileLength = 400;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(target, 'utf8');
|
|
35
|
+
const cfg = JSON.parse(raw);
|
|
36
|
+
const configured =
|
|
37
|
+
cfg?.thresholds?.max_file_length ??
|
|
38
|
+
cfg?.max_file_length;
|
|
39
|
+
if (typeof configured === 'number' && configured > 0) {
|
|
40
|
+
maxFileLength = configured;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Missing or invalid config — use defaults
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { maxFileLength };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { loadConfig, DEFAULT_CONFIG_PATH };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* file-collection.js
|
|
6
|
+
* Shared file traversal helpers for warden-lint and verify-secrets CLI tools.
|
|
7
|
+
*
|
|
8
|
+
* Previously each CLI tool duplicated identical SKIP_DIRS, SKIP_EXTS,
|
|
9
|
+
* collectFiles, and expandPaths logic — any change had to be made twice.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'dist', '.next']);
|
|
16
|
+
|
|
17
|
+
const SKIP_EXTS = new Set([
|
|
18
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp',
|
|
19
|
+
'.zip', '.tar', '.gz', '.7z', '.rar',
|
|
20
|
+
'.dll', '.exe', '.bin', '.so', '.dylib',
|
|
21
|
+
'.pdf', '.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
22
|
+
'.mp4', '.mp3', '.wav', '.ogg', '.avi', '.mov',
|
|
23
|
+
'.map', '.lock',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Recursively collect all scannable files under a directory.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} dir
|
|
30
|
+
* @param {string[]} results - accumulator (mutated)
|
|
31
|
+
*/
|
|
32
|
+
function collectFiles(dir, results) {
|
|
33
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
34
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
35
|
+
const full = path.join(dir, entry);
|
|
36
|
+
const stat = fs.statSync(full);
|
|
37
|
+
if (stat.isDirectory()) {
|
|
38
|
+
collectFiles(full, results);
|
|
39
|
+
} else if (!SKIP_EXTS.has(path.extname(entry).toLowerCase())) {
|
|
40
|
+
results.push(full);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Expand a list of CLI path arguments into a flat array of file paths.
|
|
47
|
+
* Directories are walked recursively; individual files are included as-is.
|
|
48
|
+
* Exits with usage error if no args provided or a path is not found.
|
|
49
|
+
*
|
|
50
|
+
* @param {string[]} args - process.argv slice
|
|
51
|
+
* @param {string} toolName - used in the usage message
|
|
52
|
+
* @returns {string[]}
|
|
53
|
+
*/
|
|
54
|
+
function expandPaths(args, toolName) {
|
|
55
|
+
if (args.length === 0) {
|
|
56
|
+
console.log(`Usage: ${toolName} <file|dir> [file|dir] ...`);
|
|
57
|
+
console.log(` node tools/${toolName} . # scan entire project`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const files = [];
|
|
61
|
+
for (const arg of args) {
|
|
62
|
+
if (!fs.existsSync(arg)) {
|
|
63
|
+
console.error(`Error: path not found: ${arg}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (fs.statSync(arg).isDirectory()) collectFiles(arg, files);
|
|
67
|
+
else files.push(arg);
|
|
68
|
+
}
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { collectFiles, expandPaths, SKIP_DIRS, SKIP_EXTS };
|