@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.
Files changed (110) hide show
  1. package/.npmrc.example +2 -0
  2. package/AGENTS.md +54 -0
  3. package/CLAUDE.md +70 -0
  4. package/CONTRIBUTING.md +45 -0
  5. package/README.md +232 -0
  6. package/bin/index.js +5171 -0
  7. package/hooks/hook-agentmessages/CLAUDE.md +79 -0
  8. package/hooks/hook-agentmessages/LICENSE +21 -0
  9. package/hooks/hook-agentmessages/README.md +107 -0
  10. package/hooks/hook-agentmessages/package.json +31 -0
  11. package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
  12. package/hooks/hook-agentmessages/src/install.ts +126 -0
  13. package/hooks/hook-agentmessages/src/session-start.ts +255 -0
  14. package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
  15. package/hooks/hook-branchprotect/CLAUDE.md +23 -0
  16. package/hooks/hook-branchprotect/README.md +25 -0
  17. package/hooks/hook-branchprotect/package.json +42 -0
  18. package/hooks/hook-branchprotect/src/cli.ts +126 -0
  19. package/hooks/hook-branchprotect/src/hook.ts +88 -0
  20. package/hooks/hook-branchprotect/tsconfig.json +25 -0
  21. package/hooks/hook-checkbugs/LICENSE +21 -0
  22. package/hooks/hook-checkbugs/README.md +140 -0
  23. package/hooks/hook-checkbugs/package.json +58 -0
  24. package/hooks/hook-checkbugs/src/cli.ts +628 -0
  25. package/hooks/hook-checkbugs/src/hook.ts +335 -0
  26. package/hooks/hook-checkbugs/tsconfig.json +15 -0
  27. package/hooks/hook-checkdocs/README.md +137 -0
  28. package/hooks/hook-checkdocs/package.json +57 -0
  29. package/hooks/hook-checkdocs/src/cli.ts +628 -0
  30. package/hooks/hook-checkdocs/src/hook.ts +310 -0
  31. package/hooks/hook-checkdocs/tsconfig.json +15 -0
  32. package/hooks/hook-checkfiles/LICENSE +21 -0
  33. package/hooks/hook-checkfiles/README.md +141 -0
  34. package/hooks/hook-checkfiles/package.json +56 -0
  35. package/hooks/hook-checkfiles/src/cli.ts +545 -0
  36. package/hooks/hook-checkfiles/src/hook.ts +321 -0
  37. package/hooks/hook-checkfiles/tsconfig.json +15 -0
  38. package/hooks/hook-checklint/LICENSE +21 -0
  39. package/hooks/hook-checklint/README.md +147 -0
  40. package/hooks/hook-checklint/package.json +57 -0
  41. package/hooks/hook-checklint/src/cli-patch.ts +32 -0
  42. package/hooks/hook-checklint/src/cli.ts +667 -0
  43. package/hooks/hook-checklint/src/hook.ts +473 -0
  44. package/hooks/hook-checklint/tsconfig.json +15 -0
  45. package/hooks/hook-checkpoint/CLAUDE.md +23 -0
  46. package/hooks/hook-checkpoint/README.md +37 -0
  47. package/hooks/hook-checkpoint/package.json +58 -0
  48. package/hooks/hook-checkpoint/src/cli.ts +191 -0
  49. package/hooks/hook-checkpoint/src/hook.ts +207 -0
  50. package/hooks/hook-checkpoint/tsconfig.json +25 -0
  51. package/hooks/hook-checksecurity/LICENSE +21 -0
  52. package/hooks/hook-checksecurity/README.md +158 -0
  53. package/hooks/hook-checksecurity/package.json +57 -0
  54. package/hooks/hook-checksecurity/src/cli.ts +601 -0
  55. package/hooks/hook-checksecurity/src/hook.ts +334 -0
  56. package/hooks/hook-checksecurity/tsconfig.json +15 -0
  57. package/hooks/hook-checktasks/README.md +144 -0
  58. package/hooks/hook-checktasks/package.json +55 -0
  59. package/hooks/hook-checktasks/src/cli.ts +578 -0
  60. package/hooks/hook-checktasks/src/hook.ts +308 -0
  61. package/hooks/hook-checktasks/tsconfig.json +20 -0
  62. package/hooks/hook-checktests/LICENSE +21 -0
  63. package/hooks/hook-checktests/README.md +137 -0
  64. package/hooks/hook-checktests/package.json +57 -0
  65. package/hooks/hook-checktests/src/cli.ts +627 -0
  66. package/hooks/hook-checktests/src/hook.ts +334 -0
  67. package/hooks/hook-checktests/tsconfig.json +15 -0
  68. package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
  69. package/hooks/hook-contextrefresh/README.md +42 -0
  70. package/hooks/hook-contextrefresh/package.json +42 -0
  71. package/hooks/hook-contextrefresh/src/cli.ts +152 -0
  72. package/hooks/hook-contextrefresh/src/hook.ts +148 -0
  73. package/hooks/hook-contextrefresh/tsconfig.json +25 -0
  74. package/hooks/hook-gitguard/CLAUDE.md +22 -0
  75. package/hooks/hook-gitguard/README.md +30 -0
  76. package/hooks/hook-gitguard/package.json +57 -0
  77. package/hooks/hook-gitguard/src/cli.ts +159 -0
  78. package/hooks/hook-gitguard/src/hook.ts +129 -0
  79. package/hooks/hook-gitguard/tsconfig.json +25 -0
  80. package/hooks/hook-packageage/CLAUDE.md +23 -0
  81. package/hooks/hook-packageage/README.md +33 -0
  82. package/hooks/hook-packageage/package.json +42 -0
  83. package/hooks/hook-packageage/src/cli.ts +165 -0
  84. package/hooks/hook-packageage/src/hook.ts +177 -0
  85. package/hooks/hook-packageage/tsconfig.json +25 -0
  86. package/hooks/hook-phonenotify/CLAUDE.md +25 -0
  87. package/hooks/hook-phonenotify/README.md +44 -0
  88. package/hooks/hook-phonenotify/package.json +42 -0
  89. package/hooks/hook-phonenotify/src/cli.ts +196 -0
  90. package/hooks/hook-phonenotify/src/hook.ts +139 -0
  91. package/hooks/hook-phonenotify/tsconfig.json +25 -0
  92. package/hooks/hook-precompact/CLAUDE.md +23 -0
  93. package/hooks/hook-precompact/README.md +36 -0
  94. package/hooks/hook-precompact/package.json +42 -0
  95. package/hooks/hook-precompact/src/cli.ts +168 -0
  96. package/hooks/hook-precompact/src/hook.ts +122 -0
  97. package/hooks/hook-precompact/tsconfig.json +25 -0
  98. package/package.json +61 -0
  99. package/src/cli/components/App.tsx +191 -0
  100. package/src/cli/components/CategorySelect.tsx +37 -0
  101. package/src/cli/components/DataTable.tsx +133 -0
  102. package/src/cli/components/Header.tsx +18 -0
  103. package/src/cli/components/HookSelect.tsx +29 -0
  104. package/src/cli/components/InstallProgress.tsx +105 -0
  105. package/src/cli/components/SearchView.tsx +86 -0
  106. package/src/cli/index.tsx +218 -0
  107. package/src/index.ts +31 -0
  108. package/src/lib/installer.ts +288 -0
  109. package/src/lib/registry.ts +205 -0
  110. package/tsconfig.json +17 -0
