@hasna/hooks 0.0.5 → 0.0.6

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/bin/index.js CHANGED
@@ -3521,7 +3521,6 @@ var {
3521
3521
  import chalk2 from "chalk";
3522
3522
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
3523
3523
  import { join as join2 } from "path";
3524
- import { homedir as homedir2 } from "os";
3525
3524
 
3526
3525
  // src/cli/components/App.tsx
3527
3526
  import { useState as useState7 } from "react";
@@ -4522,71 +4521,57 @@ function Spinner({ type = "dots" }) {
4522
4521
  var build_default2 = Spinner;
4523
4522
 
4524
4523
  // src/lib/installer.ts
4525
- import { existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4524
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4526
4525
  import { join, dirname } from "path";
4527
4526
  import { homedir } from "os";
4528
4527
  import { fileURLToPath } from "url";
4529
4528
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
4530
4529
  var HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
4531
- var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
4530
+ function getSettingsPath(scope = "global") {
4531
+ if (scope === "project") {
4532
+ return join(process.cwd(), ".claude", "settings.json");
4533
+ }
4534
+ return join(homedir(), ".claude", "settings.json");
4535
+ }
4532
4536
  function getHookPath(name) {
4533
4537
  const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
4534
4538
  return join(HOOKS_DIR, hookName);
4535
4539
  }
4536
- function readSettings() {
4540
+ function hookExists(name) {
4541
+ return existsSync(getHookPath(name));
4542
+ }
4543
+ function readSettings(scope = "global") {
4544
+ const path = getSettingsPath(scope);
4537
4545
  try {
4538
- if (existsSync(SETTINGS_PATH)) {
4539
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
4546
+ if (existsSync(path)) {
4547
+ return JSON.parse(readFileSync(path, "utf-8"));
4540
4548
  }
4541
4549
  } catch {}
4542
4550
  return {};
4543
4551
  }
4544
- function writeSettings(settings) {
4545
- const dir = dirname(SETTINGS_PATH);
4552
+ function writeSettings(settings, scope = "global") {
4553
+ const path = getSettingsPath(scope);
4554
+ const dir = dirname(path);
4546
4555
  if (!existsSync(dir)) {
4547
4556
  mkdirSync(dir, { recursive: true });
4548
4557
  }
4549
- writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + `
4558
+ writeFileSync(path, JSON.stringify(settings, null, 2) + `
4550
4559
  `);
4551
4560
  }
4552
4561
  function installHook(name, options = {}) {
4553
- const { targetDir = process.cwd(), overwrite = false } = options;
4562
+ const { scope = "global", overwrite = false } = options;
4554
4563
  const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
4555
4564
  const shortName = hookName.replace("hook-", "");
4556
- const sourcePath = getHookPath(name);
4557
- const destDir = join(targetDir, ".hooks");
4558
- const destPath = join(destDir, hookName);
4559
- if (!existsSync(sourcePath)) {
4560
- return {
4561
- hook: shortName,
4562
- success: false,
4563
- error: `Hook '${shortName}' not found`
4564
- };
4565
+ if (!hookExists(shortName)) {
4566
+ return { hook: shortName, success: false, error: `Hook '${shortName}' not found` };
4565
4567
  }
4566
- if (existsSync(destPath) && !overwrite) {
4567
- return {
4568
- hook: shortName,
4569
- success: false,
4570
- error: `Already installed. Use --overwrite to replace.`,
4571
- path: destPath
4572
- };
4568
+ const registered = getRegisteredHooks(scope);
4569
+ if (registered.includes(shortName) && !overwrite) {
4570
+ return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope };
4573
4571
  }
4574
4572
  try {
4575
- if (!existsSync(destDir)) {
4576
- mkdirSync(destDir, { recursive: true });
4577
- }
4578
- if (overwrite && existsSync(destPath)) {
4579
- const { rmSync } = __require("fs");
4580
- rmSync(destPath, { recursive: true, force: true });
4581
- }
4582
- cpSync(sourcePath, destPath, { recursive: true });
4583
- registerHookInSettings(shortName);
4584
- updateHooksIndex(destDir);
4585
- return {
4586
- hook: shortName,
4587
- success: true,
4588
- path: destPath
4589
- };
4573
+ registerHook(shortName, scope);
4574
+ return { hook: shortName, success: true, scope };
4590
4575
  } catch (error) {
4591
4576
  return {
4592
4577
  hook: shortName,
@@ -4595,20 +4580,18 @@ function installHook(name, options = {}) {
4595
4580
  };
4596
4581
  }
4597
4582
  }
4598
- function registerHookInSettings(name) {
4583
+ function registerHook(name, scope = "global") {
4599
4584
  const meta = getHook(name);
4600
4585
  if (!meta)
4601
4586
  return;
4602
- const settings = readSettings();
4587
+ const settings = readSettings(scope);
4603
4588
  if (!settings.hooks)
4604
4589
  settings.hooks = {};
4605
4590
  const eventKey = meta.event;
4606
4591
  if (!settings.hooks[eventKey])
4607
4592
  settings.hooks[eventKey] = [];
4608
- const hookCommand = `hook-${name}`;
4609
- const existing = settings.hooks[eventKey].find((entry2) => entry2.hooks?.some((h) => h.command?.includes(hookCommand)));
4610
- if (existing)
4611
- return;
4593
+ const hookCommand = `hooks run ${name}`;
4594
+ settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry2) => !entry2.hooks?.some((h) => h.command === hookCommand));
4612
4595
  const entry = {
4613
4596
  hooks: [{ type: "command", command: hookCommand }]
4614
4597
  };
@@ -4616,66 +4599,39 @@ function registerHookInSettings(name) {
4616
4599
  entry.matcher = meta.matcher;
4617
4600
  }
4618
4601
  settings.hooks[eventKey].push(entry);
4619
- writeSettings(settings);
4602
+ writeSettings(settings, scope);
4620
4603
  }
4621
- function unregisterHookFromSettings(name) {
4604
+ function unregisterHook(name, scope = "global") {
4622
4605
  const meta = getHook(name);
4623
4606
  if (!meta)
4624
4607
  return;
4625
- const settings = readSettings();
4608
+ const settings = readSettings(scope);
4626
4609
  if (!settings.hooks)
4627
4610
  return;
4628
4611
  const eventKey = meta.event;
4629
4612
  if (!settings.hooks[eventKey])
4630
4613
  return;
4631
- const hookCommand = `hook-${name}`;
4632
- settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command?.includes(hookCommand)));
4614
+ const hookCommand = `hooks run ${name}`;
4615
+ settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command === hookCommand));
4633
4616
  if (settings.hooks[eventKey].length === 0) {
4634
4617
  delete settings.hooks[eventKey];
4635
4618
  }
4636
4619
  if (Object.keys(settings.hooks).length === 0) {
4637
4620
  delete settings.hooks;
4638
4621
  }
4639
- writeSettings(settings);
4640
- }
4641
- function updateHooksIndex(hooksDir) {
4642
- const indexPath = join(hooksDir, "index.ts");
4643
- const { readdirSync } = __require("fs");
4644
- const hooks = readdirSync(hooksDir).filter((f) => f.startsWith("hook-") && !f.includes("."));
4645
- const exports = hooks.map((h) => {
4646
- const name = h.replace("hook-", "");
4647
- return `export * as ${name} from './${h}/src/index.js';`;
4648
- }).join(`
4649
- `);
4650
- const content = `/**
4651
- * Auto-generated index of installed hooks
4652
- * Do not edit manually - run 'hooks install' to update
4653
- */
4654
-
4655
- ${exports}
4656
- `;
4657
- writeFileSync(indexPath, content);
4658
- }
4659
- function getInstalledHooks(targetDir = process.cwd()) {
4660
- const hooksDir = join(targetDir, ".hooks");
4661
- if (!existsSync(hooksDir)) {
4662
- return [];
4663
- }
4664
- const { readdirSync, statSync } = __require("fs");
4665
- return readdirSync(hooksDir).filter((f) => {
4666
- const fullPath = join(hooksDir, f);
4667
- return f.startsWith("hook-") && statSync(fullPath).isDirectory();
4668
- }).map((f) => f.replace("hook-", ""));
4622
+ writeSettings(settings, scope);
4669
4623
  }
4670
- function getRegisteredHooks() {
4671
- const settings = readSettings();
4624
+ function getRegisteredHooks(scope = "global") {
4625
+ const settings = readSettings(scope);
4672
4626
  if (!settings.hooks)
4673
4627
  return [];
4674
4628
  const registered = [];
4675
4629
  for (const eventKey of Object.keys(settings.hooks)) {
4676
4630
  for (const entry of settings.hooks[eventKey]) {
4677
4631
  for (const hook of entry.hooks || []) {
4678
- const match = hook.command?.match(/hook-(\w+)/);
4632
+ const newMatch = hook.command?.match(/^hooks run (\w+)$/);
4633
+ const oldMatch = hook.command?.match(/^hook-(\w+)$/);
4634
+ const match = newMatch || oldMatch;
4679
4635
  if (match) {
4680
4636
  registered.push(match[1]);
4681
4637
  }
@@ -4684,18 +4640,17 @@ function getRegisteredHooks() {
4684
4640
  }
4685
4641
  return [...new Set(registered)];
4686
4642
  }
4687
- function removeHook(name, targetDir = process.cwd()) {
4688
- const { rmSync } = __require("fs");
4643
+ function getInstalledHooks(scope = "global") {
4644
+ return getRegisteredHooks(scope);
4645
+ }
4646
+ function removeHook(name, scope = "global") {
4689
4647
  const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
4690
4648
  const shortName = hookName.replace("hook-", "");
4691
- const hooksDir = join(targetDir, ".hooks");
4692
- const hookPath = join(hooksDir, hookName);
4693
- if (!existsSync(hookPath)) {
4649
+ const registered = getRegisteredHooks(scope);
4650
+ if (!registered.includes(shortName)) {
4694
4651
  return false;
4695
4652
  }
4696
- rmSync(hookPath, { recursive: true });
4697
- unregisterHookFromSettings(shortName);
4698
- updateHooksIndex(hooksDir);
4653
+ unregisterHook(shortName, scope);
4699
4654
  return true;
4700
4655
  }
4701
4656
 
@@ -5035,11 +4990,45 @@ function App({ initialHooks, overwrite = false }) {
5035
4990
  // src/cli/index.tsx
5036
4991
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
5037
4992
  var program2 = new Command;
5038
- program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.5");
4993
+ function resolveScope(options) {
4994
+ if (options.project)
4995
+ return "project";
4996
+ return "global";
4997
+ }
4998
+ program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.6");
5039
4999
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
5040
5000
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
5041
5001
  });
5042
- program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
5002
+ program2.command("run").argument("<hook>", "Hook to run").description("Execute a hook (called by Claude Code)").action(async (hook) => {
5003
+ const meta = getHook(hook);
5004
+ if (!meta) {
5005
+ console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
5006
+ process.exit(1);
5007
+ }
5008
+ const hookDir = getHookPath(hook);
5009
+ const hookScript = join2(hookDir, "src", "hook.ts");
5010
+ if (!existsSync2(hookScript)) {
5011
+ console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
5012
+ process.exit(1);
5013
+ }
5014
+ const stdin = await new Response(Bun.stdin.stream()).text();
5015
+ const proc = Bun.spawn(["bun", "run", hookScript], {
5016
+ stdin: new Response(stdin),
5017
+ stdout: "pipe",
5018
+ stderr: "pipe",
5019
+ env: process.env
5020
+ });
5021
+ const stdout = await new Response(proc.stdout).text();
5022
+ const stderr = await new Response(proc.stderr).text();
5023
+ const exitCode = await proc.exited;
5024
+ if (stdout)
5025
+ process.stdout.write(stdout);
5026
+ if (stderr)
5027
+ process.stderr.write(stderr);
5028
+ process.exit(exitCode);
5029
+ });
5030
+ program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
5031
+ const scope = resolveScope(options);
5043
5032
  let toInstall = hooks;
5044
5033
  if (options.all) {
5045
5034
  toInstall = HOOKS.map((h) => h.name);
@@ -5062,7 +5051,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
5062
5051
  }
5063
5052
  const results = [];
5064
5053
  for (const name of toInstall) {
5065
- const result = installHook(name, { overwrite: options.overwrite });
5054
+ const result = installHook(name, { scope, overwrite: options.overwrite });
5066
5055
  results.push(result);
5067
5056
  }
5068
5057
  if (options.json) {
@@ -5070,43 +5059,46 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
5070
5059
  installed: results.filter((r) => r.success).map((r) => r.hook),
5071
5060
  failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
5072
5061
  total: results.length,
5073
- success: results.filter((r) => r.success).length
5062
+ success: results.filter((r) => r.success).length,
5063
+ scope
5074
5064
  }));
5075
5065
  return;
5076
5066
  }
5067
+ const settingsFile = scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
5077
5068
  console.log(chalk2.bold(`
5078
- Installing hooks...
5069
+ Installing hooks (${scope})...
5079
5070
  `));
5080
5071
  for (const result of results) {
5081
5072
  if (result.success) {
5082
5073
  const meta = getHook(result.hook);
5083
5074
  console.log(chalk2.green(`\u2713 ${result.hook}`));
5084
5075
  if (meta) {
5085
- console.log(chalk2.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} \u2192 hook-${result.hook}`));
5076
+ console.log(chalk2.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} \u2192 hooks run ${result.hook}`));
5086
5077
  }
5087
5078
  } else {
5088
5079
  console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
5089
5080
  }
5090
5081
  }
5091
5082
  console.log(chalk2.dim(`
5092
- Hooks installed to .hooks/ and registered in ~/.claude/settings.json`));
5083
+ Registered in ${settingsFile}`));
5093
5084
  });
5094
- program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show hooks registered in Claude settings", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
5095
- if (options.registered) {
5096
- const registered = getRegisteredHooks();
5085
+ program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show hooks registered in Claude settings", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
5086
+ const scope = resolveScope(options);
5087
+ if (options.registered || options.installed) {
5088
+ const registered = getRegisteredHooks(scope);
5097
5089
  if (options.json) {
5098
5090
  console.log(JSON.stringify(registered.map((name) => {
5099
5091
  const meta = getHook(name);
5100
- return { name, event: meta?.event, description: meta?.description };
5092
+ return { name, event: meta?.event, version: meta?.version, description: meta?.description, scope };
5101
5093
  })));
5102
5094
  return;
5103
5095
  }
5104
5096
  if (registered.length === 0) {
5105
- console.log(chalk2.dim("No hooks registered in Claude settings"));
5097
+ console.log(chalk2.dim(`No hooks registered (${scope})`));
5106
5098
  return;
5107
5099
  }
5108
5100
  console.log(chalk2.bold(`
5109
- Registered hooks (${registered.length}):
5101
+ Registered hooks \u2014 ${scope} (${registered.length}):
5110
5102
  `));
5111
5103
  for (const name of registered) {
5112
5104
  const meta = getHook(name);
@@ -5114,27 +5106,6 @@ Registered hooks (${registered.length}):
5114
5106
  }
5115
5107
  return;
5116
5108
  }
5117
- if (options.installed) {
5118
- const installed = getInstalledHooks();
5119
- if (options.json) {
5120
- console.log(JSON.stringify(installed.map((name) => {
5121
- const meta = getHook(name);
5122
- return { name, event: meta?.event, version: meta?.version, description: meta?.description };
5123
- })));
5124
- return;
5125
- }
5126
- if (installed.length === 0) {
5127
- console.log(chalk2.dim("No hooks installed"));
5128
- return;
5129
- }
5130
- console.log(chalk2.bold(`
5131
- Installed hooks (${installed.length}):
5132
- `));
5133
- for (const name of installed) {
5134
- console.log(` ${name}`);
5135
- }
5136
- return;
5137
- }
5138
5109
  if (options.category) {
5139
5110
  const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
5140
5111
  if (!category) {
@@ -5197,16 +5168,17 @@ Found ${results.length} hook(s):
5197
5168
  console.log(` ${h.description}`);
5198
5169
  }
5199
5170
  });
5200
- program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
5201
- const removed = removeHook(hook);
5171
+ program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
5172
+ const scope = resolveScope(options);
5173
+ const removed = removeHook(hook, scope);
5202
5174
  if (options.json) {
5203
- console.log(JSON.stringify({ hook, removed }));
5175
+ console.log(JSON.stringify({ hook, removed, scope }));
5204
5176
  return;
5205
5177
  }
5206
5178
  if (removed) {
5207
- console.log(chalk2.green(`\u2713 Removed ${hook} (unregistered from Claude settings)`));
5179
+ console.log(chalk2.green(`\u2713 Removed ${hook} (${scope})`));
5208
5180
  } else {
5209
- console.log(chalk2.red(`\u2717 ${hook} is not installed`));
5181
+ console.log(chalk2.red(`\u2717 ${hook} is not installed (${scope})`));
5210
5182
  }
5211
5183
  });
