claude-devkit-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/devkit.js +3 -0
- package/package.json +36 -0
- package/src/cli.js +72 -0
- package/src/commands/check.js +33 -0
- package/src/commands/diff.js +90 -0
- package/src/commands/init.js +232 -0
- package/src/commands/list.js +50 -0
- package/src/commands/remove.js +74 -0
- package/src/commands/upgrade.js +108 -0
- package/src/lib/detector.js +93 -0
- package/src/lib/hasher.js +21 -0
- package/src/lib/installer.js +175 -0
- package/src/lib/logger.js +16 -0
- package/src/lib/manifest.js +79 -0
- package/templates/.claude/CLAUDE.md +74 -0
- package/templates/.claude/commands/challenge.md +210 -0
- package/templates/.claude/commands/commit.md +97 -0
- package/templates/.claude/commands/fix.md +95 -0
- package/templates/.claude/commands/plan.md +141 -0
- package/templates/.claude/commands/review.md +109 -0
- package/templates/.claude/commands/test.md +99 -0
- package/templates/.claude/hooks/comment-guard.js +114 -0
- package/templates/.claude/hooks/file-guard.js +120 -0
- package/templates/.claude/hooks/glob-guard.js +96 -0
- package/templates/.claude/hooks/path-guard.sh +73 -0
- package/templates/.claude/hooks/self-review.sh +29 -0
- package/templates/.claude/hooks/sensitive-guard.sh +214 -0
- package/templates/.claude/settings.json +68 -0
- package/templates/docs/WORKFLOW.md +231 -0
- package/templates/docs/specs/.gitkeep +0 -0
- package/templates/docs/test-plans/.gitkeep +0 -0
- package/templates/scripts/build-test.sh +260 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
EXECUTE, not EXPLORE. Write tests, run them, make them pass. Don't read unrelated files.
|
|
2
|
+
|
|
3
|
+
## Phase 0: Build Context
|
|
4
|
+
|
|
5
|
+
1. **Find what changed:**
|
|
6
|
+
```
|
|
7
|
+
BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||') || BASE="main"
|
|
8
|
+
git diff --name-only "$BASE"...HEAD
|
|
9
|
+
```
|
|
10
|
+
If `$ARGUMENTS` provided → scope to that file or feature only.
|
|
11
|
+
If no changes → "No source changes found. Specify a file or feature."
|
|
12
|
+
|
|
13
|
+
2. **Read the test plan** in `docs/test-plans/` if it exists — this is your roadmap.
|
|
14
|
+
3. **Read the spec** in `docs/specs/` if it exists — understand the INTENT behind the code.
|
|
15
|
+
4. **Read existing tests** for the changed files — find patterns, fixtures, naming conventions. Don't duplicate.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Phase 1: Decide What to Test
|
|
20
|
+
|
|
21
|
+
Test behavior, not implementation. If the internals change but behavior stays the same, tests should still pass.
|
|
22
|
+
|
|
23
|
+
**What NOT to test:**
|
|
24
|
+
- Private/internal methods (test through public API)
|
|
25
|
+
- Framework behavior (test YOUR handler, not that Express routes work)
|
|
26
|
+
- Trivial getters/setters (unless they have validation)
|
|
27
|
+
- Implementation details (HOW it works — test WHAT it does)
|
|
28
|
+
|
|
29
|
+
**Quality check for each test:**
|
|
30
|
+
- Does it test one concept? If it fails, do you know exactly what broke?
|
|
31
|
+
- Is it independent? No test depends on another running first.
|
|
32
|
+
- Is it deterministic? No random, no time-dependent, no external service calls.
|
|
33
|
+
- Does the name describe the scenario? (`returns_error_when_input_is_empty`)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Phase 2: Write Tests
|
|
38
|
+
|
|
39
|
+
Follow the project's existing test patterns. If using `$ARGUMENTS` as a filter, use `--filter` when running:
|
|
40
|
+
```
|
|
41
|
+
bash scripts/build-test.sh --filter "$ARGUMENTS"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Phase 3: Build and Run
|
|
47
|
+
|
|
48
|
+
Compile/typecheck first (tsc --noEmit, cargo check, go vet, swift build, etc.).
|
|
49
|
+
|
|
50
|
+
Then run tests:
|
|
51
|
+
```
|
|
52
|
+
bash scripts/build-test.sh
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If `scripts/build-test.sh` doesn't exist, detect and run directly:
|
|
56
|
+
| Marker | Command |
|
|
57
|
+
|--------|---------|
|
|
58
|
+
| vitest config / vitest in package.json | `npx vitest run` |
|
|
59
|
+
| jest config / jest in package.json | `npx jest --no-cache` |
|
|
60
|
+
| pyproject.toml / pytest.ini | `python3 -m pytest -x` |
|
|
61
|
+
| Cargo.toml | `cargo test` |
|
|
62
|
+
| go.mod | `go test ./...` |
|
|
63
|
+
| build.gradle | `./gradlew test` |
|
|
64
|
+
| *.sln | `dotnet test` |
|
|
65
|
+
| Package.swift | `swift test` |
|
|
66
|
+
| Gemfile | `bundle exec rspec` |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Phase 4: Fix Loop
|
|
71
|
+
|
|
72
|
+
If tests fail:
|
|
73
|
+
1. Read error output. Is the test wrong or the production code wrong?
|
|
74
|
+
2. If production code seems wrong → **ASK the user:** "Test expects X but code does Y. Fix production code or adjust test?"
|
|
75
|
+
3. Fix test code only. Re-run. Max 3 attempts, then stop and report.
|
|
76
|
+
|
|
77
|
+
**NEVER:**
|
|
78
|
+
- Fix production code without asking
|
|
79
|
+
- Delete or weaken existing tests
|
|
80
|
+
- Add `skip`/`xit`/`@disabled` to hide failures
|
|
81
|
+
- Use mocks solely to avoid a real failure
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Phase 5: Summary
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Tests: X added, Y modified, Z unchanged
|
|
89
|
+
Result: All passing ✓
|
|
90
|
+
Coverage: [critical uncovered paths if any]
|
|
91
|
+
Files: [test files touched]
|
|
92
|
+
Plan: [TC-001 ✓, TC-002 ✓, TC-005 new]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If behavior changed: "Consider updating the spec in docs/specs/."
|
|
96
|
+
|
|
97
|
+
## Rules
|
|
98
|
+
1. **Behavior over implementation.** Test what code DOES, not how.
|
|
99
|
+
2. **Independent tests.** Each test sets up its own state, cleans up after.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// comment-guard.js — PreToolUse hook for Claude Code
|
|
3
|
+
//
|
|
4
|
+
// Detects when an Edit would replace real code with placeholder comments like
|
|
5
|
+
// "// ... existing code ..." or "// rest of implementation". This is a
|
|
6
|
+
// common LLM failure mode where the model gets lazy and drops code.
|
|
7
|
+
//
|
|
8
|
+
// Blocking: Yes — exits 2 to reject the edit BEFORE it is applied.
|
|
9
|
+
// Event: PreToolUse on Edit|MultiEdit
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
|
|
15
|
+
// Patterns that indicate lazy placeholder comments (case-insensitive)
|
|
16
|
+
const PLACEHOLDER_PATTERNS = [
|
|
17
|
+
/\/\/\s*\.{2,}\s*(existing|remaining|rest|previous|other|same|original)/i,
|
|
18
|
+
/\/\/\s*\.{2,}\s*(code|implementation|logic|methods|functions|properties)/i,
|
|
19
|
+
/\/\/\s*\[.*(?:remains?|unchanged|omitted|removed|truncated|collapsed).*\]/i,
|
|
20
|
+
/\/\/\s*(?:unchanged|omitted|keep|stays?)\s*(?:as\s*(?:is|before))?/i,
|
|
21
|
+
/\/\*\s*\.{2,}\s*\*\//, // /* ... */
|
|
22
|
+
/#\s*\.{2,}\s*(existing|remaining|rest|previous)/i, // Python: # ... existing
|
|
23
|
+
/\/\/\s*TODO:?\s*implement/i, // // TODO: implement
|
|
24
|
+
/\/\/\s*(?:add|put|insert)\s+.*\s+here/i, // // add code here
|
|
25
|
+
/\/\/\s*<\s*(?:your|actual)\s+/i, // // <your code>
|
|
26
|
+
/pass\s*#\s*(?:TODO|placeholder|implement)/i, // Python: pass # TODO
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function isCommentLine(line) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (trimmed === "") return true; // blank lines are neutral
|
|
32
|
+
if (trimmed.startsWith("//")) return true;
|
|
33
|
+
if (trimmed.startsWith("#") && !trimmed.startsWith("#!")) return true;
|
|
34
|
+
if (trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.endsWith("*/")) return true;
|
|
35
|
+
if (trimmed.startsWith("<!--")) return true;
|
|
36
|
+
if (trimmed === "pass") return true; // Python pass statement as placeholder
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getCodeLineCount(text) {
|
|
41
|
+
if (!text) return 0;
|
|
42
|
+
return text.split("\n").filter((line) => !isCommentLine(line)).length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasPlaceholderPattern(text) {
|
|
46
|
+
return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(text));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function main() {
|
|
50
|
+
let input;
|
|
51
|
+
try {
|
|
52
|
+
input = fs.readFileSync(0, "utf-8").trim();
|
|
53
|
+
} catch {
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!input) process.exit(0);
|
|
58
|
+
|
|
59
|
+
let payload;
|
|
60
|
+
try {
|
|
61
|
+
payload = JSON.parse(input);
|
|
62
|
+
} catch {
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const oldStr = payload.tool_input?.old_string;
|
|
67
|
+
const newStr = payload.tool_input?.new_string;
|
|
68
|
+
|
|
69
|
+
// Only applies to Edit (not Write — Write creates new content)
|
|
70
|
+
if (!oldStr || !newStr) process.exit(0);
|
|
71
|
+
|
|
72
|
+
// If old content was already all comments, this is just editing comments — allow
|
|
73
|
+
const oldCodeLines = getCodeLineCount(oldStr);
|
|
74
|
+
if (oldCodeLines === 0) process.exit(0);
|
|
75
|
+
|
|
76
|
+
// If new content has real code, allow (even if it also has comments)
|
|
77
|
+
const newCodeLines = getCodeLineCount(newStr);
|
|
78
|
+
if (newCodeLines > 0) process.exit(0);
|
|
79
|
+
|
|
80
|
+
// At this point: old had code, new is all comments/blanks
|
|
81
|
+
// Check if the new content contains placeholder patterns
|
|
82
|
+
if (hasPlaceholderPattern(newStr)) {
|
|
83
|
+
process.stderr.write(
|
|
84
|
+
"Blocked: real code was replaced with placeholder comments. " +
|
|
85
|
+
"Preserve the original code and make targeted changes instead.\n"
|
|
86
|
+
);
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// New is all comments but no placeholder pattern — could be intentional
|
|
91
|
+
// (e.g., replacing a code block with documentation comments)
|
|
92
|
+
// Allow but only if the replacement is not drastically shorter
|
|
93
|
+
const oldLines = oldStr.split("\n").length;
|
|
94
|
+
const newLines = newStr.split("\n").length;
|
|
95
|
+
|
|
96
|
+
if (newLines < oldLines * 0.3) {
|
|
97
|
+
// Suspiciously shorter and all comments — likely lazy replacement
|
|
98
|
+
process.stderr.write(
|
|
99
|
+
"Blocked: code block was replaced with a much shorter comment-only block. " +
|
|
100
|
+
"This looks like an accidental truncation. Preserve the original code.\n"
|
|
101
|
+
);
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Seems intentional
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
main();
|
|
111
|
+
} catch {
|
|
112
|
+
// Never crash — allow on error
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// file-guard.js — PostToolUse hook for Claude Code
|
|
3
|
+
//
|
|
4
|
+
// Warns when a Write/Edit operation produces a file exceeding a line threshold.
|
|
5
|
+
// Non-blocking: always exits 0 and injects advisory context.
|
|
6
|
+
//
|
|
7
|
+
// Environment:
|
|
8
|
+
// FILE_GUARD_THRESHOLD — max lines before warning (default: 200)
|
|
9
|
+
// FILE_GUARD_EXCLUDE — comma-separated globs to skip (e.g. "*.generated.swift,*.pb.go")
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
|
|
16
|
+
const THRESHOLD = parseInt(process.env.FILE_GUARD_THRESHOLD, 10) || 200;
|
|
17
|
+
const EXCLUDE = (process.env.FILE_GUARD_EXCLUDE || "")
|
|
18
|
+
.split(",")
|
|
19
|
+
.map((g) => g.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
|
|
22
|
+
function matchesExclude(filePath) {
|
|
23
|
+
const name = path.basename(filePath);
|
|
24
|
+
return EXCLUDE.some((pattern) => {
|
|
25
|
+
// Simple glob: *.ext or exact match
|
|
26
|
+
if (pattern.startsWith("*")) {
|
|
27
|
+
return name.endsWith(pattern.slice(1));
|
|
28
|
+
}
|
|
29
|
+
return name === pattern;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isBinary(buf) {
|
|
34
|
+
// Check first 512 bytes for null bytes (common binary indicator)
|
|
35
|
+
const check = buf.slice(0, 512);
|
|
36
|
+
for (let i = 0; i < check.length; i++) {
|
|
37
|
+
if (check[i] === 0) return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function main() {
|
|
43
|
+
let input;
|
|
44
|
+
try {
|
|
45
|
+
input = fs.readFileSync(0, "utf-8").trim();
|
|
46
|
+
} catch {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!input) process.exit(0);
|
|
51
|
+
|
|
52
|
+
let payload;
|
|
53
|
+
try {
|
|
54
|
+
payload = JSON.parse(input);
|
|
55
|
+
} catch {
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const filePath = payload.tool_input?.file_path;
|
|
60
|
+
if (!filePath) process.exit(0);
|
|
61
|
+
|
|
62
|
+
// Skip excluded patterns
|
|
63
|
+
if (matchesExclude(filePath)) process.exit(0);
|
|
64
|
+
|
|
65
|
+
// Skip if file doesn't exist (deleted?) or is a symlink to outside project
|
|
66
|
+
try {
|
|
67
|
+
const stat = fs.lstatSync(filePath);
|
|
68
|
+
if (stat.isSymbolicLink() || !stat.isFile()) process.exit(0);
|
|
69
|
+
} catch {
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Cap read at 1MB to avoid OOM on huge files
|
|
74
|
+
const MAX_BYTES = 1024 * 1024;
|
|
75
|
+
let content;
|
|
76
|
+
try {
|
|
77
|
+
const stat = fs.statSync(filePath);
|
|
78
|
+
if (stat.size > MAX_BYTES) {
|
|
79
|
+
// >1MB = definitely over threshold, warn without exact count
|
|
80
|
+
const rel = path.relative(process.cwd(), filePath);
|
|
81
|
+
process.stdout.write(JSON.stringify({
|
|
82
|
+
continue: true,
|
|
83
|
+
hookSpecificOutput: {
|
|
84
|
+
hookEventName: "PostToolUse",
|
|
85
|
+
additionalContext: `Warning: ${rel} is ${Math.round(stat.size / 1024)}KB. Consider splitting into smaller modules.`,
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
const buf = fs.readFileSync(filePath);
|
|
91
|
+
if (isBinary(buf)) process.exit(0);
|
|
92
|
+
content = buf.toString("utf-8");
|
|
93
|
+
} catch {
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lineCount = content.split("\n").length;
|
|
98
|
+
if (lineCount <= THRESHOLD) process.exit(0);
|
|
99
|
+
|
|
100
|
+
// Inject non-blocking warning
|
|
101
|
+
const rel = path.relative(process.cwd(), filePath);
|
|
102
|
+
const warning = `Warning: ${rel} has ${lineCount} lines (threshold: ${THRESHOLD}). Consider splitting into smaller, focused modules.`;
|
|
103
|
+
|
|
104
|
+
process.stdout.write(
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
continue: true,
|
|
107
|
+
hookSpecificOutput: {
|
|
108
|
+
hookEventName: "PostToolUse",
|
|
109
|
+
additionalContext: warning,
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
main();
|
|
117
|
+
} catch {
|
|
118
|
+
// Never block on error
|
|
119
|
+
}
|
|
120
|
+
process.exit(0);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// glob-guard.js — PreToolUse hook for Claude Code
|
|
3
|
+
//
|
|
4
|
+
// Blocks overly broad glob patterns (e.g. **/*.ts at project root) that would
|
|
5
|
+
// return thousands of files and fill the context window. Suggests scoped
|
|
6
|
+
// alternatives instead.
|
|
7
|
+
//
|
|
8
|
+
// Blocking: Yes — exits 2 when a broad pattern at a high-level path is detected.
|
|
9
|
+
// Event: PreToolUse on Glob
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
|
|
15
|
+
// Patterns that match too many files when run at project root
|
|
16
|
+
const BROAD_PATTERNS = [
|
|
17
|
+
/^\*\*$/, // **
|
|
18
|
+
/^\*$/, // *
|
|
19
|
+
/^\*\*\/\*$/, // **/*
|
|
20
|
+
/^\*\.\w+$/, // *.ts, *.js
|
|
21
|
+
/^\*\.\{[^}]+\}$/, // *.{ts,js}
|
|
22
|
+
/^\*\*\/\*\.\w+$/, // **/*.ts
|
|
23
|
+
/^\*\*\/\*\.\{[^}]+\}$/, // **/*.{ts,tsx}
|
|
24
|
+
/^\*\*\/\.\*$/, // **/.* (all dotfiles)
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Directories that indicate an intentional, scoped search
|
|
28
|
+
const SCOPED_DIRS = [
|
|
29
|
+
"src", "lib", "app", "apps", "packages", "components", "pages",
|
|
30
|
+
"api", "server", "client", "web", "mobile", "shared", "common",
|
|
31
|
+
"utils", "helpers", "services", "hooks", "store", "routes",
|
|
32
|
+
"models", "controllers", "views", "tests", "__tests__", "spec",
|
|
33
|
+
"Sources", "Tests", "cmd", "pkg", "internal",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function isBroadPattern(pattern) {
|
|
37
|
+
if (!pattern) return false;
|
|
38
|
+
return BROAD_PATTERNS.some((re) => re.test(pattern.trim()));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function startsWithScopedDir(pattern) {
|
|
42
|
+
if (!pattern) return false;
|
|
43
|
+
// Only allow dirs explicitly in SCOPED_DIRS — not arbitrary dirs like node_modules/
|
|
44
|
+
return SCOPED_DIRS.some(
|
|
45
|
+
(d) => pattern.startsWith(d + "/") || pattern.startsWith("./" + d + "/")
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isRootLevel(basePath) {
|
|
50
|
+
if (!basePath) return true;
|
|
51
|
+
const norm = basePath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
52
|
+
if (!norm || norm === ".") return true;
|
|
53
|
+
const segments = norm.split("/").filter((s) => s && s !== ".");
|
|
54
|
+
if (segments.length === 0) return true;
|
|
55
|
+
if (segments.length === 1 && !SCOPED_DIRS.includes(segments[0])) return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function suggest(pattern) {
|
|
60
|
+
let ext = "";
|
|
61
|
+
const m = pattern.match(/\*\.(\{[^}]+\}|\w+)$/);
|
|
62
|
+
if (m) ext = m[1];
|
|
63
|
+
const dirs = ["src", "lib", "app", "tests"];
|
|
64
|
+
return ext
|
|
65
|
+
? dirs.map((d) => `${d}/**/*.${ext}`).slice(0, 3)
|
|
66
|
+
: dirs.map((d) => `${d}/**/*`).slice(0, 3);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function main() {
|
|
70
|
+
let input;
|
|
71
|
+
try { input = fs.readFileSync(0, "utf-8").trim(); } catch { process.exit(0); }
|
|
72
|
+
if (!input) process.exit(0);
|
|
73
|
+
|
|
74
|
+
let payload;
|
|
75
|
+
try { payload = JSON.parse(input); } catch { process.exit(0); }
|
|
76
|
+
|
|
77
|
+
const pattern = payload.tool_input?.pattern;
|
|
78
|
+
const basePath = payload.tool_input?.path;
|
|
79
|
+
|
|
80
|
+
if (!pattern) process.exit(0);
|
|
81
|
+
if (startsWithScopedDir(pattern)) process.exit(0);
|
|
82
|
+
if (!isBroadPattern(pattern)) process.exit(0);
|
|
83
|
+
if (!isRootLevel(basePath)) process.exit(0);
|
|
84
|
+
|
|
85
|
+
const alt = suggest(pattern);
|
|
86
|
+
process.stderr.write(
|
|
87
|
+
[
|
|
88
|
+
`Blocked: '${pattern}' is too broad for ${basePath || "project root"} — would fill the context window.`,
|
|
89
|
+
"Use a scoped pattern instead:",
|
|
90
|
+
...alt.map((s) => ` - ${s}`),
|
|
91
|
+
].join("\n") + "\n"
|
|
92
|
+
);
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try { main(); } catch { process.exit(0); }
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# path-guard.sh — PreToolUse hook for Claude Code
|
|
3
|
+
#
|
|
4
|
+
# Blocks Bash commands that target directories known to be large and wasteful
|
|
5
|
+
# to explore (node_modules, build artifacts, .git internals, etc.).
|
|
6
|
+
#
|
|
7
|
+
# Exit codes:
|
|
8
|
+
# 0 — command allowed
|
|
9
|
+
# 2 — command blocked (policy)
|
|
10
|
+
#
|
|
11
|
+
# Environment:
|
|
12
|
+
# PATH_GUARD_EXTRA — additional pipe-separated patterns to block
|
|
13
|
+
# e.g. "\.terraform|\.vagrant"
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
# ─── Read hook payload from stdin ───────────────────────────────────
|
|
18
|
+
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
[[ -z "$INPUT" ]] && exit 0
|
|
21
|
+
|
|
22
|
+
# Check Node.js availability
|
|
23
|
+
if ! command -v node &>/dev/null; then
|
|
24
|
+
echo "WARNING: path-guard disabled — Node.js not found." >&2
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Parse JSON with inline Node.js (avoids jq dependency)
|
|
29
|
+
COMMAND=$(printf '%s' "$INPUT" | node -e "
|
|
30
|
+
try {
|
|
31
|
+
const d = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
|
32
|
+
const cmd = d.tool_input?.command;
|
|
33
|
+
if (typeof cmd === 'string') process.stdout.write(cmd);
|
|
34
|
+
else process.exit(0);
|
|
35
|
+
} catch { process.exit(0); }
|
|
36
|
+
" 2>/dev/null) || exit 0
|
|
37
|
+
|
|
38
|
+
[[ -z "$COMMAND" ]] && exit 0
|
|
39
|
+
|
|
40
|
+
# ─── Blocked directory patterns ─────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
BLOCKED="node_modules"
|
|
43
|
+
BLOCKED+="|__pycache__"
|
|
44
|
+
BLOCKED+="|\.git/objects"
|
|
45
|
+
BLOCKED+="|\.git/refs"
|
|
46
|
+
BLOCKED+="|dist/"
|
|
47
|
+
BLOCKED+="|build/"
|
|
48
|
+
BLOCKED+="|\.next/"
|
|
49
|
+
BLOCKED+="|vendor/"
|
|
50
|
+
BLOCKED+="|Pods/"
|
|
51
|
+
BLOCKED+="|\.build/"
|
|
52
|
+
BLOCKED+="|DerivedData"
|
|
53
|
+
BLOCKED+="|\.gradle/"
|
|
54
|
+
BLOCKED+="|target/debug"
|
|
55
|
+
BLOCKED+="|target/release"
|
|
56
|
+
BLOCKED+="|\.nuget"
|
|
57
|
+
BLOCKED+="|\.cache"
|
|
58
|
+
|
|
59
|
+
# Append project-specific patterns from env
|
|
60
|
+
if [[ -n "${PATH_GUARD_EXTRA:-}" ]]; then
|
|
61
|
+
BLOCKED+="|$PATH_GUARD_EXTRA"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# ─── Match and block ────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
if printf '%s\n' "$COMMAND" | grep -qE "$BLOCKED"; then
|
|
67
|
+
# Extract which pattern matched for a useful error message
|
|
68
|
+
MATCHED=$(printf '%s\n' "$COMMAND" | grep -oE "$BLOCKED" | head -1)
|
|
69
|
+
echo "Blocked: command references '$MATCHED' — this directory is typically large and exploring it wastes tokens. Use Glob or Grep tools instead." >&2
|
|
70
|
+
exit 2
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
exit 0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# self-review.sh — Stop hook for Claude Code
|
|
3
|
+
#
|
|
4
|
+
# Injects a self-review checklist when Claude is about to finish.
|
|
5
|
+
# Non-blocking: always exits 0, just adds context for Claude to consider.
|
|
6
|
+
#
|
|
7
|
+
# Environment:
|
|
8
|
+
# SELF_REVIEW_ENABLED — set to "false" to disable (default: true)
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
# Check if disabled
|
|
13
|
+
if [[ "${SELF_REVIEW_ENABLED:-true}" == "false" ]]; then
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Read stdin (Stop event payload — may be empty or minimal)
|
|
18
|
+
cat > /dev/null 2>&1 || true
|
|
19
|
+
|
|
20
|
+
# Inject self-review checklist as context
|
|
21
|
+
cat <<'REVIEW_JSON'
|
|
22
|
+
{
|
|
23
|
+
"continue": true,
|
|
24
|
+
"hookSpecificOutput": {
|
|
25
|
+
"hookEventName": "Stop",
|
|
26
|
+
"additionalContext": "Self-review before finishing:\n1. Did you leave any TODO/FIXME comments that should be resolved now?\n2. Did you create mock or fake implementations just to pass tests?\n3. Did you replace real code with placeholder comments like '// ... existing code'?\n4. Do all changed files compile and typecheck cleanly?\n5. Did you run the full test suite, not just the new tests?\n6. Are there any files you modified but forgot to include in the summary?"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
REVIEW_JSON
|