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.
Files changed (39) hide show
  1. package/CONFIGURE.md +39 -0
  2. package/DECISIONS.md +107 -0
  3. package/README.md +137 -0
  4. package/SKILL.md +169 -0
  5. package/codewarden.json +14 -0
  6. package/examples/governed-session.md +132 -0
  7. package/install.js +399 -0
  8. package/install.ps1 +32 -0
  9. package/install.sh +33 -0
  10. package/package.json +19 -0
  11. package/references/anti-drift.md +55 -0
  12. package/references/architecture.md +26 -0
  13. package/references/cleanup.md +30 -0
  14. package/references/cognition.md +36 -0
  15. package/references/operations.md +45 -0
  16. package/references/planning-gates.md +83 -0
  17. package/references/research-and-fit.md +51 -0
  18. package/references/safety.md +31 -0
  19. package/templates/ci/github-actions.yml +66 -0
  20. package/tools/auto-detect.js +91 -0
  21. package/tools/auto-targets.js +104 -0
  22. package/tools/auto-windsurf-adapter.js +75 -0
  23. package/tools/get-context.js +50 -0
  24. package/tools/hooks/claude/install-hooks.js +112 -0
  25. package/tools/hooks/claude/uninstall-hooks.js +75 -0
  26. package/tools/hooks/claude/warden-lint-hook.js +106 -0
  27. package/tools/hooks/claude/warden-secrets-hook.js +73 -0
  28. package/tools/hooks/codex/install-hooks.js +100 -0
  29. package/tools/hooks/codex/uninstall-hooks.js +53 -0
  30. package/tools/hooks/codex/warden-apply-patch-hook.js +113 -0
  31. package/tools/hooks/codex/warden-bash-hook.js +51 -0
  32. package/tools/lib/config.js +49 -0
  33. package/tools/lib/file-collection.js +72 -0
  34. package/tools/lib/line-count.js +28 -0
  35. package/tools/lib/secret-patterns.js +57 -0
  36. package/tools/tests/fixtures/clean.js +9 -0
  37. package/tools/tests/run-tests.js +210 -0
  38. package/tools/verify-secrets.js +26 -0
  39. package/tools/warden-lint.js +27 -0