@@ -0,0 +1,473 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: check-lint
5
+ *
6
+ * Runs linting after every N file edits and creates tasks for errors.
7
+ *
8
+ * Configuration priority:
9
+ * 1. settings.json checkLintConfig (project or global)
10
+ * 2. Environment variables (legacy)
11
+ *
12
+ * Config options:
13
+ * - taskListId: task list for creating lint error tasks (default: auto-detect bugfixes list)
14
+ * - lintCommand: lint command to run (default: auto-detect)
15
+ * - editThreshold: run lint after this many edits (default: 3, range: 3-7)
16
+ * - keywords: keywords that trigger the check (default: ["dev"])
17
+ * - enabled: enable/disable the hook
18
+ */
19
+
20
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
21
+ import { join, dirname } from "path";
22
+ import { homedir } from "os";
23
+ import { execSync } from "child_process";
24
+
25
+ interface CheckLintConfig {
26
+ taskListId?: string;
27
+ lintCommand?: string;
28
+ editThreshold?: number;
29
+ keywords?: string[];
30
+ enabled?: boolean;
31
+ createTasks?: boolean;
32
+ }
33
+
34
+ interface HookInput {
35
+ session_id: string;
36
+ transcript_path: string;
37
+ cwd: string;
38
+ tool_name: string;
39
+ tool_input: Record<string, unknown>;
40
+ tool_output?: string;
41
+ }
42
+
43
+ interface LintError {
44
+ file: string;
45
+ line: number;
46
+ column: number;
47
+ message: string;
48
+ rule?: string;
49
+ severity: "error" | "warning";
50
+ }
51
+
52
+ interface SessionState {
53
+ editCount: number;
54
+ editedFiles: string[];
55
+ lastLintRun: number;
56
+ }
57
+
58
+ const CONFIG_KEY = "checkLintConfig";
59
+ const STATE_DIR = join(homedir(), ".claude", "hook-state");
60
+ const EDIT_TOOLS = ["Edit", "Write", "NotebookEdit"];
61
+
62
+ // Allowed lint commands - whitelist approach for security
63
+ const ALLOWED_LINT_COMMANDS = [
64
+ "bun lint",
65
+ "bun lint:check",
66
+ "bun eslint",
67
+ "bun biome check",
68
+ "bunx @biomejs/biome check .",
69
+ "bunx eslint .",
70
+ "npm run lint",
71
+ "npm run lint:check",
72
+ "npx eslint .",
73
+ "npx @biomejs/biome check .",
74
+ ];
75
+
76
+ /**
77
+ * Sanitize ID to prevent path traversal and injection attacks
78
+ */
79
+ function sanitizeId(id: string): string {
80
+ if (!id || typeof id !== 'string') return 'default';
81
+ return id.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 100) || 'default';
82
+ }
83
+
84
+ /**
85
+ * Validate lint command against whitelist to prevent command injection
86
+ */
87
+ function isValidLintCommand(cmd: string): boolean {
88
+ if (!cmd || typeof cmd !== 'string') return false;
89
+ return ALLOWED_LINT_COMMANDS.some(allowed =>
90
+ cmd === allowed || cmd.startsWith(allowed + " ")
91
+ );
92
+ }
93
+
94
+ function isValidRepoPattern(cwd: string): boolean {
95
+ const dirName = cwd.split("/").filter(Boolean).pop() || "";
96
+ // Match: hook-checklint, skill-installhook, iapp-mail, etc.
97
+ return /^[a-z]+-[a-z0-9-]+$/i.test(dirName);
98
+ }
99
+
100
+ function readStdinJson(): HookInput | null {
101
+ try {
102
+ const stdin = readFileSync(0, "utf-8");
103
+ return JSON.parse(stdin);
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function readSettings(path: string): Record<string, unknown> {
110
+ if (!existsSync(path)) return {};
111
+ try {
112
+ return JSON.parse(readFileSync(path, "utf-8"));
113
+ } catch {
114
+ return {};
115
+ }
116
+ }
117
+
118
+ function getConfig(cwd: string): CheckLintConfig {
119
+ // Try project settings first
120
+ const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
121
+ if (projectSettings[CONFIG_KEY]) {
122
+ return projectSettings[CONFIG_KEY] as CheckLintConfig;
123
+ }
124
+
125
+ // Fall back to global settings
126
+ const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
127
+ if (globalSettings[CONFIG_KEY]) {
128
+ return globalSettings[CONFIG_KEY] as CheckLintConfig;
129
+ }
130
+
131
+ // Default config
132
+ return {
133
+ editThreshold: 3,
134
+ keywords: ["dev"],
135
+ enabled: true,
136
+ createTasks: true,
137
+ };
138
+ }
139
+
140
+ function getStateFile(sessionId: string): string {
141
+ mkdirSync(STATE_DIR, { recursive: true });
142
+ const safeSessionId = sanitizeId(sessionId);
143
+ return join(STATE_DIR, `checklint-${safeSessionId}.json`);
144
+ }
145
+
146
+ function getSessionState(sessionId: string): SessionState {
147
+ const stateFile = getStateFile(sessionId);
148
+ if (existsSync(stateFile)) {
149
+ try {
150
+ return JSON.parse(readFileSync(stateFile, "utf-8"));
151
+ } catch {
152
+ // Corrupted state, reset
153
+ }
154
+ }
155
+ return { editCount: 0, editedFiles: [], lastLintRun: 0 };
156
+ }
157
+
158
+ function saveSessionState(sessionId: string, state: SessionState): void {
159
+ const stateFile = getStateFile(sessionId);
160
+ writeFileSync(stateFile, JSON.stringify(state, null, 2));
161
+ }
162
+
163
+ function detectLintCommand(cwd: string): string | null {
164
+ const packageJsonPath = join(cwd, "package.json");
165
+ if (existsSync(packageJsonPath)) {
166
+ try {
167
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
168
+ const scripts = pkg.scripts || {};
169
+
170
+ // Check for common lint script names
171
+ if (scripts.lint) return "bun lint";
172
+ if (scripts["lint:check"]) return "bun lint:check";
173
+ if (scripts.eslint) return "bun eslint";
174
+ if (scripts.biome) return "bun biome check";
175
+ } catch {
176
+ // Ignore parse errors
177
+ }
178
+ }
179
+
180
+ // Check for config files
181
+ if (existsSync(join(cwd, "biome.json")) || existsSync(join(cwd, "biome.jsonc"))) {
182
+ return "bunx @biomejs/biome check .";
183
+ }
184
+ if (existsSync(join(cwd, ".eslintrc.json")) || existsSync(join(cwd, ".eslintrc.js")) || existsSync(join(cwd, "eslint.config.js"))) {
185
+ return "bunx eslint .";
186
+ }
187
+
188
+ return null;
189
+ }
190
+
191
+ function runLint(cwd: string, command: string): { success: boolean; output: string } {
192
+ try {
193
+ const output = execSync(command, {
194
+ cwd,
195
+ encoding: "utf-8",
196
+ stdio: ["pipe", "pipe", "pipe"],
197
+ timeout: 60000, // 60s timeout
198
+ });
199
+ return { success: true, output };
200
+ } catch (error: unknown) {
201
+ const execError = error as { stdout?: string; stderr?: string };
202
+ const output = (execError.stdout || "") + (execError.stderr || "");
203
+ return { success: false, output };
204
+ }
205
+ }
206
+
207
+ function parseLintOutput(output: string): LintError[] {
208
+ const errors: LintError[] = [];
209
+ const lines = output.split("\n");
210
+
211
+ for (const line of lines) {
212
+ // ESLint format: /path/file.ts:10:5: error Message [rule-name]
213
+ // Biome format: path/file.ts:10:5 lint/rule ERROR message
214
+ // Common format: file:line:col: message
215
+
216
+ // Try ESLint/common format
217
+ const eslintMatch = line.match(/^(.+?):(\d+):(\d+):\s*(error|warning)\s+(.+?)(?:\s+\[(.+?)\])?$/i);
218
+ if (eslintMatch) {
219
+ errors.push({
220
+ file: eslintMatch[1],
221
+ line: parseInt(eslintMatch[2], 10),
222
+ column: parseInt(eslintMatch[3], 10),
223
+ severity: eslintMatch[4].toLowerCase() as "error" | "warning",
224
+ message: eslintMatch[5].trim(),
225
+ rule: eslintMatch[6],
226
+ });
227
+ continue;
228
+ }
229
+
230
+ // Try Biome format
231
+ const biomeMatch = line.match(/^(.+?):(\d+):(\d+)\s+(\w+\/\w+)\s+(ERROR|WARNING|WARN)\s+(.+)$/i);
232
+ if (biomeMatch) {
233
+ errors.push({
234
+ file: biomeMatch[1],
235
+ line: parseInt(biomeMatch[2], 10),
236
+ column: parseInt(biomeMatch[3], 10),
237
+ rule: biomeMatch[4],
238
+ severity: biomeMatch[5].toUpperCase().startsWith("ERR") ? "error" : "warning",
239
+ message: biomeMatch[6].trim(),
240
+ });
241
+ continue;
242
+ }
243
+
244
+ // Generic file:line:col format
245
+ const genericMatch = line.match(/^(.+?):(\d+):(\d+):\s*(.+)$/);
246
+ if (genericMatch && !genericMatch[1].startsWith(" ")) {
247
+ errors.push({
248
+ file: genericMatch[1],
249
+ line: parseInt(genericMatch[2], 10),
250
+ column: parseInt(genericMatch[3], 10),
251
+ severity: line.toLowerCase().includes("error") ? "error" : "warning",
252
+ message: genericMatch[4].trim(),
253
+ });
254
+ }
255
+ }
256
+
257
+ return errors;
258
+ }
259
+
260
+ function getProjectTaskList(cwd: string): string | null {
261
+ const tasksDir = join(homedir(), ".claude", "tasks");
262
+ if (!existsSync(tasksDir)) return null;
263
+
264
+ const dirName = cwd.split("/").filter(Boolean).pop() || "";
265
+
266
+ try {
267
+ const lists = readdirSync(tasksDir, { withFileTypes: true })
268
+ .filter((d) => d.isDirectory())
269
+ .map((d) => d.name);
270
+
271
+ // Look for a bugfixes list for this project
272
+ const bugfixList = lists.find((list) => {
273
+ const listLower = list.toLowerCase();
274
+ const dirLower = dirName.toLowerCase();
275
+ return listLower.startsWith(dirLower) && listLower.includes("bugfix");
276
+ });
277
+
278
+ if (bugfixList) return bugfixList;
279
+
280
+ // Fall back to dev list
281
+ const devList = lists.find((list) => {
282
+ const listLower = list.toLowerCase();
283
+ const dirLower = dirName.toLowerCase();
284
+ return listLower.startsWith(dirLower) && listLower.includes("dev");
285
+ });
286
+
287
+ return devList || null;
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+
293
+ function createTask(taskListId: string, error: LintError): void {
294
+ const tasksDir = join(homedir(), ".claude", "tasks", taskListId);
295
+ mkdirSync(tasksDir, { recursive: true });
296
+
297
+ const taskId = `lint-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
298
+ const severity = error.severity === "error" ? "MEDIUM" : "LOW";
299
+ const ruleInfo = error.rule ? ` (${error.rule})` : "";
300
+
301
+ const task = {
302
+ id: taskId,
303
+ subject: `BUG: ${severity} - Fix lint ${error.severity} in ${error.file}:${error.line}`,
304
+ description: `Fix lint ${error.severity} at ${error.file}:${error.line}:${error.column}\n\n**Error:** ${error.message}${ruleInfo}\n\n**File:** ${error.file}\n**Line:** ${error.line}\n**Column:** ${error.column}${error.rule ? `\n**Rule:** ${error.rule}` : ""}\n\n**Acceptance criteria:**\n- Lint error is fixed\n- No new lint errors introduced`,
305
+ status: "pending",
306
+ createdAt: new Date().toISOString(),
307
+ metadata: {
308
+ source: "hook-checklint",
309
+ file: error.file,
310
+ line: error.line,
311
+ column: error.column,
312
+ rule: error.rule,
313
+ severity: error.severity,
314
+ },
315
+ };
316
+
317
+ const taskFile = join(tasksDir, `${taskId}.json`);
318
+ writeFileSync(taskFile, JSON.stringify(task, null, 2));
319
+ }
320
+
321
+ function getSessionName(transcriptPath: string): string | null {
322
+ if (!existsSync(transcriptPath)) return null;
323
+
324
+ try {
325
+ const content = readFileSync(transcriptPath, "utf-8");
326
+ let lastTitle: string | null = null;
327
+ let searchStart = 0;
328
+
329
+ while (true) {
330
+ const titleIndex = content.indexOf('"custom-title"', searchStart);
331
+ if (titleIndex === -1) break;
332
+
333
+ const lineStart = content.lastIndexOf("\n", titleIndex) + 1;
334
+ const lineEnd = content.indexOf("\n", titleIndex);
335
+ const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
336
+
337
+ try {
338
+ const entry = JSON.parse(line);
339
+ if (entry.type === "custom-title" && entry.customTitle) {
340
+ lastTitle = entry.customTitle;
341
+ }
342
+ } catch {
343
+ // Skip malformed lines
344
+ }
345
+
346
+ searchStart = titleIndex + 1;
347
+ }
348
+
349
+ return lastTitle;
350
+ } catch {
351
+ return null;
352
+ }
353
+ }
354
+
355
+ function approve() {
356
+ console.log(JSON.stringify({ decision: "approve" }));
357
+ process.exit(0);
358
+ }
359
+
360
+ export function run() {
361
+ const hookInput = readStdinJson();
362
+ if (!hookInput) {
363
+ approve();
364
+ return;
365
+ }
366
+
367
+ const { session_id, cwd, tool_name, tool_input, transcript_path } = hookInput;
368
+
369
+ // Only process edit tools
370
+ if (!EDIT_TOOLS.includes(tool_name)) {
371
+ approve();
372
+ return;
373
+ }
374
+
375
+ // Check repo pattern - only run for [prefix]-[name] folders
376
+ if (!isValidRepoPattern(cwd)) {
377
+ approve();
378
+ return;
379
+ }
380
+
381
+ const config = getConfig(cwd);
382
+
383
+ // Check if hook is disabled
384
+ if (config.enabled === false) {
385
+ approve();
386
+ return;
387
+ }
388
+
389
+ // Check keywords match
390
+ const sessionName = transcript_path ? getSessionName(transcript_path) : null;
391
+ const nameToCheck = sessionName || config.taskListId || "";
392
+ const keywords = config.keywords || ["dev"];
393
+
394
+ const matchesKeyword = keywords.some((keyword) =>
395
+ nameToCheck.toLowerCase().includes(keyword.toLowerCase())
396
+ );
397
+
398
+ // If keywords are configured and we have a session name, check for match
399
+ if (keywords.length > 0 && nameToCheck && !matchesKeyword) {
400
+ approve();
401
+ return;
402
+ }
403
+
404
+ // Get edited file path
405
+ const filePath = (tool_input.file_path || tool_input.notebook_path) as string | undefined;
406
+ if (!filePath) {
407
+ approve();
408
+ return;
409
+ }
410
+
411
+ // Update session state
412
+ const state = getSessionState(session_id);
413
+ state.editCount++;
414
+
415
+ if (!state.editedFiles.includes(filePath)) {
416
+ state.editedFiles.push(filePath);
417
+ }
418
+
419
+ const threshold = Math.min(7, Math.max(3, config.editThreshold || 3));
420
+
421
+ // Check if we should run lint
422
+ if (state.editCount >= threshold) {
423
+ // Detect or use configured lint command
424
+ const lintCommand = config.lintCommand || detectLintCommand(cwd);
425
+
426
+ // Validate lint command against whitelist to prevent command injection
427
+ if (lintCommand && isValidLintCommand(lintCommand)) {
428
+ const { success, output } = runLint(cwd, lintCommand);
429
+
430
+ if (!success) {
431
+ const errors = parseLintOutput(output);
432
+
433
+ // Filter to errors in files we edited (optional - could check all)
434
+ const relevantErrors = errors.filter((e) =>
435
+ state.editedFiles.some((f) => f.endsWith(e.file) || e.file.endsWith(f.split("/").pop() || ""))
436
+ );
437
+
438
+ // Create tasks for lint errors if enabled
439
+ if (config.createTasks !== false && relevantErrors.length > 0) {
440
+ const taskListId = config.taskListId || getProjectTaskList(cwd);
441
+
442
+ if (taskListId) {
443
+ // Limit to first 5 errors to avoid task spam
444
+ const errorsToReport = relevantErrors.slice(0, 5);
445
+ for (const error of errorsToReport) {
446
+ createTask(taskListId, error);
447
+ }
448
+
449
+ // Log summary
450
+ console.error(
451
+ `[hook-checklint] Created ${errorsToReport.length} task(s) for lint errors in "${taskListId}"`
452
+ );
453
+ if (relevantErrors.length > 5) {
454
+ console.error(`[hook-checklint] (${relevantErrors.length - 5} more errors not reported)`);
455
+ }
456
+ }
457
+ }
458
+ }
459
+ }
460
+
461
+ // Reset counter after lint run
462
+ state.editCount = 0;
463
+ state.lastLintRun = Date.now();
464
+ }
465
+
466
+ saveSessionState(session_id, state);
467
+ approve();
468
+ }
469
+
470
+ // Allow direct execution
471
+ if (import.meta.main) {
472
+ run();
473
+ }
@@ -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,23 @@
1
+ # CLAUDE.md
2
+
3
+ ## hook-checkpoint
4
+
5
+ A PreToolUse hook that creates shadow git snapshots before file modifications.
6
+
7
+ ### Key Files
8
+
9
+ | File | Purpose |
10
+ |------|---------|
11
+ | `src/hook.ts` | Main hook logic — reads stdin, creates checkpoints |
12
+ | `src/cli.ts` | CLI — install/uninstall/status/list/restore |
13
+
14
+ ### Hook Events
15
+
16
+ - **PreToolUse** (matcher: `Write|Edit|NotebookEdit`)
17
+
18
+ ### Behavior
19
+
20
+ - Creates `.claude-checkpoints/` shadow git repo in project root
21
+ - Copies original files before modification and commits them
22
+ - Never blocks operations — checkpoint failures are logged but ignored
23
+ - Auto-adds `.claude-checkpoints/` to `.gitignore`
@@ -0,0 +1,37 @@
1
+ # hook-checkpoint
2
+
3
+ Claude Code hook that creates shadow git snapshots before file modifications for easy rollback.
4
+
5
+ ## Overview
6
+
7
+ Before any `Write`, `Edit`, or `NotebookEdit` tool execution, this hook copies the original file into a shadow git repository (`.claude-checkpoints/`). This gives you a full history of every file before Claude modified it, without cluttering your main git history.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun install -g @hasnaxyz/hook-checkpoint
13
+ hook-checkpoint install
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ hook-checkpoint install # Install to Claude Code settings
20
+ hook-checkpoint uninstall # Remove from Claude Code settings
21
+ hook-checkpoint status # Check installation status
22
+ hook-checkpoint list # Show recent checkpoints
23
+ hook-checkpoint restore <ref> # Restore files from a checkpoint
24
+ ```
25
+
26
+ ## How It Works
27
+
28
+ 1. Hook intercepts `Write`/`Edit`/`NotebookEdit` calls (PreToolUse)
29
+ 2. Copies the original file into `.claude-checkpoints/files/`
30
+ 3. Commits to a shadow git repo with metadata
31
+ 4. Always approves the operation (non-blocking)
32
+
33
+ The `.claude-checkpoints/` directory is automatically added to `.gitignore`.
34
+
35
+ ## License
36
+
37
+ MIT
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@hasnaxyz/hook-checkpoint",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that creates shadow git snapshots before file modifications for easy rollback",
5
+ "type": "module",
6
+ "bin": {
7
+ "hook-checkpoint": "./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
+ "checkpoint",
33
+ "git",
34
+ "snapshot",
35
+ "rollback",
36
+ "safety",
37
+ "cli"
38
+ ],
39
+ "author": "Hasna",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/hasnaxyz/hook-checkpoint.git"
44
+ },
45
+ "publishConfig": {
46
+ "access": "restricted",
47
+ "registry": "https://registry.npmjs.org/"
48
+ },
49
+ "engines": {
50
+ "node": ">=18",
51
+ "bun": ">=1.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/bun": "^1.3.8",
55
+ "@types/node": "^20",
56
+ "typescript": "^5.0.0"
57
+ }
58
+ }