@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,334 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Hook: check-security
|
|
5
|
+
*
|
|
6
|
+
* Runs security checks via Claude and Codex headless agents.
|
|
7
|
+
* This is a BLOCKER hook on the Stop event.
|
|
8
|
+
*
|
|
9
|
+
* Only runs for repos matching [prefix]-[name] pattern.
|
|
10
|
+
* Only runs once per session (state flag prevents re-runs).
|
|
11
|
+
*
|
|
12
|
+
* Configuration:
|
|
13
|
+
* - taskListId: task list for dispatching security tasks
|
|
14
|
+
* - keywords: keywords that trigger the check (default: ["dev"])
|
|
15
|
+
* - enabled: enable/disable the hook
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
import { spawnSync, execSync } from "child_process";
|
|
22
|
+
|
|
23
|
+
interface CheckSecurityConfig {
|
|
24
|
+
taskListId?: string;
|
|
25
|
+
keywords?: string[];
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface HookInput {
|
|
30
|
+
session_id: string;
|
|
31
|
+
transcript_path: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SessionState {
|
|
36
|
+
securityChecked: boolean;
|
|
37
|
+
lastCheckRun: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CONFIG_KEY = "checkSecurityConfig";
|
|
41
|
+
const STATE_DIR = join(homedir(), ".claude", "hook-state");
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sanitize ID to prevent path traversal and injection attacks
|
|
45
|
+
*/
|
|
46
|
+
function sanitizeId(id: string): string {
|
|
47
|
+
if (!id || typeof id !== 'string') return 'default';
|
|
48
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 100) || 'default';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const SECURITY_PROMPT = `You are a security reviewer. Analyze the codebase in the current directory for security vulnerabilities.
|
|
52
|
+
|
|
53
|
+
For each security issue found, create a task using the service-implementation CLI:
|
|
54
|
+
service-implementation task dispatch "{taskListId}" -s "SECURITY: [severity] - [brief description]" -d "[detailed description with file:line reference and remediation advice]"
|
|
55
|
+
|
|
56
|
+
Severity levels: CRITICAL, HIGH, MEDIUM, LOW
|
|
57
|
+
|
|
58
|
+
Focus on:
|
|
59
|
+
- Injection vulnerabilities (SQL, XSS, command injection)
|
|
60
|
+
- Authentication/authorization issues
|
|
61
|
+
- Sensitive data exposure
|
|
62
|
+
- Insecure configurations
|
|
63
|
+
- Dependency vulnerabilities
|
|
64
|
+
- Hardcoded secrets or credentials
|
|
65
|
+
- Input validation issues
|
|
66
|
+
- CSRF vulnerabilities
|
|
67
|
+
- Insecure deserialization
|
|
68
|
+
|
|
69
|
+
If no security issues are found, do not create any tasks.
|
|
70
|
+
Only create tasks for real security concerns.
|
|
71
|
+
Limit to max 10 most critical security issues.`;
|
|
72
|
+
|
|
73
|
+
function isValidRepoPattern(cwd: string): boolean {
|
|
74
|
+
const dirName = cwd.split("/").filter(Boolean).pop() || "";
|
|
75
|
+
// Match: hook-checklint, skill-installhook, iapp-mail, etc.
|
|
76
|
+
return /^[a-z]+-[a-z0-9-]+$/i.test(dirName);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readStdinJson(): HookInput | null {
|
|
80
|
+
try {
|
|
81
|
+
const stdin = readFileSync(0, "utf-8");
|
|
82
|
+
return JSON.parse(stdin);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readSettings(path: string): Record<string, unknown> {
|
|
89
|
+
if (!existsSync(path)) return {};
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
92
|
+
} catch {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getConfig(cwd: string): CheckSecurityConfig {
|
|
98
|
+
// Try project settings first
|
|
99
|
+
const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
|
|
100
|
+
if (projectSettings[CONFIG_KEY]) {
|
|
101
|
+
return projectSettings[CONFIG_KEY] as CheckSecurityConfig;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Fall back to global settings
|
|
105
|
+
const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
|
|
106
|
+
if (globalSettings[CONFIG_KEY]) {
|
|
107
|
+
return globalSettings[CONFIG_KEY] as CheckSecurityConfig;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Default config
|
|
111
|
+
return {
|
|
112
|
+
keywords: ["dev"],
|
|
113
|
+
enabled: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getStateFile(sessionId: string): string {
|
|
118
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
119
|
+
const safeSessionId = sanitizeId(sessionId);
|
|
120
|
+
return join(STATE_DIR, `checksecurity-${safeSessionId}.json`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getSessionState(sessionId: string): SessionState {
|
|
124
|
+
const stateFile = getStateFile(sessionId);
|
|
125
|
+
if (existsSync(stateFile)) {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
128
|
+
} catch {
|
|
129
|
+
// Corrupted state, reset
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { securityChecked: false, lastCheckRun: 0 };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function saveSessionState(sessionId: string, state: SessionState): void {
|
|
136
|
+
const stateFile = getStateFile(sessionId);
|
|
137
|
+
writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getSessionName(transcriptPath: string): string | null {
|
|
141
|
+
if (!existsSync(transcriptPath)) return null;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const content = readFileSync(transcriptPath, "utf-8");
|
|
145
|
+
let lastTitle: string | null = null;
|
|
146
|
+
let searchStart = 0;
|
|
147
|
+
|
|
148
|
+
while (true) {
|
|
149
|
+
const titleIndex = content.indexOf('"custom-title"', searchStart);
|
|
150
|
+
if (titleIndex === -1) break;
|
|
151
|
+
|
|
152
|
+
const lineStart = content.lastIndexOf("\n", titleIndex) + 1;
|
|
153
|
+
const lineEnd = content.indexOf("\n", titleIndex);
|
|
154
|
+
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const entry = JSON.parse(line);
|
|
158
|
+
if (entry.type === "custom-title" && entry.customTitle) {
|
|
159
|
+
lastTitle = entry.customTitle;
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Skip malformed lines
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
searchStart = titleIndex + 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lastTitle;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getProjectTaskListId(cwd: string): string | null {
|
|
175
|
+
const dirName = cwd.split("/").filter(Boolean).pop() || "";
|
|
176
|
+
return `${dirName}-dev`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isClaudeAvailable(): boolean {
|
|
180
|
+
try {
|
|
181
|
+
execSync("which claude", { stdio: "pipe" });
|
|
182
|
+
return true;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isCodexAvailable(): boolean {
|
|
189
|
+
try {
|
|
190
|
+
execSync("which codex", { stdio: "pipe" });
|
|
191
|
+
return true;
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function runClaudeSecurityCheck(cwd: string, taskListId: string): void {
|
|
198
|
+
const safeTaskListId = sanitizeId(taskListId);
|
|
199
|
+
const prompt = SECURITY_PROMPT.replace("{taskListId}", safeTaskListId);
|
|
200
|
+
|
|
201
|
+
console.error(`[hook-checksecurity] Running Claude security check...`);
|
|
202
|
+
|
|
203
|
+
spawnSync(
|
|
204
|
+
"claude",
|
|
205
|
+
[
|
|
206
|
+
"-p",
|
|
207
|
+
prompt,
|
|
208
|
+
"--permission-mode",
|
|
209
|
+
"acceptEdits",
|
|
210
|
+
"--allowedTools",
|
|
211
|
+
"Bash,Read,Glob,Grep",
|
|
212
|
+
"--no-session-persistence",
|
|
213
|
+
],
|
|
214
|
+
{
|
|
215
|
+
cwd,
|
|
216
|
+
stdio: "inherit",
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function runCodexSecurityCheck(cwd: string, taskListId: string): void {
|
|
222
|
+
const safeTaskListId = sanitizeId(taskListId);
|
|
223
|
+
const prompt = SECURITY_PROMPT.replace("{taskListId}", safeTaskListId);
|
|
224
|
+
|
|
225
|
+
console.error(`[hook-checksecurity] Running Codex security check...`);
|
|
226
|
+
|
|
227
|
+
spawnSync(
|
|
228
|
+
"codex",
|
|
229
|
+
[
|
|
230
|
+
"exec",
|
|
231
|
+
prompt,
|
|
232
|
+
],
|
|
233
|
+
{
|
|
234
|
+
cwd,
|
|
235
|
+
stdio: "inherit",
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function approve() {
|
|
241
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function block(reason: string) {
|
|
246
|
+
console.log(JSON.stringify({ decision: "block", reason }));
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function run() {
|
|
251
|
+
const hookInput = readStdinJson();
|
|
252
|
+
if (!hookInput) {
|
|
253
|
+
approve();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const { session_id, cwd, transcript_path } = hookInput;
|
|
258
|
+
|
|
259
|
+
// Check repo pattern - only run for [prefix]-[name] folders
|
|
260
|
+
if (!isValidRepoPattern(cwd)) {
|
|
261
|
+
approve();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const config = getConfig(cwd);
|
|
266
|
+
|
|
267
|
+
// Check if hook is disabled
|
|
268
|
+
if (config.enabled === false) {
|
|
269
|
+
approve();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check keywords match
|
|
274
|
+
const sessionName = transcript_path ? getSessionName(transcript_path) : null;
|
|
275
|
+
const nameToCheck = sessionName || config.taskListId || "";
|
|
276
|
+
const keywords = config.keywords || ["dev"];
|
|
277
|
+
|
|
278
|
+
const matchesKeyword = keywords.some((keyword) =>
|
|
279
|
+
nameToCheck.toLowerCase().includes(keyword.toLowerCase())
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// If keywords are configured and we have a session name, check for match
|
|
283
|
+
if (keywords.length > 0 && nameToCheck && !matchesKeyword) {
|
|
284
|
+
approve();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check session state - only run once per session
|
|
289
|
+
const state = getSessionState(session_id);
|
|
290
|
+
if (state.securityChecked) {
|
|
291
|
+
// Already ran security check this session
|
|
292
|
+
approve();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Mark as checked before running (prevent re-runs)
|
|
297
|
+
state.securityChecked = true;
|
|
298
|
+
state.lastCheckRun = Date.now();
|
|
299
|
+
saveSessionState(session_id, state);
|
|
300
|
+
|
|
301
|
+
const taskListId = config.taskListId || getProjectTaskListId(cwd) || "default-dev";
|
|
302
|
+
|
|
303
|
+
// Run security checks
|
|
304
|
+
const claudeAvailable = isClaudeAvailable();
|
|
305
|
+
const codexAvailable = isCodexAvailable();
|
|
306
|
+
|
|
307
|
+
if (!claudeAvailable && !codexAvailable) {
|
|
308
|
+
console.error(`[hook-checksecurity] Neither Claude nor Codex CLI available, skipping security check`);
|
|
309
|
+
approve();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.error(`[hook-checksecurity] Running security checks for ${cwd}`);
|
|
314
|
+
|
|
315
|
+
// Run Claude security check
|
|
316
|
+
if (claudeAvailable) {
|
|
317
|
+
runClaudeSecurityCheck(cwd, taskListId);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Run Codex security check
|
|
321
|
+
if (codexAvailable) {
|
|
322
|
+
runCodexSecurityCheck(cwd, taskListId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.error(`[hook-checksecurity] Security checks completed`);
|
|
326
|
+
|
|
327
|
+
// After running checks, approve (let checktasks handle blocking if tasks exist)
|
|
328
|
+
approve();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Allow direct execution
|
|
332
|
+
if (import.meta.main) {
|
|
333
|
+
run();
|
|
334
|
+
}
|
|
@@ -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,144 @@
|
|
|
1
|
+
# @hasnaxyz/hook-checktasks
|
|
2
|
+
|
|
3
|
+
A Claude Code hook that prevents Claude from stopping when there are pending tasks.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
This hook intercepts Claude's "Stop" event and:
|
|
8
|
+
|
|
9
|
+
1. Checks configured task lists for pending/in-progress tasks
|
|
10
|
+
2. If tasks remain → **blocks the stop** and prompts Claude to continue
|
|
11
|
+
3. If all complete → allows stop
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### 1. Install the CLI globally
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add -g @hasnaxyz/hook-checktasks
|
|
19
|
+
# or
|
|
20
|
+
npm install -g @hasnaxyz/hook-checktasks
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Install the hook
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Auto-detect (git repo → project, else → prompt)
|
|
27
|
+
hook-checktasks install
|
|
28
|
+
|
|
29
|
+
# Install globally
|
|
30
|
+
hook-checktasks install --global
|
|
31
|
+
|
|
32
|
+
# Install to specific path
|
|
33
|
+
hook-checktasks install /path/to/project
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The installer will prompt you to configure:
|
|
37
|
+
- **Task list ID**: specific list or leave empty for all lists
|
|
38
|
+
- **Keywords**: session/list name keywords to trigger the check (default: "dev")
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
### Update configuration
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
hook-checktasks config # Current project
|
|
46
|
+
hook-checktasks config --global # Global settings
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Check status
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
hook-checktasks status
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Shows:
|
|
56
|
+
- Where hook is installed (global/project)
|
|
57
|
+
- Current configuration
|
|
58
|
+
- Available task lists
|
|
59
|
+
|
|
60
|
+
### Uninstall
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
hook-checktasks uninstall # Current project
|
|
64
|
+
hook-checktasks uninstall --global # Global
|
|
65
|
+
hook-checktasks uninstall /path # Specific path
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## How Configuration Works
|
|
69
|
+
|
|
70
|
+
Configuration is stored in `.claude/settings.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"hooks": {
|
|
75
|
+
"Stop": [{ "hooks": [{ "type": "command", "command": "bunx @hasnaxyz/hook-checktasks run" }] }]
|
|
76
|
+
},
|
|
77
|
+
"checkTasksConfig": {
|
|
78
|
+
"taskListId": "myproject-dev",
|
|
79
|
+
"keywords": ["dev"],
|
|
80
|
+
"enabled": true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Config Options
|
|
86
|
+
|
|
87
|
+
| Option | Description | Default |
|
|
88
|
+
|--------|-------------|---------|
|
|
89
|
+
| `taskListId` | Specific task list to check, or `undefined` for all lists | `undefined` (all) |
|
|
90
|
+
| `keywords` | Keywords to match in session/list names | `["dev"]` |
|
|
91
|
+
| `enabled` | Enable/disable the hook | `true` |
|
|
92
|
+
|
|
93
|
+
### Priority
|
|
94
|
+
|
|
95
|
+
1. Project settings (`.claude/settings.json`)
|
|
96
|
+
2. Global settings (`~/.claude/settings.json`)
|
|
97
|
+
3. Environment variables (legacy)
|
|
98
|
+
|
|
99
|
+
### Legacy Environment Variables
|
|
100
|
+
|
|
101
|
+
Still supported for backwards compatibility:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
|
|
105
|
+
CHECK_TASKS_KEYWORDS=dev,sprint claude
|
|
106
|
+
CHECK_TASKS_DISABLED=1 claude
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## CLI Commands
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
hook-checktasks install [path] Install the hook
|
|
113
|
+
hook-checktasks config [path] Update configuration
|
|
114
|
+
hook-checktasks uninstall [path] Remove the hook
|
|
115
|
+
hook-checktasks status Show hook status
|
|
116
|
+
hook-checktasks run Execute hook (called by Claude Code)
|
|
117
|
+
|
|
118
|
+
Options:
|
|
119
|
+
--global, -g Apply to global settings
|
|
120
|
+
/path/to/repo Apply to specific project
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## How it Works
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
Claude tries to stop
|
|
127
|
+
│
|
|
128
|
+
▼
|
|
129
|
+
Stop hook fires
|
|
130
|
+
│
|
|
131
|
+
▼
|
|
132
|
+
Read config from settings.json
|
|
133
|
+
│
|
|
134
|
+
▼
|
|
135
|
+
Check matching task lists
|
|
136
|
+
│
|
|
137
|
+
├── Tasks remaining → BLOCK stop, prompt to continue
|
|
138
|
+
│
|
|
139
|
+
└── All complete → ALLOW stop
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasnaxyz/hook-checktasks",
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "Claude Code hook that prevents stopping when there are pending tasks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hook-checktasks": "./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
|
+
"tasks",
|
|
33
|
+
"automation",
|
|
34
|
+
"cli"
|
|
35
|
+
],
|
|
36
|
+
"author": "Hasna",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/hasnaxyz/hook-checktasks.git"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "restricted",
|
|
44
|
+
"registry": "https://registry.npmjs.org/"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18",
|
|
48
|
+
"bun": ">=1.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "^1.3.8",
|
|
52
|
+
"@types/node": "^20",
|
|
53
|
+
"typescript": "^5.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|