@hasna/hooks 0.0.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/.npmrc.example +2 -0
- package/AGENTS.md +54 -0
- package/CLAUDE.md +70 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +232 -0
- package/bin/index.js +5171 -0
- package/hooks/hook-agentmessages/CLAUDE.md +79 -0
- package/hooks/hook-agentmessages/LICENSE +21 -0
- package/hooks/hook-agentmessages/README.md +107 -0
- package/hooks/hook-agentmessages/package.json +31 -0
- package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
- package/hooks/hook-agentmessages/src/install.ts +126 -0
- package/hooks/hook-agentmessages/src/session-start.ts +255 -0
- package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
- package/hooks/hook-branchprotect/CLAUDE.md +23 -0
- package/hooks/hook-branchprotect/README.md +25 -0
- package/hooks/hook-branchprotect/package.json +42 -0
- package/hooks/hook-branchprotect/src/cli.ts +126 -0
- package/hooks/hook-branchprotect/src/hook.ts +88 -0
- package/hooks/hook-branchprotect/tsconfig.json +25 -0
- package/hooks/hook-checkbugs/LICENSE +21 -0
- package/hooks/hook-checkbugs/README.md +140 -0
- package/hooks/hook-checkbugs/package.json +58 -0
- package/hooks/hook-checkbugs/src/cli.ts +628 -0
- package/hooks/hook-checkbugs/src/hook.ts +335 -0
- package/hooks/hook-checkbugs/tsconfig.json +15 -0
- package/hooks/hook-checkdocs/README.md +137 -0
- package/hooks/hook-checkdocs/package.json +57 -0
- package/hooks/hook-checkdocs/src/cli.ts +628 -0
- package/hooks/hook-checkdocs/src/hook.ts +310 -0
- package/hooks/hook-checkdocs/tsconfig.json +15 -0
- package/hooks/hook-checkfiles/LICENSE +21 -0
- package/hooks/hook-checkfiles/README.md +141 -0
- package/hooks/hook-checkfiles/package.json +56 -0
- package/hooks/hook-checkfiles/src/cli.ts +545 -0
- package/hooks/hook-checkfiles/src/hook.ts +321 -0
- package/hooks/hook-checkfiles/tsconfig.json +15 -0
- package/hooks/hook-checklint/LICENSE +21 -0
- package/hooks/hook-checklint/README.md +147 -0
- package/hooks/hook-checklint/package.json +57 -0
- package/hooks/hook-checklint/src/cli-patch.ts +32 -0
- package/hooks/hook-checklint/src/cli.ts +667 -0
- package/hooks/hook-checklint/src/hook.ts +473 -0
- package/hooks/hook-checklint/tsconfig.json +15 -0
- package/hooks/hook-checkpoint/CLAUDE.md +23 -0
- package/hooks/hook-checkpoint/README.md +37 -0
- package/hooks/hook-checkpoint/package.json +58 -0
- package/hooks/hook-checkpoint/src/cli.ts +191 -0
- package/hooks/hook-checkpoint/src/hook.ts +207 -0
- package/hooks/hook-checkpoint/tsconfig.json +25 -0
- package/hooks/hook-checksecurity/LICENSE +21 -0
- package/hooks/hook-checksecurity/README.md +158 -0
- package/hooks/hook-checksecurity/package.json +57 -0
- package/hooks/hook-checksecurity/src/cli.ts +601 -0
- package/hooks/hook-checksecurity/src/hook.ts +334 -0
- package/hooks/hook-checksecurity/tsconfig.json +15 -0
- package/hooks/hook-checktasks/README.md +144 -0
- package/hooks/hook-checktasks/package.json +55 -0
- package/hooks/hook-checktasks/src/cli.ts +578 -0
- package/hooks/hook-checktasks/src/hook.ts +308 -0
- package/hooks/hook-checktasks/tsconfig.json +20 -0
- package/hooks/hook-checktests/LICENSE +21 -0
- package/hooks/hook-checktests/README.md +137 -0
- package/hooks/hook-checktests/package.json +57 -0
- package/hooks/hook-checktests/src/cli.ts +627 -0
- package/hooks/hook-checktests/src/hook.ts +334 -0
- package/hooks/hook-checktests/tsconfig.json +15 -0
- package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
- package/hooks/hook-contextrefresh/README.md +42 -0
- package/hooks/hook-contextrefresh/package.json +42 -0
- package/hooks/hook-contextrefresh/src/cli.ts +152 -0
- package/hooks/hook-contextrefresh/src/hook.ts +148 -0
- package/hooks/hook-contextrefresh/tsconfig.json +25 -0
- package/hooks/hook-gitguard/CLAUDE.md +22 -0
- package/hooks/hook-gitguard/README.md +30 -0
- package/hooks/hook-gitguard/package.json +57 -0
- package/hooks/hook-gitguard/src/cli.ts +159 -0
- package/hooks/hook-gitguard/src/hook.ts +129 -0
- package/hooks/hook-gitguard/tsconfig.json +25 -0
- package/hooks/hook-packageage/CLAUDE.md +23 -0
- package/hooks/hook-packageage/README.md +33 -0
- package/hooks/hook-packageage/package.json +42 -0
- package/hooks/hook-packageage/src/cli.ts +165 -0
- package/hooks/hook-packageage/src/hook.ts +177 -0
- package/hooks/hook-packageage/tsconfig.json +25 -0
- package/hooks/hook-phonenotify/CLAUDE.md +25 -0
- package/hooks/hook-phonenotify/README.md +44 -0
- package/hooks/hook-phonenotify/package.json +42 -0
- package/hooks/hook-phonenotify/src/cli.ts +196 -0
- package/hooks/hook-phonenotify/src/hook.ts +139 -0
- package/hooks/hook-phonenotify/tsconfig.json +25 -0
- package/hooks/hook-precompact/CLAUDE.md +23 -0
- package/hooks/hook-precompact/README.md +36 -0
- package/hooks/hook-precompact/package.json +42 -0
- package/hooks/hook-precompact/src/cli.ts +168 -0
- package/hooks/hook-precompact/src/hook.ts +122 -0
- package/hooks/hook-precompact/tsconfig.json +25 -0
- package/package.json +61 -0
- package/src/cli/components/App.tsx +191 -0
- package/src/cli/components/CategorySelect.tsx +37 -0
- package/src/cli/components/DataTable.tsx +133 -0
- package/src/cli/components/Header.tsx +18 -0
- package/src/cli/components/HookSelect.tsx +29 -0
- package/src/cli/components/InstallProgress.tsx +105 -0
- package/src/cli/components/SearchView.tsx +86 -0
- package/src/cli/index.tsx +218 -0
- package/src/index.ts +31 -0
- package/src/lib/installer.ts +288 -0
- package/src/lib/registry.ts +205 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Hook: check-files
|
|
5
|
+
*
|
|
6
|
+
* Runs a headless Claude Code agent to review files and create tasks.
|
|
7
|
+
* Uses service-implementation CLI to dispatch tasks.
|
|
8
|
+
*
|
|
9
|
+
* This hook runs ASYNC (non-blocking) on PostToolUse.
|
|
10
|
+
*
|
|
11
|
+
* Configuration:
|
|
12
|
+
* - taskListId: task list for dispatching review tasks
|
|
13
|
+
* - editThreshold: run review after this many edits (default: 3, range: 3-7)
|
|
14
|
+
* - keywords: keywords that trigger the check (default: ["dev"])
|
|
15
|
+
* - reviewPrompt: custom prompt for the headless agent
|
|
16
|
+
* - enabled: enable/disable the hook
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
import { homedir } from "os";
|
|
22
|
+
import { spawn } from "child_process";
|
|
23
|
+
|
|
24
|
+
interface CheckFilesConfig {
|
|
25
|
+
taskListId?: string;
|
|
26
|
+
editThreshold?: number;
|
|
27
|
+
keywords?: string[];
|
|
28
|
+
reviewPrompt?: string;
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface HookInput {
|
|
33
|
+
session_id: string;
|
|
34
|
+
transcript_path: string;
|
|
35
|
+
cwd: string;
|
|
36
|
+
tool_name: string;
|
|
37
|
+
tool_input: Record<string, unknown>;
|
|
38
|
+
tool_output?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SessionState {
|
|
42
|
+
editCount: number;
|
|
43
|
+
editedFiles: string[];
|
|
44
|
+
lastReviewRun: number;
|
|
45
|
+
reviewInProgress: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CONFIG_KEY = "checkFilesConfig";
|
|
49
|
+
const STATE_DIR = join(homedir(), ".claude", "hook-state");
|
|
50
|
+
const EDIT_TOOLS = ["Edit", "Write", "NotebookEdit"];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sanitize ID to prevent path traversal and injection attacks
|
|
54
|
+
*/
|
|
55
|
+
function sanitizeId(id: string): string {
|
|
56
|
+
if (!id || typeof id !== 'string') return 'default';
|
|
57
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 100) || 'default';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sanitize file path for safe display in prompts
|
|
62
|
+
*/
|
|
63
|
+
function sanitizePath(path: string): string {
|
|
64
|
+
if (!path || typeof path !== 'string') return '';
|
|
65
|
+
return path.replace(/[`$"'\\;&|<>(){}[\]!#*?~]/g, '_');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const DEFAULT_REVIEW_PROMPT = `You are a code reviewer. Review the following files that were recently edited and identify any issues:
|
|
69
|
+
|
|
70
|
+
FILES TO REVIEW:
|
|
71
|
+
{files}
|
|
72
|
+
|
|
73
|
+
For each issue found, create a task using the service-implementation CLI:
|
|
74
|
+
service-implementation task dispatch "{taskListId}" -s "REVIEW: [brief issue description]" -d "[detailed description with file:line reference]"
|
|
75
|
+
|
|
76
|
+
Focus on:
|
|
77
|
+
- Potential bugs or logic errors
|
|
78
|
+
- Security vulnerabilities
|
|
79
|
+
- Performance issues
|
|
80
|
+
- Code style violations
|
|
81
|
+
- Missing error handling
|
|
82
|
+
|
|
83
|
+
If no issues are found, do not create any tasks.
|
|
84
|
+
Only create tasks for real issues, not minor style preferences.
|
|
85
|
+
Limit to max 5 most important issues.`;
|
|
86
|
+
|
|
87
|
+
function readStdinJson(): HookInput | null {
|
|
88
|
+
try {
|
|
89
|
+
const stdin = readFileSync(0, "utf-8");
|
|
90
|
+
return JSON.parse(stdin);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readSettings(path: string): Record<string, unknown> {
|
|
97
|
+
if (!existsSync(path)) return {};
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
100
|
+
} catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getConfig(cwd: string): CheckFilesConfig {
|
|
106
|
+
// Try project settings first
|
|
107
|
+
const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
|
|
108
|
+
if (projectSettings[CONFIG_KEY]) {
|
|
109
|
+
return projectSettings[CONFIG_KEY] as CheckFilesConfig;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Fall back to global settings
|
|
113
|
+
const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
|
|
114
|
+
if (globalSettings[CONFIG_KEY]) {
|
|
115
|
+
return globalSettings[CONFIG_KEY] as CheckFilesConfig;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Default config
|
|
119
|
+
return {
|
|
120
|
+
editThreshold: 3,
|
|
121
|
+
keywords: ["dev"],
|
|
122
|
+
enabled: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getStateFile(sessionId: string): string {
|
|
127
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
128
|
+
const safeSessionId = sanitizeId(sessionId);
|
|
129
|
+
return join(STATE_DIR, `checkfiles-${safeSessionId}.json`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getSessionState(sessionId: string): SessionState {
|
|
133
|
+
const stateFile = getStateFile(sessionId);
|
|
134
|
+
if (existsSync(stateFile)) {
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
137
|
+
} catch {
|
|
138
|
+
// Corrupted state, reset
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { editCount: 0, editedFiles: [], lastReviewRun: 0, reviewInProgress: false };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function saveSessionState(sessionId: string, state: SessionState): void {
|
|
145
|
+
const stateFile = getStateFile(sessionId);
|
|
146
|
+
writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getSessionName(transcriptPath: string): string | null {
|
|
150
|
+
if (!existsSync(transcriptPath)) return null;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const content = readFileSync(transcriptPath, "utf-8");
|
|
154
|
+
let lastTitle: string | null = null;
|
|
155
|
+
let searchStart = 0;
|
|
156
|
+
|
|
157
|
+
while (true) {
|
|
158
|
+
const titleIndex = content.indexOf('"custom-title"', searchStart);
|
|
159
|
+
if (titleIndex === -1) break;
|
|
160
|
+
|
|
161
|
+
const lineStart = content.lastIndexOf("\n", titleIndex) + 1;
|
|
162
|
+
const lineEnd = content.indexOf("\n", titleIndex);
|
|
163
|
+
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const entry = JSON.parse(line);
|
|
167
|
+
if (entry.type === "custom-title" && entry.customTitle) {
|
|
168
|
+
lastTitle = entry.customTitle;
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Skip malformed lines
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
searchStart = titleIndex + 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return lastTitle;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getProjectTaskListId(cwd: string): string | null {
|
|
184
|
+
const dirName = cwd.split("/").filter(Boolean).pop() || "";
|
|
185
|
+
// Default to project-bugfixes or project-dev
|
|
186
|
+
return `${dirName}-bugfixes`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function runHeadlessReview(
|
|
190
|
+
cwd: string,
|
|
191
|
+
files: string[],
|
|
192
|
+
taskListId: string,
|
|
193
|
+
customPrompt?: string
|
|
194
|
+
): void {
|
|
195
|
+
// Sanitize file paths to prevent prompt injection
|
|
196
|
+
const filesFormatted = files.map((f) => `- ${sanitizePath(f)}`).join("\n");
|
|
197
|
+
// Sanitize taskListId
|
|
198
|
+
const safeTaskListId = sanitizeId(taskListId);
|
|
199
|
+
|
|
200
|
+
const prompt = (customPrompt || DEFAULT_REVIEW_PROMPT)
|
|
201
|
+
.replace("{files}", filesFormatted)
|
|
202
|
+
.replace("{taskListId}", safeTaskListId);
|
|
203
|
+
|
|
204
|
+
// Spawn headless Claude Code agent in background
|
|
205
|
+
const child = spawn(
|
|
206
|
+
"claude",
|
|
207
|
+
[
|
|
208
|
+
"-p",
|
|
209
|
+
prompt,
|
|
210
|
+
"--permission-mode",
|
|
211
|
+
"acceptEdits",
|
|
212
|
+
"--allowedTools",
|
|
213
|
+
"Bash,Read",
|
|
214
|
+
"--no-session-persistence",
|
|
215
|
+
],
|
|
216
|
+
{
|
|
217
|
+
cwd,
|
|
218
|
+
detached: true,
|
|
219
|
+
stdio: "ignore",
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Detach from parent process
|
|
224
|
+
child.unref();
|
|
225
|
+
|
|
226
|
+
console.error(`[hook-checkfiles] Started headless review of ${files.length} files`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function approve() {
|
|
230
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function run() {
|
|
235
|
+
const hookInput = readStdinJson();
|
|
236
|
+
if (!hookInput) {
|
|
237
|
+
approve();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const { session_id, cwd, tool_name, tool_input, transcript_path } = hookInput;
|
|
242
|
+
|
|
243
|
+
// Only process edit tools
|
|
244
|
+
if (!EDIT_TOOLS.includes(tool_name)) {
|
|
245
|
+
approve();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const config = getConfig(cwd);
|
|
250
|
+
|
|
251
|
+
// Check if hook is disabled
|
|
252
|
+
if (config.enabled === false) {
|
|
253
|
+
approve();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check keywords match
|
|
258
|
+
const sessionName = transcript_path ? getSessionName(transcript_path) : null;
|
|
259
|
+
const nameToCheck = sessionName || config.taskListId || "";
|
|
260
|
+
const keywords = config.keywords || ["dev"];
|
|
261
|
+
|
|
262
|
+
const matchesKeyword = keywords.some((keyword) =>
|
|
263
|
+
nameToCheck.toLowerCase().includes(keyword.toLowerCase())
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// If keywords are configured and we have a session name, check for match
|
|
267
|
+
if (keywords.length > 0 && nameToCheck && !matchesKeyword) {
|
|
268
|
+
approve();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Get edited file path
|
|
273
|
+
const filePath = (tool_input.file_path || tool_input.notebook_path) as string | undefined;
|
|
274
|
+
if (!filePath) {
|
|
275
|
+
approve();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Update session state
|
|
280
|
+
const state = getSessionState(session_id);
|
|
281
|
+
|
|
282
|
+
// Skip if review already in progress
|
|
283
|
+
if (state.reviewInProgress) {
|
|
284
|
+
approve();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
state.editCount++;
|
|
289
|
+
|
|
290
|
+
if (!state.editedFiles.includes(filePath)) {
|
|
291
|
+
state.editedFiles.push(filePath);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const threshold = Math.min(7, Math.max(3, config.editThreshold || 3));
|
|
295
|
+
|
|
296
|
+
// Check if we should run review
|
|
297
|
+
if (state.editCount >= threshold) {
|
|
298
|
+
const taskListId = config.taskListId || getProjectTaskListId(cwd) || "default-bugfixes";
|
|
299
|
+
|
|
300
|
+
// Mark review in progress
|
|
301
|
+
state.reviewInProgress = true;
|
|
302
|
+
saveSessionState(session_id, state);
|
|
303
|
+
|
|
304
|
+
// Run headless review (async, non-blocking)
|
|
305
|
+
runHeadlessReview(cwd, state.editedFiles, taskListId, config.reviewPrompt);
|
|
306
|
+
|
|
307
|
+
// Reset counter after starting review
|
|
308
|
+
state.editCount = 0;
|
|
309
|
+
state.editedFiles = [];
|
|
310
|
+
state.lastReviewRun = Date.now();
|
|
311
|
+
state.reviewInProgress = false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
saveSessionState(session_id, state);
|
|
315
|
+
approve();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Allow direct execution
|
|
319
|
+
if (import.meta.main) {
|
|
320
|
+
run();
|
|
321
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hasna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# @hasnaxyz/hook-checklint
|
|
2
|
+
|
|
3
|
+
Claude Code hook that runs linting after file edits and creates tasks for errors.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Automatic lint checking**: Runs lint after every N file edits (configurable, default: 3)
|
|
8
|
+
- **Task creation**: Creates bug tasks for lint errors so AI can fix them
|
|
9
|
+
- **Auto-detection**: Detects lint command and task lists automatically
|
|
10
|
+
- **Session-aware**: Only runs for sessions matching configured keywords
|
|
11
|
+
- **Multiple linters**: Supports ESLint, Biome, and custom lint commands
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Global CLI
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add -g @hasnaxyz/hook-checklint
|
|
19
|
+
hook-checklint install --global
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Project-specific
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd /path/to/your/project
|
|
26
|
+
bunx @hasnaxyz/hook-checklint install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Once installed, the hook runs automatically after file edits (Edit, Write, NotebookEdit tools).
|
|
32
|
+
|
|
33
|
+
### Commands
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
hook-checklint install [path] # Install the hook
|
|
37
|
+
hook-checklint config [path] # Update configuration
|
|
38
|
+
hook-checklint uninstall [path] # Remove the hook
|
|
39
|
+
hook-checklint status # Show hook status
|
|
40
|
+
hook-checklint run # Execute hook (called by Claude Code)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Options
|
|
44
|
+
|
|
45
|
+
- `--global`, `-g`: Apply to global settings (`~/.claude/settings.json`)
|
|
46
|
+
- `/path/to/repo`: Apply to specific project path
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
Configuration is stored in `.claude/settings.json`:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"hooks": {
|
|
55
|
+
"PostToolUse": [{
|
|
56
|
+
"matcher": {
|
|
57
|
+
"tool_name": "^(Edit|Write|NotebookEdit)$"
|
|
58
|
+
},
|
|
59
|
+
"hooks": [{
|
|
60
|
+
"type": "command",
|
|
61
|
+
"command": "bunx @hasnaxyz/hook-checklint@latest run",
|
|
62
|
+
"timeout": 120
|
|
63
|
+
}]
|
|
64
|
+
}]
|
|
65
|
+
},
|
|
66
|
+
"checkLintConfig": {
|
|
67
|
+
"editThreshold": 3,
|
|
68
|
+
"lintCommand": "bun lint",
|
|
69
|
+
"taskListId": "myproject-bugfixes",
|
|
70
|
+
"keywords": ["dev"],
|
|
71
|
+
"createTasks": true,
|
|
72
|
+
"enabled": true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Options
|
|
78
|
+
|
|
79
|
+
| Option | Type | Default | Description |
|
|
80
|
+
|--------|------|---------|-------------|
|
|
81
|
+
| `editThreshold` | number | 3 | Run lint after this many edits (3-7) |
|
|
82
|
+
| `lintCommand` | string | auto | Lint command to run |
|
|
83
|
+
| `taskListId` | string | auto | Task list for error tasks |
|
|
84
|
+
| `keywords` | string[] | ["dev"] | Only run for matching sessions |
|
|
85
|
+
| `createTasks` | boolean | true | Create tasks for lint errors |
|
|
86
|
+
| `enabled` | boolean | true | Enable/disable the hook |
|
|
87
|
+
|
|
88
|
+
## How It Works
|
|
89
|
+
|
|
90
|
+
1. **Tracks file edits**: Monitors Edit, Write, and NotebookEdit tool calls
|
|
91
|
+
2. **Counts edits**: Maintains a per-session edit counter
|
|
92
|
+
3. **Triggers lint**: After N edits, runs the configured lint command
|
|
93
|
+
4. **Parses output**: Extracts errors from ESLint/Biome output
|
|
94
|
+
5. **Creates tasks**: For each error, creates a bug task in the configured task list
|
|
95
|
+
6. **Resets counter**: After lint run, resets the edit counter
|
|
96
|
+
|
|
97
|
+
## Auto-Detection
|
|
98
|
+
|
|
99
|
+
### Lint Command
|
|
100
|
+
|
|
101
|
+
The hook auto-detects the lint command by checking:
|
|
102
|
+
|
|
103
|
+
1. `package.json` scripts: `lint`, `lint:check`, `eslint`, `biome`
|
|
104
|
+
2. Config files: `biome.json`, `.eslintrc.json`, `eslint.config.js`
|
|
105
|
+
|
|
106
|
+
### Task List
|
|
107
|
+
|
|
108
|
+
The hook auto-detects the task list by:
|
|
109
|
+
|
|
110
|
+
1. Looking for `{project}-bugfixes` list
|
|
111
|
+
2. Falling back to `{project}-dev` list
|
|
112
|
+
|
|
113
|
+
## Task Format
|
|
114
|
+
|
|
115
|
+
Created tasks follow this format:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Subject: BUG: MEDIUM - Fix lint error in src/file.ts:42
|
|
119
|
+
Description:
|
|
120
|
+
Fix lint error at src/file.ts:42:5
|
|
121
|
+
|
|
122
|
+
**Error:** Unexpected console statement
|
|
123
|
+
**Rule:** no-console
|
|
124
|
+
**File:** src/file.ts
|
|
125
|
+
**Line:** 42
|
|
126
|
+
**Column:** 5
|
|
127
|
+
|
|
128
|
+
**Acceptance criteria:**
|
|
129
|
+
- Lint error is fixed
|
|
130
|
+
- No new lint errors introduced
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Session State
|
|
134
|
+
|
|
135
|
+
The hook maintains session state in `~/.claude/hook-state/checklint-{session_id}.json`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"editCount": 2,
|
|
140
|
+
"editedFiles": ["src/file.ts", "src/other.ts"],
|
|
141
|
+
"lastLintRun": 1706500000000
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasnaxyz/hook-checklint",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "Claude Code hook that runs linting after file edits and creates tasks for errors",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hook-checklint": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/hook.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/hook.js",
|
|
13
|
+
"types": "./dist/hook.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./cli": {
|
|
16
|
+
"import": "./dist/cli.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "bun build ./src/cli.ts ./src/hook.ts --outdir ./dist --target node",
|
|
25
|
+
"prepublishOnly": "bun run build",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"claude-code",
|
|
30
|
+
"claude",
|
|
31
|
+
"hook",
|
|
32
|
+
"lint",
|
|
33
|
+
"eslint",
|
|
34
|
+
"biome",
|
|
35
|
+
"automation",
|
|
36
|
+
"cli"
|
|
37
|
+
],
|
|
38
|
+
"author": "Hasna",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/hasnaxyz/hook-checklint.git"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "restricted",
|
|
46
|
+
"registry": "https://registry.npmjs.org/"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18",
|
|
50
|
+
"bun": ">=1.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "^1.3.8",
|
|
54
|
+
"@types/node": "^20",
|
|
55
|
+
"typescript": "^5.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface InstallOptions {
|
|
2
|
+
global?: boolean;
|
|
3
|
+
taskListId?: string;
|
|
4
|
+
keywords?: string[];
|
|
5
|
+
editThreshold?: number;
|
|
6
|
+
yes?: boolean;
|
|
7
|
+
path?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseInstallArgs(args: string[]): InstallOptions {
|
|
11
|
+
const options: InstallOptions = {};
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const arg = args[i];
|
|
15
|
+
|
|
16
|
+
if (arg === "--global" || arg === "-g") {
|
|
17
|
+
options.global = true;
|
|
18
|
+
} else if (arg === "--yes" || arg === "-y") {
|
|
19
|
+
options.yes = true;
|
|
20
|
+
} else if (arg === "--task-list-id" || arg === "-t") {
|
|
21
|
+
options.taskListId = args[++i];
|
|
22
|
+
} else if (arg === "--keywords" || arg === "-k") {
|
|
23
|
+
options.keywords = args[++i]?.split(",").map(k => k.trim().toLowerCase()).filter(Boolean);
|
|
24
|
+
} else if (arg === "--threshold" || arg === "-n") {
|
|
25
|
+
options.editThreshold = parseInt(args[++i], 10);
|
|
26
|
+
} else if (!arg.startsWith("-")) {
|
|
27
|
+
options.path = arg;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return options;
|
|
32
|
+
}
|