@hasnaxyz/hook-checktasks 0.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 (4) hide show
  1. package/README.md +152 -0
  2. package/dist/cli.js +394 -0
  3. package/dist/hook.js +131 -0
  4. package/package.json +55 -0
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # @hasnaxyz/hook-checktasks
2
+
3
+ A Claude Code hook that prevents Claude from stopping when there are pending tasks in your task list.
4
+
5
+ ## What it does
6
+
7
+ When you have a task list configured (`CLAUDE_CODE_TASK_LIST_ID`), this hook:
8
+
9
+ 1. Checks if the session name or task list ID contains "dev" (configurable)
10
+ 2. Reads the task list from `~/.claude/tasks/{taskListId}/`
11
+ 3. If there are pending or in-progress tasks, **blocks the stop** and prompts Claude to continue working
12
+ 4. Only allows stopping when all tasks are completed
13
+
14
+ This ensures Claude doesn't abandon work mid-session.
15
+
16
+ ## Installation
17
+
18
+ ### Prerequisites
19
+
20
+ - [Bun](https://bun.sh/) runtime installed
21
+ - Access to `@hasnaxyz` npm organization
22
+
23
+ ### Quick Install
24
+
25
+ ```bash
26
+ # Install globally (applies to all Claude Code sessions)
27
+ bunx @hasnaxyz/hook-checktasks install --global
28
+
29
+ # Install for current project only
30
+ bunx @hasnaxyz/hook-checktasks install
31
+ ```
32
+
33
+ ### Check Installation Status
34
+
35
+ ```bash
36
+ bunx @hasnaxyz/hook-checktasks status
37
+ ```
38
+
39
+ ### Uninstall
40
+
41
+ ```bash
42
+ # Remove from global settings
43
+ bunx @hasnaxyz/hook-checktasks uninstall --global
44
+
45
+ # Remove from current project
46
+ bunx @hasnaxyz/hook-checktasks uninstall
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ Configure via environment variables:
52
+
53
+ | Variable | Description | Default |
54
+ |----------|-------------|---------|
55
+ | `CLAUDE_CODE_TASK_LIST_ID` | Task list to monitor (required for task checking) | - |
56
+ | `CHECK_TASKS_KEYWORDS` | Comma-separated keywords that trigger the check | `dev` |
57
+ | `CHECK_TASKS_DISABLED` | Set to `1` to disable the hook | - |
58
+
59
+ ### Examples
60
+
61
+ ```bash
62
+ # Standard usage with dev task list
63
+ CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
64
+
65
+ # Check for "dev" or "sprint" sessions
66
+ CHECK_TASKS_KEYWORDS=dev,sprint CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
67
+
68
+ # Temporarily disable the hook
69
+ CHECK_TASKS_DISABLED=1 claude
70
+ ```
71
+
72
+ ## How it works
73
+
74
+ 1. **On Stop hook trigger**: Claude is about to stop/exit
75
+ 2. **Check session name**: Reads the session name from transcript (if available)
76
+ 3. **Keyword matching**: Checks if session name or task list ID contains configured keywords
77
+ 4. **Task count**: Reads task files from `~/.claude/tasks/{taskListId}/`
78
+ 5. **Decision**:
79
+ - If tasks remain: Block stop with instructions to continue
80
+ - If all complete: Allow stop
81
+
82
+ ## What Gets Added to settings.json
83
+
84
+ The install command adds this to your Claude Code settings:
85
+
86
+ ```json
87
+ {
88
+ "hooks": {
89
+ "Stop": [
90
+ {
91
+ "hooks": [
92
+ {
93
+ "type": "command",
94
+ "command": "bunx @hasnaxyz/hook-checktasks run",
95
+ "timeout": 120
96
+ }
97
+ ]
98
+ }
99
+ ]
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Task File Format
105
+
106
+ Tasks are stored as JSON files in `~/.claude/tasks/{taskListId}/`:
107
+
108
+ ```json
109
+ {
110
+ "id": "task-001",
111
+ "subject": "Implement user authentication",
112
+ "status": "pending"
113
+ }
114
+ ```
115
+
116
+ Status values: `pending`, `in_progress`, `completed`
117
+
118
+ ## CLI Commands
119
+
120
+ ```
121
+ bunx @hasnaxyz/hook-checktasks <command> [options]
122
+
123
+ Commands:
124
+ install [--global] Install the hook to Claude Code settings
125
+ uninstall [--global] Remove the hook from Claude Code settings
126
+ run Execute the hook (called by Claude Code automatically)
127
+ status Show current hook configuration
128
+
129
+ Options:
130
+ --global, -g Apply to global settings (~/.claude/settings.json)
131
+ ```
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ # Clone the repo
137
+ git clone https://github.com/hasnaxyz/hook-checktasks.git
138
+ cd hook-checktasks
139
+
140
+ # Install dependencies
141
+ bun install
142
+
143
+ # Build
144
+ bun run build
145
+
146
+ # Test locally
147
+ bun ./dist/cli.js status
148
+ ```
149
+
150
+ ## License
151
+
152
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ import { createRequire } from "node:module";
4
+ var __defProp = Object.defineProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, {
8
+ get: all[name],
9
+ enumerable: true,
10
+ configurable: true,
11
+ set: (newValue) => all[name] = () => newValue
12
+ });
13
+ };
14
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
15
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
16
+
17
+ // src/hook.ts
18
+ var exports_hook = {};
19
+ __export(exports_hook, {
20
+ run: () => run
21
+ });
22
+ import { readdirSync, readFileSync, existsSync } from "fs";
23
+ import { join } from "path";
24
+ import { homedir } from "os";
25
+ function readStdinJson() {
26
+ try {
27
+ const stdin = readFileSync(0, "utf-8");
28
+ return JSON.parse(stdin);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function getSessionName(transcriptPath) {
34
+ if (!existsSync(transcriptPath))
35
+ return null;
36
+ try {
37
+ const content = readFileSync(transcriptPath, "utf-8");
38
+ let lastTitle = null;
39
+ let searchStart = 0;
40
+ while (true) {
41
+ const titleIndex = content.indexOf('"custom-title"', searchStart);
42
+ if (titleIndex === -1)
43
+ break;
44
+ const lineStart = content.lastIndexOf(`
45
+ `, titleIndex) + 1;
46
+ const lineEnd = content.indexOf(`
47
+ `, titleIndex);
48
+ const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
49
+ try {
50
+ const entry = JSON.parse(line);
51
+ if (entry.type === "custom-title" && entry.customTitle) {
52
+ lastTitle = entry.customTitle;
53
+ }
54
+ } catch {}
55
+ searchStart = titleIndex + 1;
56
+ }
57
+ return lastTitle;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ function approve() {
63
+ console.log(JSON.stringify({ decision: "approve" }));
64
+ process.exit(0);
65
+ }
66
+ function block(reason) {
67
+ console.log(JSON.stringify({ decision: "block", reason }));
68
+ process.exit(0);
69
+ }
70
+ function run() {
71
+ if (process.env.CHECK_TASKS_DISABLED === "1") {
72
+ approve();
73
+ }
74
+ const taskListId = process.env.CLAUDE_CODE_TASK_LIST_ID;
75
+ if (!taskListId) {
76
+ approve();
77
+ }
78
+ const hookInput = readStdinJson();
79
+ let sessionName = null;
80
+ if (hookInput?.transcript_path) {
81
+ sessionName = getSessionName(hookInput.transcript_path);
82
+ }
83
+ const nameToCheck = sessionName || taskListId || "";
84
+ const keywords = (process.env.CHECK_TASKS_KEYWORDS || "dev").split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
85
+ const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword));
86
+ if (!matchesKeyword) {
87
+ approve();
88
+ }
89
+ const tasksDir = join(homedir(), ".claude", "tasks", taskListId);
90
+ if (!existsSync(tasksDir)) {
91
+ approve();
92
+ }
93
+ const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
94
+ if (taskFiles.length === 0) {
95
+ approve();
96
+ }
97
+ const tasks = taskFiles.map((file) => {
98
+ const content = readFileSync(join(tasksDir, file), "utf-8");
99
+ return JSON.parse(content);
100
+ });
101
+ const pending = tasks.filter((t) => t.status === "pending");
102
+ const inProgress = tasks.filter((t) => t.status === "in_progress");
103
+ const completed = tasks.filter((t) => t.status === "completed");
104
+ const remainingCount = pending.length + inProgress.length;
105
+ if (remainingCount > 0) {
106
+ const nextTasks = pending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
107
+ `);
108
+ const prompt = `
109
+ STOP BLOCKED: You have ${remainingCount} tasks remaining (${pending.length} pending, ${inProgress.length} in progress, ${completed.length} completed).
110
+
111
+ You MUST continue working. Do NOT stop until all tasks are completed.
112
+
113
+ Next pending tasks:
114
+ ${nextTasks}
115
+ ${pending.length > 3 ? `... and ${pending.length - 3} more pending tasks` : ""}
116
+
117
+ INSTRUCTIONS:
118
+ 1. Use TaskList to see all tasks
119
+ 2. Pick the next pending task
120
+ 3. Use TaskUpdate to mark it as in_progress
121
+ 4. Complete the task
122
+ 5. Use TaskUpdate to mark it as completed
123
+ 6. Repeat until all tasks are done
124
+
125
+ DO NOT STOP. Continue working now.
126
+ `.trim();
127
+ block(prompt);
128
+ }
129
+ approve();
130
+ }
131
+ var init_hook = __esm(() => {
132
+ if (false) {}
133
+ });
134
+
135
+ // src/cli.ts
136
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
137
+ import { join as join2, dirname, resolve } from "path";
138
+ import { homedir as homedir2 } from "os";
139
+ import * as readline from "readline";
140
+ var PACKAGE_NAME = "@hasnaxyz/hook-checktasks";
141
+ var c = {
142
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
143
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
144
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`,
145
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
146
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
147
+ bold: (s) => `\x1B[1m${s}\x1B[0m`
148
+ };
149
+ function printUsage() {
150
+ console.log(`
151
+ ${c.bold("hook-checktasks")} - Prevents Claude from stopping with pending tasks
152
+
153
+ ${c.bold("USAGE:")}
154
+ hook-checktasks install [path] Install the hook
155
+ hook-checktasks uninstall [path] Remove the hook
156
+ hook-checktasks status Show hook status
157
+ hook-checktasks run Execute hook ${c.dim("(called by Claude Code)")}
158
+
159
+ ${c.bold("INSTALL OPTIONS:")}
160
+ ${c.dim("(no args)")} Auto-detect: if in git repo \u2192 install there, else \u2192 prompt
161
+ --global, -g Install to ~/.claude/settings.json
162
+ /path/to/repo Install to specific project path
163
+
164
+ ${c.bold("EXAMPLES:")}
165
+ hook-checktasks install ${c.dim("# Smart install")}
166
+ hook-checktasks install --global ${c.dim("# Global install")}
167
+ hook-checktasks install ~/my-project ${c.dim("# Specific project")}
168
+ hook-checktasks status ${c.dim("# Check what's installed")}
169
+
170
+ ${c.bold("GLOBAL INSTALL:")} ${c.dim("(to use without bunx)")}
171
+ bun add -g ${PACKAGE_NAME}
172
+ `);
173
+ }
174
+ function isGitRepo(path) {
175
+ return existsSync2(join2(path, ".git"));
176
+ }
177
+ function getSettingsPath(targetPath) {
178
+ if (targetPath === "global") {
179
+ return join2(homedir2(), ".claude", "settings.json");
180
+ }
181
+ return join2(targetPath, ".claude", "settings.json");
182
+ }
183
+ function readSettings(path) {
184
+ if (!existsSync2(path))
185
+ return {};
186
+ try {
187
+ return JSON.parse(readFileSync2(path, "utf-8"));
188
+ } catch {
189
+ return {};
190
+ }
191
+ }
192
+ function writeSettings(path, settings) {
193
+ mkdirSync(dirname(path), { recursive: true });
194
+ writeFileSync(path, JSON.stringify(settings, null, 2) + `
195
+ `);
196
+ }
197
+ function getHookCommand() {
198
+ return `bunx ${PACKAGE_NAME} run`;
199
+ }
200
+ function hookExists(settings) {
201
+ const hooks = settings.hooks;
202
+ if (!hooks?.Stop)
203
+ return false;
204
+ const stopHooks = hooks.Stop;
205
+ return stopHooks.some((group) => group.hooks?.some((h) => h.command?.includes(PACKAGE_NAME)));
206
+ }
207
+ function addHook(settings) {
208
+ const hookConfig = {
209
+ type: "command",
210
+ command: getHookCommand(),
211
+ timeout: 120
212
+ };
213
+ if (!settings.hooks)
214
+ settings.hooks = {};
215
+ const hooks = settings.hooks;
216
+ if (!hooks.Stop) {
217
+ hooks.Stop = [{ hooks: [hookConfig] }];
218
+ } else {
219
+ const stopHooks = hooks.Stop;
220
+ if (stopHooks[0]?.hooks) {
221
+ stopHooks[0].hooks.push(hookConfig);
222
+ } else {
223
+ stopHooks.push({ hooks: [hookConfig] });
224
+ }
225
+ }
226
+ return settings;
227
+ }
228
+ function removeHook(settings) {
229
+ const hooks = settings.hooks;
230
+ if (!hooks?.Stop)
231
+ return settings;
232
+ const stopHooks = hooks.Stop;
233
+ for (const group of stopHooks) {
234
+ if (group.hooks) {
235
+ group.hooks = group.hooks.filter((h) => !h.command?.includes(PACKAGE_NAME));
236
+ }
237
+ }
238
+ hooks.Stop = stopHooks.filter((g) => g.hooks && g.hooks.length > 0);
239
+ if (hooks.Stop.length === 0)
240
+ delete hooks.Stop;
241
+ return settings;
242
+ }
243
+ async function prompt(question) {
244
+ const rl = readline.createInterface({
245
+ input: process.stdin,
246
+ output: process.stdout
247
+ });
248
+ return new Promise((resolve2) => {
249
+ rl.question(question, (answer) => {
250
+ rl.close();
251
+ resolve2(answer.trim());
252
+ });
253
+ });
254
+ }
255
+ async function resolveTarget(args) {
256
+ if (args.includes("--global") || args.includes("-g")) {
257
+ return { path: "global", label: "global (~/.claude/settings.json)" };
258
+ }
259
+ const pathArg = args.find((a) => !a.startsWith("-"));
260
+ if (pathArg) {
261
+ const fullPath = resolve(pathArg);
262
+ if (!existsSync2(fullPath)) {
263
+ console.log(c.red("\u2717"), `Path does not exist: ${fullPath}`);
264
+ return null;
265
+ }
266
+ return { path: fullPath, label: `project (${fullPath})` };
267
+ }
268
+ const cwd = process.cwd();
269
+ if (isGitRepo(cwd)) {
270
+ console.log(c.green("\u2713"), `Detected git repo: ${c.cyan(cwd)}`);
271
+ return { path: cwd, label: `project (${cwd})` };
272
+ }
273
+ console.log(c.yellow("!"), `Not in a git repository
274
+ `);
275
+ console.log(`Where would you like to install?
276
+ `);
277
+ console.log(" 1. Global", c.dim("(~/.claude/settings.json)"));
278
+ console.log(` 2. Enter a path
279
+ `);
280
+ const choice = await prompt("Choice (1/2): ");
281
+ if (choice === "1") {
282
+ return { path: "global", label: "global (~/.claude/settings.json)" };
283
+ } else if (choice === "2") {
284
+ const inputPath = await prompt("Path: ");
285
+ if (!inputPath) {
286
+ console.log(c.red("\u2717"), "No path provided");
287
+ return null;
288
+ }
289
+ const fullPath = resolve(inputPath);
290
+ if (!existsSync2(fullPath)) {
291
+ console.log(c.red("\u2717"), `Path does not exist: ${fullPath}`);
292
+ return null;
293
+ }
294
+ return { path: fullPath, label: `project (${fullPath})` };
295
+ } else {
296
+ console.log(c.red("\u2717"), "Invalid choice");
297
+ return null;
298
+ }
299
+ }
300
+ async function install(args) {
301
+ console.log(`
302
+ ${c.bold("hook-checktasks install")}
303
+ `);
304
+ const target = await resolveTarget(args);
305
+ if (!target)
306
+ return;
307
+ const settingsPath = getSettingsPath(target.path);
308
+ const settings = readSettings(settingsPath);
309
+ if (hookExists(settings)) {
310
+ console.log(c.yellow("!"), `Already installed in ${target.label}`);
311
+ return;
312
+ }
313
+ const updated = addHook(settings);
314
+ writeSettings(settingsPath, updated);
315
+ console.log(c.green("\u2713"), `Installed to ${target.label}
316
+ `);
317
+ console.log(c.bold("Usage:"));
318
+ console.log(` CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
319
+ `);
320
+ }
321
+ async function uninstall(args) {
322
+ console.log(`
323
+ ${c.bold("hook-checktasks uninstall")}
324
+ `);
325
+ const target = await resolveTarget(args);
326
+ if (!target)
327
+ return;
328
+ const settingsPath = getSettingsPath(target.path);
329
+ if (!existsSync2(settingsPath)) {
330
+ console.log(c.yellow("!"), `No settings file at ${settingsPath}`);
331
+ return;
332
+ }
333
+ const settings = readSettings(settingsPath);
334
+ if (!hookExists(settings)) {
335
+ console.log(c.yellow("!"), `Hook not found in ${target.label}`);
336
+ return;
337
+ }
338
+ const updated = removeHook(settings);
339
+ writeSettings(settingsPath, updated);
340
+ console.log(c.green("\u2713"), `Removed from ${target.label}`);
341
+ }
342
+ function status() {
343
+ console.log(`
344
+ ${c.bold("hook-checktasks status")}
345
+ `);
346
+ const globalPath = getSettingsPath("global");
347
+ const globalSettings = readSettings(globalPath);
348
+ const globalInstalled = hookExists(globalSettings);
349
+ console.log(globalInstalled ? c.green("\u2713") : c.red("\u2717"), "Global:", globalInstalled ? "Installed" : "Not installed", c.dim(`(${globalPath})`));
350
+ const cwd = process.cwd();
351
+ const projectPath = getSettingsPath(cwd);
352
+ if (isGitRepo(cwd)) {
353
+ const projectSettings = readSettings(projectPath);
354
+ const projectInstalled = hookExists(projectSettings);
355
+ console.log(projectInstalled ? c.green("\u2713") : c.red("\u2717"), "Project:", projectInstalled ? "Installed" : "Not installed", c.dim(`(${projectPath})`));
356
+ } else {
357
+ console.log(c.dim("\xB7"), "Project:", c.dim("Not in a git repo"));
358
+ }
359
+ console.log();
360
+ console.log(c.bold("Environment:"));
361
+ const taskListId = process.env.CLAUDE_CODE_TASK_LIST_ID;
362
+ const keywords = process.env.CHECK_TASKS_KEYWORDS;
363
+ const disabled = process.env.CHECK_TASKS_DISABLED === "1";
364
+ console.log(" CLAUDE_CODE_TASK_LIST_ID:", taskListId || c.yellow("(not set)"));
365
+ console.log(" CHECK_TASKS_KEYWORDS:", keywords || c.dim("dev (default)"));
366
+ console.log(" CHECK_TASKS_DISABLED:", disabled ? c.yellow("yes") : "no");
367
+ console.log();
368
+ }
369
+ var args = process.argv.slice(2);
370
+ var command = args[0];
371
+ var commandArgs = args.slice(1);
372
+ switch (command) {
373
+ case "install":
374
+ install(commandArgs);
375
+ break;
376
+ case "uninstall":
377
+ uninstall(commandArgs);
378
+ break;
379
+ case "run":
380
+ Promise.resolve().then(() => (init_hook(), exports_hook)).then((m) => m.run());
381
+ break;
382
+ case "status":
383
+ status();
384
+ break;
385
+ case "--help":
386
+ case "-h":
387
+ case undefined:
388
+ printUsage();
389
+ break;
390
+ default:
391
+ console.error(c.red(`Unknown command: ${command}`));
392
+ printUsage();
393
+ process.exit(1);
394
+ }
package/dist/hook.js ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bun
2
+ import { createRequire } from "node:module";
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
15
+
16
+ // src/hook.ts
17
+ import { readdirSync, readFileSync, existsSync } from "fs";
18
+ import { join } from "path";
19
+ import { homedir } from "os";
20
+ function readStdinJson() {
21
+ try {
22
+ const stdin = readFileSync(0, "utf-8");
23
+ return JSON.parse(stdin);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function getSessionName(transcriptPath) {
29
+ if (!existsSync(transcriptPath))
30
+ return null;
31
+ try {
32
+ const content = readFileSync(transcriptPath, "utf-8");
33
+ let lastTitle = null;
34
+ let searchStart = 0;
35
+ while (true) {
36
+ const titleIndex = content.indexOf('"custom-title"', searchStart);
37
+ if (titleIndex === -1)
38
+ break;
39
+ const lineStart = content.lastIndexOf(`
40
+ `, titleIndex) + 1;
41
+ const lineEnd = content.indexOf(`
42
+ `, titleIndex);
43
+ const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
44
+ try {
45
+ const entry = JSON.parse(line);
46
+ if (entry.type === "custom-title" && entry.customTitle) {
47
+ lastTitle = entry.customTitle;
48
+ }
49
+ } catch {}
50
+ searchStart = titleIndex + 1;
51
+ }
52
+ return lastTitle;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ function approve() {
58
+ console.log(JSON.stringify({ decision: "approve" }));
59
+ process.exit(0);
60
+ }
61
+ function block(reason) {
62
+ console.log(JSON.stringify({ decision: "block", reason }));
63
+ process.exit(0);
64
+ }
65
+ function run() {
66
+ if (process.env.CHECK_TASKS_DISABLED === "1") {
67
+ approve();
68
+ }
69
+ const taskListId = process.env.CLAUDE_CODE_TASK_LIST_ID;
70
+ if (!taskListId) {
71
+ approve();
72
+ }
73
+ const hookInput = readStdinJson();
74
+ let sessionName = null;
75
+ if (hookInput?.transcript_path) {
76
+ sessionName = getSessionName(hookInput.transcript_path);
77
+ }
78
+ const nameToCheck = sessionName || taskListId || "";
79
+ const keywords = (process.env.CHECK_TASKS_KEYWORDS || "dev").split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
80
+ const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword));
81
+ if (!matchesKeyword) {
82
+ approve();
83
+ }
84
+ const tasksDir = join(homedir(), ".claude", "tasks", taskListId);
85
+ if (!existsSync(tasksDir)) {
86
+ approve();
87
+ }
88
+ const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
89
+ if (taskFiles.length === 0) {
90
+ approve();
91
+ }
92
+ const tasks = taskFiles.map((file) => {
93
+ const content = readFileSync(join(tasksDir, file), "utf-8");
94
+ return JSON.parse(content);
95
+ });
96
+ const pending = tasks.filter((t) => t.status === "pending");
97
+ const inProgress = tasks.filter((t) => t.status === "in_progress");
98
+ const completed = tasks.filter((t) => t.status === "completed");
99
+ const remainingCount = pending.length + inProgress.length;
100
+ if (remainingCount > 0) {
101
+ const nextTasks = pending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
102
+ `);
103
+ const prompt = `
104
+ STOP BLOCKED: You have ${remainingCount} tasks remaining (${pending.length} pending, ${inProgress.length} in progress, ${completed.length} completed).
105
+
106
+ You MUST continue working. Do NOT stop until all tasks are completed.
107
+
108
+ Next pending tasks:
109
+ ${nextTasks}
110
+ ${pending.length > 3 ? `... and ${pending.length - 3} more pending tasks` : ""}
111
+
112
+ INSTRUCTIONS:
113
+ 1. Use TaskList to see all tasks
114
+ 2. Pick the next pending task
115
+ 3. Use TaskUpdate to mark it as in_progress
116
+ 4. Complete the task
117
+ 5. Use TaskUpdate to mark it as completed
118
+ 6. Repeat until all tasks are done
119
+
120
+ DO NOT STOP. Continue working now.
121
+ `.trim();
122
+ block(prompt);
123
+ }
124
+ approve();
125
+ }
126
+ if (__require.main == __require.module) {
127
+ run();
128
+ }
129
+ export {
130
+ run
131
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@hasnaxyz/hook-checktasks",
3
+ "version": "0.1.1",
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": "latest",
52
+ "@types/node": "^20",
53
+ "typescript": "^5.0.0"
54
+ }
55
+ }