5212
5184
  program2.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
@@ -5236,12 +5208,10 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
5236
5208
  }
5237
5209
  return;
5238
5210
  }
5239
- const registered = getRegisteredHooks();
5240
- const isRegistered = registered.includes(meta.name);
5241
- const installed = getInstalledHooks();
5242
- const isInstalled = installed.includes(meta.name);
5211
+ const globalInstalled = getRegisteredHooks("global").includes(meta.name);
5212
+ const projectInstalled = getRegisteredHooks("project").includes(meta.name);
5243
5213
  if (options.json) {
5244
- console.log(JSON.stringify({ ...meta, registered: isRegistered, installed: isInstalled }));
5214
+ console.log(JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }));
5245
5215
  return;
5246
5216
  }
5247
5217
  console.log(chalk2.bold(`
@@ -5254,53 +5224,50 @@ ${meta.displayName}
5254
5224
  console.log(` ${chalk2.dim("Event:")} ${meta.event}`);
5255
5225
  console.log(` ${chalk2.dim("Matcher:")} ${meta.matcher || "(none)"}`);
5256
5226
  console.log(` ${chalk2.dim("Tags:")} ${meta.tags.join(", ")}`);
5257
- console.log(` ${chalk2.dim("Package:")} hook-${meta.name}`);
5227
+ console.log(` ${chalk2.dim("Command:")} hooks run ${meta.name}`);
5258
5228
  console.log();
5259
- if (isRegistered) {
5260
- console.log(chalk2.green(" \u25CF Registered in Claude settings"));
5229
+ if (globalInstalled) {
5230
+ console.log(chalk2.green(" \u25CF Installed globally"));
5261
5231
  } else {
5262
- console.log(chalk2.dim(" \u25CB Not registered"));
5232
+ console.log(chalk2.dim(" \u25CB Not installed globally"));
5263
5233
  }
5264
- if (isInstalled) {
5265
- console.log(chalk2.green(" \u25CF Installed in .hooks/"));
5234
+ if (projectInstalled) {
5235
+ console.log(chalk2.green(" \u25CF Installed in project"));
5266
5236
  } else {
5267
- console.log(chalk2.dim(" \u25CB Not installed"));
5237
+ console.log(chalk2.dim(" \u25CB Not installed in project"));
5268
5238
  }
5269
5239
  });
5270
- program2.command("doctor").option("-j, --json", "Output as JSON", false).description("Check health of installed hooks").action((options) => {
5271
- const settingsPath = join2(homedir2(), ".claude", "settings.json");
5240
+ program2.command("doctor").option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-j, --json", "Output as JSON", false).description("Check health of installed hooks").action((options) => {
5241
+ const scope = resolveScope(options);
5242
+ const settingsPath = getSettingsPath(scope);
5272
5243
  const issues = [];
5273
5244
  const healthy = [];
5274
5245
  const settingsExist = existsSync2(settingsPath);
5275
5246
  if (!settingsExist) {
5276
- issues.push({ hook: "(settings)", issue: "~/.claude/settings.json not found", severity: "warning" });
5247
+ issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
5277
5248
  }
5278
- const installed = getInstalledHooks();
5279
- const registered = getRegisteredHooks();
5280
- for (const name of installed) {
5249
+ const registered = getRegisteredHooks(scope);
5250
+ for (const name of registered) {
5281
5251
  const meta = getHook(name);
5282
- const hookDir = join2(process.cwd(), ".hooks", `hook-${name}`);
5283
5252
  let hookHealthy = true;
5284
- const srcDir = join2(hookDir, "src");
5285
- if (!existsSync2(srcDir)) {
5286
- issues.push({ hook: name, issue: "Missing src/ directory", severity: "error" });
5287
- hookHealthy = false;
5288
- }
5289
- if (!existsSync2(join2(hookDir, "package.json"))) {
5290
- issues.push({ hook: name, issue: "Missing package.json", severity: "error" });
5253
+ if (!hookExists(name)) {
5254
+ issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
5291
5255
  hookHealthy = false;
5256
+ continue;
5292
5257
  }
5293
- if (!registered.includes(name)) {
5294
- issues.push({ hook: name, issue: "Installed but not registered in Claude settings", severity: "warning" });
5258
+ const hookDir = getHookPath(name);
5259
+ const hookScript = join2(hookDir, "src", "hook.ts");
5260
+ if (!existsSync2(hookScript)) {
5261
+ issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
5295
5262
  hookHealthy = false;
5296
5263
  }
5297
5264
  if (meta && settingsExist) {
5298
5265
  try {
5299
5266
  const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
5300
5267
  const eventHooks = settings.hooks?.[meta.event] || [];
5301
- const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command?.includes(`hook-${name}`)));
5302
- if (registered.includes(name) && !found) {
5303
- issues.push({ hook: name, issue: `Registered but not under ${meta.event} event`, severity: "error" });
5268
+ const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command === `hooks run ${name}`));
5269
+ if (!found) {
5270
+ issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
5304
5271
  hookHealthy = false;
5305
5272
  }
5306
5273
  } catch {}
@@ -5309,20 +5276,15 @@ program2.command("doctor").option("-j, --json", "Output as JSON", false).descrip
5309
5276
  healthy.push(name);
5310
5277
  }
5311
5278
  }
5312
- for (const name of registered) {
5313
- if (!installed.includes(name)) {
5314
- issues.push({ hook: name, issue: "Registered in settings but not installed in .hooks/", severity: "warning" });
5315
- }
5316
- }
5317
5279
  if (options.json) {
5318
- console.log(JSON.stringify({ healthy, issues, installed, registered }));
5280
+ console.log(JSON.stringify({ healthy, issues, registered, scope }));
5319
5281
  return;
5320
5282
  }
5321
5283
  console.log(chalk2.bold(`
5322
- Hook Health Check
5284
+ Hook Health Check (${scope})
5323
5285
  `));
5324
- if (installed.length === 0 && registered.length === 0) {
5325
- console.log(chalk2.dim(" No hooks installed or registered."));
5286
+ if (registered.length === 0) {
5287
+ console.log(chalk2.dim(" No hooks registered."));
5326
5288
  console.log(chalk2.dim(" Run: hooks install gitguard"));
5327
5289
  return;
5328
5290
  }
@@ -5345,8 +5307,9 @@ Hook Health Check
5345
5307
  }
5346
5308
  console.log();
5347
5309
  });
5348
- program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-j, --json", "Output as JSON", false).description("Update installed hooks to latest version from package").action((hooks, options) => {
5349
- const installed = getInstalledHooks();
5310
+ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version)").action((hooks, options) => {
5311
+ const scope = resolveScope(options);
5312
+ const installed = getInstalledHooks(scope);
5350
5313
  const toUpdate = hooks.length > 0 ? hooks : installed;
5351
5314
  if (toUpdate.length === 0) {
5352
5315
  if (options.json) {
@@ -5362,7 +5325,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
5362
5325
  results.push({ hook: name, success: false, error: "Not installed" });
5363
5326
  continue;
5364
5327
  }
5365
- const result = installHook(name, { overwrite: true });
5328
+ const result = installHook(name, { scope, overwrite: true });
5366
5329
  results.push(result);
5367
5330
  }
5368
5331
  if (options.json) {
@@ -5412,24 +5375,24 @@ ${meta.displayName} v${meta.version}
5412
5375
  console.log(chalk2.bold(" Configuration:"));
5413
5376
  console.log(` Event: ${meta.event}`);
5414
5377
  console.log(` Matcher: ${meta.matcher || "(all tools)"}`);
5415
- console.log(` Package: hook-${meta.name}`);
5378
+ console.log(` Command: hooks run ${meta.name}`);
5416
5379
  console.log();
5417
5380
  console.log(chalk2.bold(" Install:"));
5418
- console.log(` hooks install ${meta.name}`);
5381
+ console.log(` hooks install ${meta.name} # global`);
5382
+ console.log(` hooks install ${meta.name} --project # project only`);
5419
5383
  console.log();
5420
5384
  if (readme) {
5421
5385
  console.log(chalk2.bold(` README:
5422
5386
  `));
5423
- const lines = readme.split(`
5424
- `);
5425
- for (const line of lines) {
5387
+ for (const line of readme.split(`
5388
+ `)) {
5426
5389
  console.log(` ${line}`);
5427
5390
  }
5428
5391
  }
5429
5392
  return;
5430
5393
  }
5431
5394
  const generalDocs = {
5432
- overview: "Claude Code hooks are scripts that run at specific points in a Claude Code session.",
5395
+ overview: "Claude Code hooks are scripts that run at specific points in a Claude Code session. Install @hasna/hooks globally, then register hooks \u2014 no files are copied to your project.",
5433
5396
  events: {
5434
5397
  PreToolUse: 'Fires before a tool executes. Can block the operation by returning { "decision": "block" }.',
5435
5398
  PostToolUse: "Fires after a tool executes. Runs asynchronously, cannot block.",
@@ -5437,25 +5400,26 @@ ${meta.displayName} v${meta.version}
5437
5400
  Notification: "Fires on notification events like context compaction."
5438
5401
  },
5439
5402
  installation: {
5440
- single: "hooks install gitguard",
5441
- multiple: "hooks install gitguard checkpoint packageage",
5403
+ global: "hooks install gitguard",
5404
+ project: "hooks install gitguard --project",
5442
5405
  category: 'hooks install --category "Git Safety"',
5443
5406
  all: "hooks install --all"
5444
5407
  },
5445
5408
  management: {
5446
5409
  list: "hooks list",
5447
5410
  listInstalled: "hooks list --installed",
5448
- listRegistered: "hooks list --registered",
5449
5411
  search: "hooks search <query>",
5450
5412
  info: "hooks info <name>",
5451
5413
  remove: "hooks remove <name>",
5452
5414
  update: "hooks update",
5453
- doctor: "hooks doctor"
5415
+ doctor: "hooks doctor",
5416
+ docs: "hooks docs <name>"
5454
5417
  },
5455
- structure: {
5456
- hooksDir: ".hooks/hook-<name>/",
5457
- settings: "~/.claude/settings.json",
5458
- hookSource: ".hooks/hook-<name>/src/hook.ts"
5418
+ howItWorks: {
5419
+ install: "bun install -g @hasna/hooks",
5420
+ register: "hooks install gitguard \u2192 writes to ~/.claude/settings.json",
5421
+ execution: "Claude Code runs 'hooks run gitguard' \u2192 executes hook from global package",
5422
+ noFileCopy: "No files are copied to your project. Hooks run from the global @hasna/hooks package."
5459
5423
  }
5460
5424
  };
5461
5425
  if (options.json) {
@@ -5469,7 +5433,13 @@ ${meta.displayName} v${meta.version}
5469
5433
  `));
5470
5434
  console.log(` ${generalDocs.overview}
5471
5435
  `);
5472
- console.log(chalk2.bold(` Hook Events
5436
+ console.log(chalk2.bold(` How It Works
5437
+ `));
5438
+ for (const [label, desc] of Object.entries(generalDocs.howItWorks)) {
5439
+ console.log(` ${chalk2.dim(label + ":")} ${desc}`);
5440
+ }
5441
+ console.log(chalk2.bold(`
5442
+ Hook Events
5473
5443
  `));
5474
5444
  for (const [event, desc] of Object.entries(generalDocs.events)) {
5475
5445
  console.log(` ${chalk2.cyan(event)}`);
@@ -5488,12 +5458,6 @@ ${meta.displayName} v${meta.version}
5488
5458
  console.log(` ${chalk2.dim(label + ":")} ${cmd}`);
5489
5459
  }
5490
5460
  console.log(chalk2.bold(`
5491
- File Structure
5492
- `));
5493
- for (const [label, path] of Object.entries(generalDocs.structure)) {
5494
- console.log(` ${chalk2.dim(label + ":")} ${path}`);
5495
- }
5496
- console.log(chalk2.bold(`
5497
5461
  Hook-Specific Docs
5498
5462
  `));
5499
5463
  console.log(` hooks docs <name> View README for a specific hook`);
package/dist/index.js CHANGED
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __require = import.meta.require;
3
-
4
2
  // src/lib/registry.ts
5
3
  var CATEGORIES = [
6
4
  "Git Safety",
@@ -172,13 +170,18 @@ function getHook(name) {
172
170
  return HOOKS.find((h) => h.name === name);
173
171
  }
174
172
  // src/lib/installer.ts
175
- import { existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
173
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
176
174
  import { join, dirname } from "path";
177
175
  import { homedir } from "os";
178
176
  import { fileURLToPath } from "url";
179
177
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
180
178
  var HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
181
- var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
179
+ function getSettingsPath(scope = "global") {
180
+ if (scope === "project") {
181
+ return join(process.cwd(), ".claude", "settings.json");
182
+ }
183
+ return join(homedir(), ".claude", "settings.json");
184
+ }
182
185
  function getHookPath(name) {
183
186
  const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
184
187
  return join(HOOKS_DIR, hookName);
@@ -186,60 +189,38 @@ function getHookPath(name) {
186
189
  function hookExists(name) {
187
190
  return existsSync(getHookPath(name));
188
191
  }
189
- function readSettings() {
192
+ function readSettings(scope = "global") {
193
+ const path = getSettingsPath(scope);
190
194
  try {
191
- if (existsSync(SETTINGS_PATH)) {
192
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
195
+ if (existsSync(path)) {
196
+ return JSON.parse(readFileSync(path, "utf-8"));
193
197
  }
194
198
  } catch {}
195
199
  return {};
196
200
  }
197
- function writeSettings(settings) {
198
- const dir = dirname(SETTINGS_PATH);
201
+ function writeSettings(settings, scope = "global") {
202
+ const path = getSettingsPath(scope);
203
+ const dir = dirname(path);
199
204
  if (!existsSync(dir)) {
200
205
  mkdirSync(dir, { recursive: true });
201
206
  }
202
- writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + `
207
+ writeFileSync(path, JSON.stringify(settings, null, 2) + `
203
208
  `);
204
209
  }
205
210
  function installHook(name, options = {}) {
206
- const { targetDir = process.cwd(), overwrite = false } = options;
211
+ const { scope = "global", overwrite = false } = options;
207
212
  const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
208
213
  const shortName = hookName.replace("hook-", "");
209
- const sourcePath = getHookPath(name);
210
- const destDir = join(targetDir, ".hooks");
211
- const destPath = join(destDir, hookName);
212
- if (!existsSync(sourcePath)) {
213
- return {
214
- hook: shortName,
215
- success: false,
216
- error: `Hook '${shortName}' not found`
217
- };
214
+ if (!hookExists(shortName)) {
215
+ return { hook: shortName, success: false, error: `Hook '${shortName}' not found` };
218
216
  }
219
- if (existsSync(destPath) && !overwrite) {
220
- return {
221
- hook: shortName,
222
- success: false,
223
- error: `Already installed. Use --overwrite to replace.`,
224
- path: destPath
225
- };
217
+ const registered = getRegisteredHooks(scope);
218
+ if (registered.includes(shortName) && !overwrite) {
219
+ return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope };
226
220
  }
227
221
  try {
228
- if (!existsSync(destDir)) {
229
- mkdirSync(destDir, { recursive: true });
230
- }
231
- if (overwrite && existsSync(destPath)) {
232
- const { rmSync } = __require("fs");
233
- rmSync(destPath, { recursive: true, force: true });
234
- }
235
- cpSync(sourcePath, destPath, { recursive: true });
236
- registerHookInSettings(shortName);
237
- updateHooksIndex(destDir);
238
- return {
239
- hook: shortName,
240
- success: true,
241
- path: destPath
242
- };
222
+ registerHook(shortName, scope);
223
+ return { hook: shortName, success: true, scope };
243
224
  } catch (error) {
244
225
  return {
245
226
  hook: shortName,
@@ -248,20 +229,18 @@ function installHook(name, options = {}) {
248
229
  };
249
230
  }
250
231
  }
251
- function registerHookInSettings(name) {
232
+ function registerHook(name, scope = "global") {
252
233
  const meta = getHook(name);
253
234
  if (!meta)
254
235
  return;
255
- const settings = readSettings();
236
+ const settings = readSettings(scope);
256
237
  if (!settings.hooks)
257
238
  settings.hooks = {};
258
239
  const eventKey = meta.event;
259
240
  if (!settings.hooks[eventKey])
260
241
  settings.hooks[eventKey] = [];
261
- const hookCommand = `hook-${name}`;
262
- const existing = settings.hooks[eventKey].find((entry2) => entry2.hooks?.some((h) => h.command?.includes(hookCommand)));
263
- if (existing)
264
- return;
242
+ const hookCommand = `hooks run ${name}`;
243
+ settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry2) => !entry2.hooks?.some((h) => h.command === hookCommand));
265
244
  const entry = {
266
245
  hooks: [{ type: "command", command: hookCommand }]
267
246
  };
@@ -269,69 +248,42 @@ function registerHookInSettings(name) {
269
248
  entry.matcher = meta.matcher;
270
249
  }
271
250
  settings.hooks[eventKey].push(entry);
272
- writeSettings(settings);
251
+ writeSettings(settings, scope);
273
252
  }
274
- function unregisterHookFromSettings(name) {
253
+ function unregisterHook(name, scope = "global") {
275
254
  const meta = getHook(name);
276
255
  if (!meta)
277
256
  return;
278
- const settings = readSettings();
257
+ const settings = readSettings(scope);
279
258
  if (!settings.hooks)
280
259
  return;
281
260
  const eventKey = meta.event;
282
261
  if (!settings.hooks[eventKey])
283
262
  return;
284
- const hookCommand = `hook-${name}`;
285
- settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command?.includes(hookCommand)));
263
+ const hookCommand = `hooks run ${name}`;
264
+ settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command === hookCommand));
286
265
  if (settings.hooks[eventKey].length === 0) {
287
266
  delete settings.hooks[eventKey];
288
267
  }
289
268
  if (Object.keys(settings.hooks).length === 0) {
290
269
  delete settings.hooks;
291
270
  }
292
- writeSettings(settings);
271
+ writeSettings(settings, scope);
293
272
  }
294
273
  function installHooks(names, options = {}) {
295
274
  return names.map((name) => installHook(name, options));
296
275
  }
297
- function updateHooksIndex(hooksDir) {
298
- const indexPath = join(hooksDir, "index.ts");
299
- const { readdirSync } = __require("fs");
300
- const hooks = readdirSync(hooksDir).filter((f) => f.startsWith("hook-") && !f.includes("."));
301
- const exports = hooks.map((h) => {
302
- const name = h.replace("hook-", "");
303
- return `export * as ${name} from './${h}/src/index.js';`;
304
- }).join(`
305
- `);
306
- const content = `/**
307
- * Auto-generated index of installed hooks
308
- * Do not edit manually - run 'hooks install' to update
309
- */
310
-
311
- ${exports}
312
- `;
313
- writeFileSync(indexPath, content);
314
- }
315
- function getInstalledHooks(targetDir = process.cwd()) {
316
- const hooksDir = join(targetDir, ".hooks");
317
- if (!existsSync(hooksDir)) {
318
- return [];
319
- }
320
- const { readdirSync, statSync } = __require("fs");
321
- return readdirSync(hooksDir).filter((f) => {
322
- const fullPath = join(hooksDir, f);
323
- return f.startsWith("hook-") && statSync(fullPath).isDirectory();
324
- }).map((f) => f.replace("hook-", ""));
325
- }
326
- function getRegisteredHooks() {
327
- const settings = readSettings();
276
+ function getRegisteredHooks(scope = "global") {
277
+ const settings = readSettings(scope);
328
278
  if (!settings.hooks)
329
279
  return [];
330
280
  const registered = [];
331
281
  for (const eventKey of Object.keys(settings.hooks)) {
332
282
  for (const entry of settings.hooks[eventKey]) {
333
283
  for (const hook of entry.hooks || []) {
334
- const match = hook.command?.match(/hook-(\w+)/);
284
+ const newMatch = hook.command?.match(/^hooks run (\w+)$/);
285
+ const oldMatch = hook.command?.match(/^hook-(\w+)$/);
286
+ const match = newMatch || oldMatch;
335
287
  if (match) {
336
288
  registered.push(match[1]);
337
289
  }
@@ -340,18 +292,17 @@ function getRegisteredHooks() {
340
292
  }
341
293
  return [...new Set(registered)];
342
294
  }
343
- function removeHook(name, targetDir = process.cwd()) {
344
- const { rmSync } = __require("fs");
295
+ function getInstalledHooks(scope = "global") {
296
+ return getRegisteredHooks(scope);
297
+ }
298
+ function removeHook(name, scope = "global") {
345
299
  const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
346
300
  const shortName = hookName.replace("hook-", "");
347
- const hooksDir = join(targetDir, ".hooks");
348
- const hookPath = join(hooksDir, hookName);
349
- if (!existsSync(hookPath)) {
301
+ const registered = getRegisteredHooks(scope);
302
+ if (!registered.includes(shortName)) {
350
303
  return false;
351
304
  }
352
- rmSync(hookPath, { recursive: true });
353
- unregisterHookFromSettings(shortName);
354
- updateHooksIndex(hooksDir);
305
+ unregisterHook(shortName, scope);
355
306
  return true;
356
307
  }
357
308
  export {
@@ -360,6 +311,7 @@ export {
360
311
  installHooks,
361
312
  installHook,
362
313
  hookExists,
314
+ getSettingsPath,
363
315
  getRegisteredHooks,
364
316
  getInstalledHooks,
365
317
  getHooksByCategory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Open source Claude Code hooks library - Install hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {
package/.hooks/index.ts DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * Auto-generated index of installed hooks
3
- * Do not edit manually - run 'hooks install' to update
4
- */
5
-
6
-