@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,218 @@
1
+ #!/usr/bin/env bun
2
+ import React from "react";
3
+ import { render } from "ink";
4
+ import { Command } from "commander";
5
+ import chalk from "chalk";
6
+ import { App } from "./components/App.js";
7
+ import {
8
+ HOOKS,
9
+ CATEGORIES,
10
+ getHooksByCategory,
11
+ searchHooks,
12
+ getHook,
13
+ } from "../lib/registry.js";
14
+ import {
15
+ installHook,
16
+ getInstalledHooks,
17
+ getRegisteredHooks,
18
+ removeHook,
19
+ } from "../lib/installer.js";
20
+
21
+ const program = new Command();
22
+
23
+ program
24
+ .name("hooks")
25
+ .description("Install Claude Code hooks for your project")
26
+ .version("0.0.1");
27
+
28
+ // Interactive mode (default)
29
+ program
30
+ .command("interactive", { isDefault: true })
31
+ .alias("i")
32
+ .description("Interactive hook browser")
33
+ .action(() => {
34
+ render(<App />);
35
+ });
36
+
37
+ // Install command
38
+ program
39
+ .command("install")
40
+ .alias("add")
41
+ .argument("[hooks...]", "Hooks to install")
42
+ .option("-o, --overwrite", "Overwrite existing hooks", false)
43
+ .description("Install one or more hooks")
44
+ .action((hooks: string[], options) => {
45
+ if (hooks.length === 0) {
46
+ render(<App />);
47
+ return;
48
+ }
49
+
50
+ console.log(chalk.bold("\nInstalling hooks...\n"));
51
+
52
+ for (const name of hooks) {
53
+ const result = installHook(name, { overwrite: options.overwrite });
54
+ if (result.success) {
55
+ const meta = getHook(name);
56
+ console.log(chalk.green(`✓ ${name}`));
57
+ if (meta) {
58
+ console.log(
59
+ chalk.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} → hook-${name}`)
60
+ );
61
+ }
62
+ } else {
63
+ console.log(chalk.red(`✗ ${name}: ${result.error}`));
64
+ }
65
+ }
66
+
67
+ console.log(chalk.dim("\nHooks installed to .hooks/ and registered in ~/.claude/settings.json"));
68
+ });
69
+
70
+ // List command
71
+ program
72
+ .command("list")
73
+ .alias("ls")
74
+ .option("-c, --category <category>", "Filter by category")
75
+ .option("-a, --all", "Show all available hooks", false)
76
+ .option("-i, --installed", "Show only installed hooks", false)
77
+ .option("-r, --registered", "Show hooks registered in Claude settings", false)
78
+ .description("List available or installed hooks")
79
+ .action((options) => {
80
+ if (options.registered) {
81
+ const registered = getRegisteredHooks();
82
+ if (registered.length === 0) {
83
+ console.log(chalk.dim("No hooks registered in Claude settings"));
84
+ return;
85
+ }
86
+ console.log(chalk.bold(`\nRegistered hooks (${registered.length}):\n`));
87
+ for (const name of registered) {
88
+ const meta = getHook(name);
89
+ console.log(
90
+ ` ${chalk.cyan(name)} ${chalk.dim(`[${meta?.event || "unknown"}]`)} - ${meta?.description || ""}`
91
+ );
92
+ }
93
+ return;
94
+ }
95
+
96
+ if (options.installed) {
97
+ const installed = getInstalledHooks();
98
+ if (installed.length === 0) {
99
+ console.log(chalk.dim("No hooks installed"));
100
+ return;
101
+ }
102
+ console.log(chalk.bold(`\nInstalled hooks (${installed.length}):\n`));
103
+ for (const name of installed) {
104
+ console.log(` ${name}`);
105
+ }
106
+ return;
107
+ }
108
+
109
+ if (options.category) {
110
+ const category = CATEGORIES.find(
111
+ (c) => c.toLowerCase() === options.category.toLowerCase()
112
+ );
113
+ if (!category) {
114
+ console.log(chalk.red(`Unknown category: ${options.category}`));
115
+ console.log(chalk.dim(`Available: ${CATEGORIES.join(", ")}`));
116
+ return;
117
+ }
118
+ const hooks = getHooksByCategory(category);
119
+ console.log(chalk.bold(`\n${category} (${hooks.length}):\n`));
120
+ for (const h of hooks) {
121
+ console.log(
122
+ ` ${chalk.cyan(h.name)} ${chalk.dim(`[${h.event}]`)} - ${h.description}`
123
+ );
124
+ }
125
+ return;
126
+ }
127
+
128
+ // Show all by category
129
+ console.log(chalk.bold(`\nAvailable hooks (${HOOKS.length}):\n`));
130
+ for (const category of CATEGORIES) {
131
+ const hooks = getHooksByCategory(category);
132
+ console.log(chalk.bold(`${category} (${hooks.length}):`));
133
+ for (const h of hooks) {
134
+ console.log(
135
+ ` ${chalk.cyan(h.name)} ${chalk.dim(`[${h.event}]`)} - ${h.description}`
136
+ );
137
+ }
138
+ console.log();
139
+ }
140
+ });
141
+
142
+ // Search command
143
+ program
144
+ .command("search")
145
+ .argument("<query>", "Search term")
146
+ .description("Search for hooks")
147
+ .action((query: string) => {
148
+ const results = searchHooks(query);
149
+ if (results.length === 0) {
150
+ console.log(chalk.dim(`No hooks found for "${query}"`));
151
+ return;
152
+ }
153
+ console.log(chalk.bold(`\nFound ${results.length} hook(s):\n`));
154
+ for (const h of results) {
155
+ console.log(
156
+ ` ${chalk.cyan(h.name)} ${chalk.dim(`[${h.event}] [${h.category}]`)}`
157
+ );
158
+ console.log(` ${h.description}`);
159
+ }
160
+ });
161
+
162
+ // Remove command
163
+ program
164
+ .command("remove")
165
+ .alias("rm")
166
+ .argument("<hook>", "Hook to remove")
167
+ .description("Remove an installed hook")
168
+ .action((hook: string) => {
169
+ const removed = removeHook(hook);
170
+ if (removed) {
171
+ console.log(chalk.green(`✓ Removed ${hook} (unregistered from Claude settings)`));
172
+ } else {
173
+ console.log(chalk.red(`✗ ${hook} is not installed`));
174
+ }
175
+ });
176
+
177
+ // Categories command
178
+ program
179
+ .command("categories")
180
+ .description("List all categories")
181
+ .action(() => {
182
+ console.log(chalk.bold("\nCategories:\n"));
183
+ for (const category of CATEGORIES) {
184
+ const count = getHooksByCategory(category).length;
185
+ console.log(` ${category} (${count})`);
186
+ }
187
+ });
188
+
189
+ // Info command
190
+ program
191
+ .command("info")
192
+ .argument("<hook>", "Hook name")
193
+ .description("Show detailed info about a hook")
194
+ .action((hook: string) => {
195
+ const meta = getHook(hook);
196
+ if (!meta) {
197
+ console.log(chalk.red(`Hook '${hook}' not found`));
198
+ return;
199
+ }
200
+ console.log(chalk.bold(`\n${meta.displayName}\n`));
201
+ console.log(` ${meta.description}`);
202
+ console.log();
203
+ console.log(` ${chalk.dim("Category:")} ${meta.category}`);
204
+ console.log(` ${chalk.dim("Event:")} ${meta.event}`);
205
+ console.log(` ${chalk.dim("Matcher:")} ${meta.matcher || "(none)"}`);
206
+ console.log(` ${chalk.dim("Tags:")} ${meta.tags.join(", ")}`);
207
+ console.log(` ${chalk.dim("Package:")} hook-${meta.name}`);
208
+ console.log();
209
+
210
+ const registered = getRegisteredHooks();
211
+ if (registered.includes(meta.name)) {
212
+ console.log(chalk.green(" ● Registered in Claude settings"));
213
+ } else {
214
+ console.log(chalk.dim(" ○ Not registered"));
215
+ }
216
+ });
217
+
218
+ program.parse();
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @hasna/hooks - Open source Claude Code hooks library
3
+ *
4
+ * Install hooks with a single command:
5
+ * npx @hasna/hooks install gitguard branchprotect
6
+ *
7
+ * Or use the interactive CLI:
8
+ * npx @hasna/hooks
9
+ */
10
+
11
+ export {
12
+ HOOKS,
13
+ CATEGORIES,
14
+ getHook,
15
+ getHooksByCategory,
16
+ searchHooks,
17
+ type HookMeta,
18
+ type Category,
19
+ } from "./lib/registry.js";
20
+
21
+ export {
22
+ installHook,
23
+ installHooks,
24
+ getInstalledHooks,
25
+ getRegisteredHooks,
26
+ removeHook,
27
+ hookExists,
28
+ getHookPath,
29
+ type InstallResult,
30
+ type InstallOptions,
31
+ } from "./lib/installer.js";
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Hook installer - handles copying hooks to user projects and configuring settings
3
+ */
4
+
5
+ import { existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
6
+ import { join, dirname } from "path";
7
+ import { homedir } from "os";
8
+ import { fileURLToPath } from "url";
9
+ import { getHook } from "./registry.js";
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const HOOKS_DIR = join(__dirname, "..", "..", "hooks");
13
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
14
+
15
+ export interface InstallResult {
16
+ hook: string;
17
+ success: boolean;
18
+ error?: string;
19
+ path?: string;
20
+ }
21
+
22
+ export interface InstallOptions {
23
+ targetDir?: string;
24
+ overwrite?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Get the path to a hook in the package
29
+ */
30
+ export function getHookPath(name: string): string {
31
+ const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
32
+ return join(HOOKS_DIR, hookName);
33
+ }
34
+
35
+ /**
36
+ * Check if a hook exists in the package
37
+ */
38
+ export function hookExists(name: string): boolean {
39
+ return existsSync(getHookPath(name));
40
+ }
41
+
42
+ /**
43
+ * Read Claude settings.json
44
+ */
45
+ function readSettings(): Record<string, any> {
46
+ try {
47
+ if (existsSync(SETTINGS_PATH)) {
48
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
49
+ }
50
+ } catch {}
51
+ return {};
52
+ }
53
+
54
+ /**
55
+ * Write Claude settings.json
56
+ */
57
+ function writeSettings(settings: Record<string, any>): void {
58
+ const dir = dirname(SETTINGS_PATH);
59
+ if (!existsSync(dir)) {
60
+ mkdirSync(dir, { recursive: true });
61
+ }
62
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
63
+ }
64
+
65
+ /**
66
+ * Install a single hook to the target directory and register in settings
67
+ */
68
+ export function installHook(
69
+ name: string,
70
+ options: InstallOptions = {}
71
+ ): InstallResult {
72
+ const { targetDir = process.cwd(), overwrite = false } = options;
73
+
74
+ const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
75
+ const shortName = hookName.replace("hook-", "");
76
+ const sourcePath = getHookPath(name);
77
+ const destDir = join(targetDir, ".hooks");
78
+ const destPath = join(destDir, hookName);
79
+
80
+ // Check if hook exists in package
81
+ if (!existsSync(sourcePath)) {
82
+ return {
83
+ hook: shortName,
84
+ success: false,
85
+ error: `Hook '${shortName}' not found`,
86
+ };
87
+ }
88
+
89
+ // Check if already installed
90
+ if (existsSync(destPath) && !overwrite) {
91
+ return {
92
+ hook: shortName,
93
+ success: false,
94
+ error: `Already installed. Use --overwrite to replace.`,
95
+ path: destPath,
96
+ };
97
+ }
98
+
99
+ try {
100
+ // Ensure .hooks directory exists
101
+ if (!existsSync(destDir)) {
102
+ mkdirSync(destDir, { recursive: true });
103
+ }
104
+
105
+ // Copy hook
106
+ cpSync(sourcePath, destPath, { recursive: true });
107
+
108
+ // Register hook in Claude settings
109
+ registerHookInSettings(shortName);
110
+
111
+ // Update .hooks/index.ts
112
+ updateHooksIndex(destDir);
113
+
114
+ return {
115
+ hook: shortName,
116
+ success: true,
117
+ path: destPath,
118
+ };
119
+ } catch (error) {
120
+ return {
121
+ hook: shortName,
122
+ success: false,
123
+ error: error instanceof Error ? error.message : "Unknown error",
124
+ };
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Register a hook in ~/.claude/settings.json
130
+ */
131
+ function registerHookInSettings(name: string): void {
132
+ const meta = getHook(name);
133
+ if (!meta) return;
134
+
135
+ const settings = readSettings();
136
+ if (!settings.hooks) settings.hooks = {};
137
+
138
+ const eventKey = meta.event;
139
+ if (!settings.hooks[eventKey]) settings.hooks[eventKey] = [];
140
+
141
+ // Check if already registered
142
+ const hookCommand = `hook-${name}`;
143
+ const existing = settings.hooks[eventKey].find((entry: any) =>
144
+ entry.hooks?.some((h: any) => h.command?.includes(hookCommand))
145
+ );
146
+ if (existing) return;
147
+
148
+ const entry: Record<string, any> = {
149
+ hooks: [{ type: "command", command: hookCommand }],
150
+ };
151
+ if (meta.matcher) {
152
+ entry.matcher = meta.matcher;
153
+ }
154
+
155
+ settings.hooks[eventKey].push(entry);
156
+ writeSettings(settings);
157
+ }
158
+
159
+ /**
160
+ * Unregister a hook from ~/.claude/settings.json
161
+ */
162
+ function unregisterHookFromSettings(name: string): void {
163
+ const meta = getHook(name);
164
+ if (!meta) return;
165
+
166
+ const settings = readSettings();
167
+ if (!settings.hooks) return;
168
+
169
+ const eventKey = meta.event;
170
+ if (!settings.hooks[eventKey]) return;
171
+
172
+ const hookCommand = `hook-${name}`;
173
+ settings.hooks[eventKey] = settings.hooks[eventKey].filter(
174
+ (entry: any) =>
175
+ !entry.hooks?.some((h: any) => h.command?.includes(hookCommand))
176
+ );
177
+
178
+ // Clean up empty arrays
179
+ if (settings.hooks[eventKey].length === 0) {
180
+ delete settings.hooks[eventKey];
181
+ }
182
+ if (Object.keys(settings.hooks).length === 0) {
183
+ delete settings.hooks;
184
+ }
185
+
186
+ writeSettings(settings);
187
+ }
188
+
189
+ /**
190
+ * Install multiple hooks
191
+ */
192
+ export function installHooks(
193
+ names: string[],
194
+ options: InstallOptions = {}
195
+ ): InstallResult[] {
196
+ return names.map((name) => installHook(name, options));
197
+ }
198
+
199
+ /**
200
+ * Update the .hooks/index.ts file to export all installed hooks
201
+ */
202
+ function updateHooksIndex(hooksDir: string): void {
203
+ const indexPath = join(hooksDir, "index.ts");
204
+ const { readdirSync } = require("fs");
205
+ const hooks = readdirSync(hooksDir).filter(
206
+ (f: string) => f.startsWith("hook-") && !f.includes(".")
207
+ );
208
+
209
+ const exports = hooks
210
+ .map((h: string) => {
211
+ const name = h.replace("hook-", "");
212
+ return `export * as ${name} from './${h}/src/index.js';`;
213
+ })
214
+ .join("\n");
215
+
216
+ const content = `/**
217
+ * Auto-generated index of installed hooks
218
+ * Do not edit manually - run 'hooks install' to update
219
+ */
220
+
221
+ ${exports}
222
+ `;
223
+
224
+ writeFileSync(indexPath, content);
225
+ }
226
+
227
+ /**
228
+ * Get list of installed hooks in a directory
229
+ */
230
+ export function getInstalledHooks(targetDir: string = process.cwd()): string[] {
231
+ const hooksDir = join(targetDir, ".hooks");
232
+
233
+ if (!existsSync(hooksDir)) {
234
+ return [];
235
+ }
236
+
237
+ const { readdirSync, statSync } = require("fs");
238
+ return readdirSync(hooksDir)
239
+ .filter((f: string) => {
240
+ const fullPath = join(hooksDir, f);
241
+ return f.startsWith("hook-") && statSync(fullPath).isDirectory();
242
+ })
243
+ .map((f: string) => f.replace("hook-", ""));
244
+ }
245
+
246
+ /**
247
+ * Check which hooks are registered in Claude settings
248
+ */
249
+ export function getRegisteredHooks(): string[] {
250
+ const settings = readSettings();
251
+ if (!settings.hooks) return [];
252
+
253
+ const registered: string[] = [];
254
+ for (const eventKey of Object.keys(settings.hooks)) {
255
+ for (const entry of settings.hooks[eventKey]) {
256
+ for (const hook of entry.hooks || []) {
257
+ const match = hook.command?.match(/hook-(\w+)/);
258
+ if (match) {
259
+ registered.push(match[1]);
260
+ }
261
+ }
262
+ }
263
+ }
264
+ return [...new Set(registered)];
265
+ }
266
+
267
+ /**
268
+ * Remove an installed hook
269
+ */
270
+ export function removeHook(
271
+ name: string,
272
+ targetDir: string = process.cwd()
273
+ ): boolean {
274
+ const { rmSync } = require("fs");
275
+ const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
276
+ const shortName = hookName.replace("hook-", "");
277
+ const hooksDir = join(targetDir, ".hooks");
278
+ const hookPath = join(hooksDir, hookName);
279
+
280
+ if (!existsSync(hookPath)) {
281
+ return false;
282
+ }
283
+
284
+ rmSync(hookPath, { recursive: true });
285
+ unregisterHookFromSettings(shortName);
286
+ updateHooksIndex(hooksDir);
287
+ return true;
288
+ }