@@ -0,0 +1,83 @@
1
+ # Planning Gates
2
+
3
+ Two mandatory declaration blocks that fire before implementation begins.
4
+ Neither is a checklist — they are structured outputs the AI produces and
5
+ the user confirms before any code is written.
6
+
7
+ ---
8
+
9
+ ## Scope Gate
10
+
11
+ Fires when: a new session begins, scope is ambiguous, or a request would
12
+ touch files outside the current declared scope.
13
+
14
+ **AI produces this block. User confirms before proceeding.**
15
+
16
+ ```
17
+ SCOPE GATE
18
+
19
+ Goal: [One sentence — what this session will accomplish]
20
+ Non-goals: [What is explicitly out of scope for this session]
21
+ Files in: [Complete list of files the AI expects to read or modify]
22
+ Files out: [Files that must not be touched, with reason]
23
+ Verify after: [The exact commands that will confirm success]
24
+ Rollback: [One-step revert — e.g. git checkout HEAD -- <files>]
25
+ ```
26
+
27
+ Rules:
28
+ - Goal must be one sentence. If it cannot be, split into multiple sessions.
29
+ - Files-in list is a contract. Any unlisted file requires a new Scope Gate
30
+ or explicit user approval before it is touched.
31
+ - Rollback must be a concrete command, not "undo manually."
32
+ - If any field is unknown, write `[UNKNOWN — user must provide]` and halt.
33
+
34
+ ---
35
+
36
+ ## Plan Gate
37
+
38
+ Fires when: the session will touch more than one file, or any single change
39
+ exceeds 30 lines.
40
+
41
+ **AI produces this block after Scope Gate is confirmed. User confirms before any edits.**
42
+
43
+ ```
44
+ PLAN GATE
45
+
46
+ Patch order:
47
+ 1. <file> — <one-line description of change>
48
+ 2. <file> — <one-line description of change>
49
+ ...
50
+
51
+ Blast radius class: [CONTAINED | MODERATE | HIGH]
52
+ CONTAINED — changes isolated to declared files, no interface changes
53
+ MODERATE — shared interfaces or types modified, downstream callers may need updates
54
+ HIGH — data-flow changes, schema changes, or deletions of public APIs
55
+
56
+ Human checkpoint: [YES | NO]
57
+ YES if: blast radius is MODERATE or HIGH, or more than 2 files are touched.
58
+
59
+ Post-patch checks:
60
+ After step 1: [command or verification]
61
+ After step 2: [command or verification]
62
+ After all: [final verification suite]
63
+ ```
64
+
65
+ Rules:
66
+ - Patch order is sequential. Do not parallelise edits across files unless
67
+ confirmed safe and explicitly approved.
68
+ - Blast radius class must be declared before the first edit, not assessed after.
69
+ - If Human Checkpoint is YES, pause after the last patch and output
70
+ `[AWAITING CONFIRMATION]` before marking the task complete.
71
+ - Post-patch checks must be concrete commands, not "verify it works."
72
+
73
+ ---
74
+
75
+ ## Gate Failure Response
76
+
77
+ If either gate cannot be completed:
78
+
79
+ 1. State which field is blocking and why.
80
+ 2. Ask for the minimum information needed to fill it.
81
+ 3. Do not produce partial gates. Do not proceed without both gates confirmed.
82
+
83
+ A partial plan is not a plan. Implement nothing until both gates are signed off.
@@ -0,0 +1,51 @@
1
+ # Research and Fit
2
+
3
+ ## Live Research Gate
4
+
5
+ Do not rely on model training data when a decision depends on current facts.
6
+ Run live research first when the task involves:
7
+ - Current package versions, APIs, pricing, licenses, limits, or deprecations.
8
+ - Framework, runtime, cloud, database, or platform recommendations.
9
+ - Security, legal, policy, compliance, or financial claims.
10
+ - Recently released tools, libraries, models, protocols, or standards.
11
+ - Anything the user describes as latest, current, modern, today, new, or changed.
12
+
13
+ Research standard:
14
+ - Prefer official docs, release notes, standards, or source repositories.
15
+ - Use registry metadata for package versions and license facts.
16
+ - Capture the date-sensitive fact, source, and date accessed in the response or decision log.
17
+ - If live research is unavailable, say so and treat the claim as unverified.
18
+
19
+ ## Default-Pattern Challenge
20
+
21
+ Before choosing a stack, architecture, or product shape, challenge familiar defaults.
22
+ Do not pick Node, Next.js, React, a SaaS dashboard, CRUD admin, or auth-first app
23
+ unless the project context makes that choice fit.
24
+
25
+ Required fit check:
26
+ - User goal: what is the thing being built?
27
+ - Primary user: who uses it and under what constraints?
28
+ - Runtime/environment: browser, desktop, mobile, server, embedded, game engine, CLI, or hybrid.
29
+ - Data shape: static, local-first, collaborative, realtime, batch, transactional, analytical, or media-heavy.
30
+ - Interaction shape: workflow tool, creative tool, game, simulation, content site, dashboard, automation, API, or library.
31
+ - Operational constraints: deployment target, offline needs, privacy, performance, budget, and maintenance burden.
32
+
33
+ ## Recommendation Discipline
34
+
35
+ When recommending an approach over alternatives:
36
+ - Name at least two viable alternatives unless the user already chose the stack.
37
+ - Explain why the chosen approach fits the project's constraints better.
38
+ - Identify the main tradeoff or downside.
39
+ - Do not optimize for what is easiest for the model to generate.
40
+
41
+ ## Product-Shape Guardrail
42
+
43
+ Do not assume every app is a SaaS dashboard.
44
+ Match the first screen and navigation to the domain:
45
+ - Operational tools can be dense and workflow-first.
46
+ - Creative tools should put the canvas or creation surface first.
47
+ - Games should open on the playable loop, not a marketing page.
48
+ - Content sites should prioritize the content object or story.
49
+ - Developer tools should expose the core command, API, or artifact early.
50
+
51
+ If the shape is unclear, make a small fit assessment before designing or coding.
@@ -0,0 +1,31 @@
1
+ # Execution and Safety
2
+
3
+ ## Blast Radius Check
4
+
5
+ Before deleting or rewriting any working code, define:
6
+ 1. **What might break** - list affected modules and interfaces.
7
+ 2. **How it will be tested** - specific test strategy or smoke test.
8
+ 3. **Rollback procedure** - one-step command or revert method.
9
+
10
+ Example: `git checkout HEAD -- path/to/file`
11
+
12
+ Never skip for any rewrite touching core logic.
13
+
14
+ ## Patch-First Editing
15
+
16
+ - Prefer diff/patch edits over full file rewrites.
17
+ - Only rewrite an entire file when structural changes affect >50% of its content.
18
+ - All full rewrites require a Blast Radius Check first.
19
+ - Surgical edits reduce accidental deletions, regression bugs, and context loss.
20
+
21
+ ## Zero-Trust Secrets
22
+
23
+ - Never generate code with hardcoded API keys, passwords, tokens, or placeholder secrets.
24
+ - Always default to environment variables such as `.env` or secure vault implementations.
25
+ - No "TODO: replace later" escape hatches. Ever.
26
+
27
+ ## Dependency Freeze
28
+
29
+ - Before any structural changes, list all current dependency versions.
30
+ - Do not silently upgrade packages during a refactor.
31
+ - Flag outdated dependencies separately. Do not fix without explicit confirmation.
@@ -0,0 +1,66 @@
1
+ # Code-Warden Quality Gate — Project Template
2
+ # https://github.com/Kodaxadev/Code-Warden
3
+ #
4
+ # Copy this file to .github/workflows/code-warden.yml in your project.
5
+ #
6
+ # What it enforces:
7
+ # - File length limits (warden-lint.js, default 400 lines per codewarden.json)
8
+ # - Zero-trust secrets (verify-secrets.js, hardcoded-credential patterns)
9
+ #
10
+ # How code-warden is made available in CI (choose one):
11
+ #
12
+ # Option A — Download from release (recommended, no files to commit)
13
+ # Set CODE_WARDEN_VERSION below to pin a specific release.
14
+ # The "Install Code-Warden" step downloads and extracts automatically.
15
+ #
16
+ # Option B — Commit to your repo
17
+ # Run: node /path/to/code-warden/install.js --target=claude
18
+ # Add .claude/skills/code-warden/ to git tracking.
19
+ # Set CODE_WARDEN_PATH: .claude/skills/code-warden
20
+ # Remove the "Install Code-Warden" step.
21
+ #
22
+ # Customise thresholds in codewarden.json after install:
23
+ # max_file_length (default 400 lines)
24
+ # pre_flight_trigger_lines (default 150 lines)
25
+ # human_checkpoint_files (default 2 files)
26
+
27
+ name: Code-Warden Quality Gate
28
+
29
+ on:
30
+ push:
31
+ branches: [main, master]
32
+ pull_request:
33
+ branches: [main, master]
34
+
35
+ env:
36
+ CODE_WARDEN_VERSION: v3.1.0
37
+ CODE_WARDEN_PATH: .code-warden-ci
38
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
39
+
40
+ jobs:
41
+ code-warden:
42
+ name: Code-Warden Quality Gate
43
+ runs-on: ubuntu-latest
44
+
45
+ steps:
46
+ - name: Checkout
47
+ uses: actions/checkout@v4
48
+
49
+ - name: Setup Node.js
50
+ uses: actions/setup-node@v4
51
+ with:
52
+ node-version: '24'
53
+
54
+ # Option A: download from GitHub release (remove if using Option B)
55
+ - name: Install Code-Warden
56
+ run: |
57
+ curl -fsSL -o cw.zip \
58
+ "https://github.com/Kodaxadev/Code-Warden/releases/download/${{ env.CODE_WARDEN_VERSION }}/code-warden-${{ env.CODE_WARDEN_VERSION }}.zip"
59
+ mkdir -p ${{ env.CODE_WARDEN_PATH }}
60
+ unzip -q cw.zip -d ${{ env.CODE_WARDEN_PATH }}
61
+
62
+ - name: Lint — enforce file length limits
63
+ run: node ${{ env.CODE_WARDEN_PATH }}/tools/warden-lint.js .
64
+
65
+ - name: Secrets — zero-trust scan
66
+ run: node ${{ env.CODE_WARDEN_PATH }}/tools/verify-secrets.js .
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * auto-detect.js
4
+ * Detection logic for the code-warden auto-installer.
5
+ * Exports scanTargets(targets) -> targets annotated with detected/method fields.
6
+ *
7
+ * Detection order per target:
8
+ * 1. Binary found in PATH (where / which)
9
+ * 2. Config directory exists in HOME
10
+ * 3. App install path exists (platform-specific)
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const { execSync } = require('child_process');
15
+
16
+ /**
17
+ * Returns true if a CLI binary is resolvable via PATH.
18
+ * Uses 'where' on Windows, 'which' on Unix.
19
+ */
20
+ function commandExists(bin) {
21
+ try {
22
+ const cmd = process.platform === 'win32' ? `where ${bin}` : `which ${bin}`;
23
+ execSync(cmd, { stdio: 'ignore' });
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Returns true if a path exists and is a directory.
32
+ */
33
+ function dirExists(p) {
34
+ try {
35
+ return fs.existsSync(p) && fs.statSync(p).isDirectory();
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Returns true if a path exists (file or directory).
43
+ */
44
+ function pathExists(p) {
45
+ try {
46
+ return fs.existsSync(p);
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Checks all detection signals for a single target.
54
+ * Returns { detected: boolean, method: string|null }
55
+ */
56
+ function isInstalled(target) {
57
+ for (const bin of (target.detect.binaries || [])) {
58
+ if (commandExists(bin)) {
59
+ return { detected: true, method: `binary:${bin}` };
60
+ }
61
+ }
62
+
63
+ for (const dir of (target.detect.dirs || [])) {
64
+ if (dirExists(dir)) {
65
+ return { detected: true, method: `dir:${dir}` };
66
+ }
67
+ }
68
+
69
+ const platformApps = (target.detect.apps || {})[process.platform] || [];
70
+ for (const appPath of platformApps) {
71
+ if (pathExists(appPath)) {
72
+ return { detected: true, method: `app:${appPath}` };
73
+ }
74
+ }
75
+
76
+ return { detected: false, method: null };
77
+ }
78
+
79
+ /**
80
+ * Scans all targets and annotates each with detection results.
81
+ * @param {Array} targets - TARGETS array from auto-targets.js
82
+ * @returns {Array} - same array with detected and method fields added
83
+ */
84
+ function scanTargets(targets) {
85
+ return targets.map(target => {
86
+ const result = isInstalled(target);
87
+ return { ...target, ...result };
88
+ });
89
+ }
90
+
91
+ module.exports = { scanTargets, isInstalled, commandExists };
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * auto-targets.js
4
+ * Target registry for the code-warden auto-installer.
5
+ * Each entry describes one AI app, how to detect it, and where to install.
6
+ *
7
+ * format values:
8
+ * 'skill-md' - copy the full skill folder (SKILL.md + references/ + tools/)
9
+ * 'windsurf-flat' - concatenate into a single .md file via auto-windsurf-adapter.js
10
+ */
11
+
12
+ const os = require('os');
13
+ const path = require('path');
14
+
15
+ const HOME = os.homedir();
16
+ const LOCALAPPDATA = process.env.LOCALAPPDATA || '';
17
+ const APPDATA = process.env.APPDATA || '';
18
+
19
+ const TARGETS = [
20
+ {
21
+ id: 'claude',
22
+ name: 'Claude Code',
23
+ format: 'skill-md',
24
+ skillsDir: path.join(HOME, '.claude', 'skills'),
25
+ detect: {
26
+ binaries: ['claude'],
27
+ dirs: [path.join(HOME, '.claude')],
28
+ apps: {
29
+ darwin: ['/Applications/Claude.app'],
30
+ win32: [path.join(LOCALAPPDATA, 'Programs', 'claude', 'claude.exe')],
31
+ linux: [],
32
+ },
33
+ },
34
+ },
35
+ {
36
+ id: 'cursor',
37
+ name: 'Cursor',
38
+ format: 'skill-md',
39
+ skillsDir: path.join(HOME, '.cursor', 'skills'),
40
+ detect: {
41
+ binaries: ['cursor'],
42
+ dirs: [path.join(HOME, '.cursor')],
43
+ apps: {
44
+ darwin: ['/Applications/Cursor.app'],
45
+ win32: [path.join(LOCALAPPDATA, 'Programs', 'cursor', 'Cursor.exe')],
46
+ linux: [path.join(HOME, '.local', 'share', 'cursor')],
47
+ },
48
+ },
49
+ },
50
+ {
51
+ id: 'warp',
52
+ name: 'Warp',
53
+ format: 'skill-md',
54
+ skillsDir: path.join(HOME, '.warp', 'skills'),
55
+ detect: {
56
+ binaries: ['warp', 'warp-terminal'],
57
+ dirs: [path.join(HOME, '.warp')],
58
+ apps: {
59
+ darwin: ['/Applications/Warp.app'],
60
+ win32: [path.join(LOCALAPPDATA, 'Programs', 'Warp', 'Warp.exe')],
61
+ linux: [],
62
+ },
63
+ },
64
+ },
65
+ {
66
+ id: 'codex',
67
+ name: 'OpenAI Codex',
68
+ format: 'skill-md',
69
+ skillsDir: path.join(HOME, '.codex', 'skills'),
70
+ detect: {
71
+ binaries: ['codex'],
72
+ dirs: [path.join(HOME, '.codex')],
73
+ apps: { darwin: [], win32: [], linux: [] },
74
+ },
75
+ },
76
+ {
77
+ id: 'agents',
78
+ name: 'Generic Agents',
79
+ format: 'skill-md',
80
+ skillsDir: path.join(HOME, '.agents', 'skills'),
81
+ detect: {
82
+ binaries: [],
83
+ dirs: [path.join(HOME, '.agents')],
84
+ apps: { darwin: [], win32: [], linux: [] },
85
+ },
86
+ },
87
+ {
88
+ id: 'windsurf',
89
+ name: 'Windsurf',
90
+ format: 'windsurf-flat',
91
+ skillsDir: path.join(HOME, '.windsurf', 'rules'),
92
+ detect: {
93
+ binaries: ['windsurf'],
94
+ dirs: [path.join(HOME, '.windsurf')],
95
+ apps: {
96
+ darwin: ['/Applications/Windsurf.app'],
97
+ win32: [path.join(LOCALAPPDATA, 'Programs', 'Windsurf', 'Windsurf.exe')],
98
+ linux: [],
99
+ },
100
+ },
101
+ },
102
+ ];
103
+
104
+ module.exports = { TARGETS };
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * auto-windsurf-adapter.js
4
+ * Converts the modular code-warden skill into a single flat markdown file
5
+ * compatible with Windsurf's rules format (.windsurf/rules/).
6
+ *
7
+ * Concatenation order:
8
+ * SKILL.md -> planning-gates -> architecture -> safety -> cognition -> cleanup
9
+ * -> anti-drift -> operations -> research-and-fit
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const REFERENCE_ORDER = [
16
+ 'planning-gates',
17
+ 'architecture',
18
+ 'safety',
19
+ 'cognition',
20
+ 'cleanup',
21
+ 'anti-drift',
22
+ 'operations',
23
+ 'research-and-fit',
24
+ ];
25
+
26
+ const OUTPUT_FILENAME = 'code-warden.md';
27
+
28
+ /**
29
+ * Reads a file and returns its content.
30
+ * - SKILL.md missing: throws (hard fail — output is meaningless without it)
31
+ * - Reference missing: warns to stderr, inserts a comment, continues
32
+ */
33
+ function readSafe(filePath, label) {
34
+ if (!fs.existsSync(filePath)) {
35
+ if (label === 'SKILL.md') {
36
+ throw new Error(`SKILL.md not found at ${filePath} — cannot generate Windsurf output.`);
37
+ }
38
+ console.warn(`[CodeWarden] WARN ${label} not found, skipping.`);
39
+ return `\n<!-- [CodeWarden] WARNING: ${label} not found — section omitted -->\n`;
40
+ }
41
+ return fs.readFileSync(filePath, 'utf8');
42
+ }
43
+
44
+ /**
45
+ * Builds and writes the flat Windsurf rules file.
46
+ * @param {string} sourceDir - root of the code-warden skill source
47
+ * @param {string} destDir - target directory (e.g. ~/.windsurf/rules)
48
+ * @returns {string} - full path of the written file
49
+ */
50
+ function installWindsurf(sourceDir, destDir) {
51
+ const sections = [];
52
+
53
+ sections.push('<!-- Generated by code-warden auto-installer. Do not edit manually. -->');
54
+ sections.push('<!-- Source: https://github.com/Kodaxadev/Code-Warden -->');
55
+ sections.push('');
56
+
57
+ const skillMdPath = path.join(sourceDir, 'SKILL.md');
58
+ sections.push(readSafe(skillMdPath, 'SKILL.md'));
59
+
60
+ for (const ref of REFERENCE_ORDER) {
61
+ const refPath = path.join(sourceDir, 'references', `${ref}.md`);
62
+ sections.push(`\n---\n`);
63
+ sections.push(readSafe(refPath, `references/${ref}.md`));
64
+ }
65
+
66
+ const output = sections.join('\n');
67
+
68
+ fs.mkdirSync(destDir, { recursive: true });
69
+ const outPath = path.join(destDir, OUTPUT_FILENAME);
70
+ fs.writeFileSync(outPath, output, 'utf8');
71
+
72
+ return outPath;
73
+ }
74
+
75
+ module.exports = { installWindsurf };
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const candidates = [
6
+ 'AGENTS.md',
7
+ '.codex/AGENTS.md',
8
+ 'ARCHITECTURE.md',
9
+ 'docs/ARCHITECTURE.md',
10
+ '.agents/AGENTS.md',
11
+ '.claude/CLAUDE.md',
12
+ 'CLAUDE.md',
13
+ 'README.md',
14
+ 'docs/README.md',
15
+ 'PRD.md',
16
+ ];
17
+
18
+ function findContextFile(startDir) {
19
+ let currDir = path.resolve(startDir);
20
+
21
+ while (true) {
22
+ for (const candidate of candidates) {
23
+ const fullPath = path.join(currDir, candidate);
24
+ if (fs.existsSync(fullPath)) {
25
+ return fullPath;
26
+ }
27
+ }
28
+
29
+ const parentDir = path.dirname(currDir);
30
+ if (parentDir === currDir) {
31
+ return null;
32
+ }
33
+ currDir = parentDir;
34
+ }
35
+ }
36
+
37
+ const contextFile = findContextFile(process.cwd());
38
+
39
+ if (contextFile) {
40
+ console.log(`Found architectural context at: ${contextFile}\n`);
41
+ const content = fs.readFileSync(contextFile, 'utf8');
42
+ if (content.length > 5000) {
43
+ console.log(`${content.substring(0, 5000)}\n...[Content truncated]`);
44
+ } else {
45
+ console.log(content);
46
+ }
47
+ } else {
48
+ console.log('[WARN] No AGENTS.md, architecture doc, CLAUDE.md, PRD, or README found in the repository hierarchy.');
49
+ process.exit(1);
50
+ }
@@ -0,0 +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 };