@hasna/hooks 0.6.5 → 0.6.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
@@ -47,7 +47,7 @@ var __export = (target, all) => {
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = import.meta.require;
49
49
 
50
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/error.js
50
+ // node_modules/commander/lib/error.js
51
51
  var require_error = __commonJS((exports) => {
52
52
  class CommanderError extends Error {
53
53
  constructor(exitCode, code, message) {
@@ -71,7 +71,7 @@ var require_error = __commonJS((exports) => {
71
71
  exports.InvalidArgumentError = InvalidArgumentError;
72
72
  });
73
73
 
74
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/argument.js
74
+ // node_modules/commander/lib/argument.js
75
75
  var require_argument = __commonJS((exports) => {
76
76
  var { InvalidArgumentError } = require_error();
77
77
 
@@ -150,7 +150,7 @@ var require_argument = __commonJS((exports) => {
150
150
  exports.humanReadableArgName = humanReadableArgName;
151
151
  });
152
152
 
153
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/help.js
153
+ // node_modules/commander/lib/help.js
154
154
  var require_help = __commonJS((exports) => {
155
155
  var { humanReadableArgName } = require_argument();
156
156
 
@@ -399,7 +399,7 @@ var require_help = __commonJS((exports) => {
399
399
  exports.Help = Help;
400
400
  });
401
401
 
402
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/option.js
402
+ // node_modules/commander/lib/option.js
403
403
  var require_option = __commonJS((exports) => {
404
404
  var { InvalidArgumentError } = require_error();
405
405
 
@@ -550,7 +550,7 @@ var require_option = __commonJS((exports) => {
550
550
  exports.DualOptions = DualOptions;
551
551
  });
552
552
 
553
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js
553
+ // node_modules/commander/lib/suggestSimilar.js
554
554
  var require_suggestSimilar = __commonJS((exports) => {
555
555
  var maxDistance = 3;
556
556
  function editDistance(a, b) {
@@ -623,7 +623,7 @@ var require_suggestSimilar = __commonJS((exports) => {
623
623
  exports.suggestSimilar = suggestSimilar;
624
624
  });
625
625
 
626
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/command.js
626
+ // node_modules/commander/lib/command.js
627
627
  var require_command = __commonJS((exports) => {
628
628
  var EventEmitter = __require("events").EventEmitter;
629
629
  var childProcess = __require("child_process");
@@ -1866,7 +1866,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1866
1866
  exports.Command = Command;
1867
1867
  });
1868
1868
 
1869
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/index.js
1869
+ // node_modules/commander/index.js
1870
1870
  var require_commander = __commonJS((exports) => {
1871
1871
  var { Argument } = require_argument();
1872
1872
  var { Command } = require_command();
@@ -2419,7 +2419,7 @@ var init_registry = __esm(() => {
2419
2419
  ];
2420
2420
  });
2421
2421
 
2422
- // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/spinners.json
2422
+ // node_modules/cli-spinners/spinners.json
2423
2423
  var require_spinners = __commonJS((exports, module) => {
2424
2424
  module.exports = {
2425
2425
  dots: {
@@ -4045,7 +4045,7 @@ var require_spinners = __commonJS((exports, module) => {
4045
4045
  };
4046
4046
  });
4047
4047
 
4048
- // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/index.js
4048
+ // node_modules/cli-spinners/index.js
4049
4049
  var require_cli_spinners = __commonJS((exports, module) => {
4050
4050
  var spinners = Object.assign({}, require_spinners());
4051
4051
  var spinnersList = Object.keys(spinners);
@@ -4119,24 +4119,30 @@ function resolveApiUrl() {
4119
4119
  const cfg = readConfig();
4120
4120
  return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
4121
4121
  }
4122
- function resolveApiKey(flagKey) {
4123
- if (flagKey && flagKey.length > 0)
4124
- return flagKey;
4122
+ function resolveApiKey() {
4125
4123
  return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
4126
4124
  }
4127
4125
  var init_config = () => {};
4128
4126
 
4127
+ // src/lib/semver.ts
4128
+ var SEMVER_PATTERN;
4129
+ var init_semver = __esm(() => {
4130
+ SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
4131
+ });
4132
+
4129
4133
  // src/lib/manifest.ts
4130
4134
  var exports_manifest = {};
4131
4135
  __export(exports_manifest, {
4132
4136
  writeCustomHook: () => writeCustomHook,
4133
4137
  shortManifestName: () => shortManifestName,
4138
+ scriptRelFor: () => scriptRelFor,
4134
4139
  resolveScript: () => resolveScript,
4135
4140
  readCustomManifest: () => readCustomManifest,
4136
4141
  parseManifest: () => parseManifest,
4137
4142
  normalizeManifestName: () => normalizeManifestName,
4138
4143
  manifestSchema: () => manifestSchema,
4139
4144
  listCustomHooks: () => listCustomHooks,
4145
+ isInlineScript: () => isInlineScript,
4140
4146
  customHookDir: () => customHookDir,
4141
4147
  assertContained: () => assertContained,
4142
4148
  ScriptContainmentError: () => ScriptContainmentError,
@@ -4190,16 +4196,25 @@ function assertContained(target, root, raw) {
4190
4196
  assertContainedReal(root, lexical, raw);
4191
4197
  return lexical;
4192
4198
  }
4193
- function resolveScript(manifest, manifestDir) {
4194
- if (manifest.script.includes(`
4195
- `)) {
4199
+ function isInlineScript(manifest) {
4200
+ return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
4201
+ `);
4202
+ }
4203
+ function scriptRelFor(manifest) {
4204
+ if (isInlineScript(manifest)) {
4196
4205
  const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
4197
- return { path: `script${ext}`, content: manifest.script };
4206
+ return `script${ext}`;
4198
4207
  }
4199
4208
  const rel = normalize(manifest.script);
4200
4209
  if (isAbsolute(rel)) {
4201
4210
  throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
4202
4211
  }
4212
+ return rel;
4213
+ }
4214
+ function resolveScript(manifest, manifestDir) {
4215
+ if (isInlineScript(manifest))
4216
+ return { path: scriptRelFor(manifest), content: manifest.script };
4217
+ const rel = scriptRelFor(manifest);
4203
4218
  const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
4204
4219
  if (!existsSync3(resolved)) {
4205
4220
  throw new Error(`manifest script file not found: ${resolved}`);
@@ -4217,8 +4232,8 @@ function readCustomManifest(name) {
4217
4232
  try {
4218
4233
  const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
4219
4234
  const script = resolveScript(manifest, dir);
4220
- return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline: manifest.script.includes(`
4221
- `) };
4235
+ const scriptIsInline = isInlineScript(manifest);
4236
+ return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline };
4222
4237
  } catch (err) {
4223
4238
  if (err instanceof ScriptContainmentError)
4224
4239
  throw err;
@@ -4264,13 +4279,15 @@ function readdirSafe(dir) {
4264
4279
  var HOOK_NAME_RE, manifestSchema, ScriptContainmentError;
4265
4280
  var init_manifest = __esm(() => {
4266
4281
  init_config();
4282
+ init_semver();
4267
4283
  HOOK_NAME_RE = /^[\w-]+$/;
4268
4284
  manifestSchema = z.object({
4269
4285
  name: z.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
4270
- version: z.string().regex(/^\d+\.\d+\.\d+/, "version must be semver (e.g. 1.2.3)"),
4286
+ version: z.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
4271
4287
  description: z.string().optional(),
4272
4288
  events: z.array(z.string()).min(1, "at least one event is required"),
4273
4289
  script: z.string().min(1, "script must name a relative path or inline content"),
4290
+ script_kind: z.enum(["inline", "file"]).optional(),
4274
4291
  args: z.array(z.string()).optional(),
4275
4292
  timeout_ms: z.number().int().positive().optional()
4276
4293
  });
@@ -4799,11 +4816,13 @@ __export(exports_store, {
4799
4816
  listHookRecords: () => listHookRecords,
4800
4817
  getPinnedHook: () => getPinnedHook,
4801
4818
  getHookRecord: () => getHookRecord,
4802
- checkScriptHash: () => checkScriptHash
4819
+ checkScriptHash: () => checkScriptHash,
4820
+ LockFileError: () => LockFileError
4803
4821
  });
4804
4822
  import { createHash } from "crypto";
4805
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4823
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync } from "fs";
4806
4824
  import { readFile as readFile2 } from "fs/promises";
4825
+ import { randomBytes } from "crypto";
4807
4826
  function sha256Of(input) {
4808
4827
  return createHash("sha256").update(input).digest("hex");
4809
4828
  }
@@ -4846,16 +4865,39 @@ function removeHookRecord(db, name) {
4846
4865
  const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
4847
4866
  return res.changes > 0;
4848
4867
  }
4868
+ function assertLockShape(parsed, path) {
4869
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4870
+ throw new LockFileError(`hooks.lock (${path}) is malformed: expected a JSON object with a "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
4871
+ }
4872
+ const hooks = parsed.hooks;
4873
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
4874
+ throw new LockFileError(`hooks.lock (${path}) is malformed: missing or invalid "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
4875
+ }
4876
+ for (const [name, entry] of Object.entries(hooks)) {
4877
+ const e = entry;
4878
+ if (e === null || typeof e !== "object" || typeof e.version !== "string" || typeof e.sha256 !== "string") {
4879
+ throw new LockFileError(`hooks.lock (${path}) is malformed: entry for '${name}' is not a valid pin (version and sha256 strings required). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it.`);
4880
+ }
4881
+ }
4882
+ }
4849
4883
  function readLock() {
4884
+ const path = getLockPath();
4885
+ if (!existsSync7(path))
4886
+ return { hooks: {} };
4887
+ let raw;
4850
4888
  try {
4851
- const path = getLockPath();
4852
- if (!existsSync7(path))
4853
- return { hooks: {} };
4854
- const parsed = JSON.parse(readFileSync4(path, "utf-8"));
4855
- return { hooks: parsed.hooks ?? {} };
4889
+ raw = readFileSync4(path, "utf-8");
4856
4890
  } catch {
4857
- return { hooks: {} };
4891
+ throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
4858
4892
  }
4893
+ let parsed;
4894
+ try {
4895
+ parsed = JSON.parse(raw);
4896
+ } catch {
4897
+ throw new LockFileError(`hooks.lock (${path}) is malformed: not valid JSON (${String((raw ?? "").slice(0, 80))}). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
4898
+ }
4899
+ assertLockShape(parsed, path);
4900
+ return parsed;
4859
4901
  }
4860
4902
  function writeLock(lock) {
4861
4903
  const path = getLockPath();
@@ -4864,8 +4906,10 @@ function writeLock(lock) {
4864
4906
  for (const name of Object.keys(lock.hooks).sort()) {
4865
4907
  sorted[name] = lock.hooks[name];
4866
4908
  }
4867
- writeFileSync3(path, JSON.stringify({ hooks: sorted }, null, 2) + `
4909
+ const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
4910
+ writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
4868
4911
  `, "utf-8");
4912
+ renameSync(tmp, path);
4869
4913
  return path;
4870
4914
  }
4871
4915
  function setPinnedHook(name, entry) {
@@ -4951,9 +4995,16 @@ function retrustHook(name, scriptPath, version, source) {
4951
4995
  setPinnedHook(name, { version, sha256: actual, source });
4952
4996
  return { ok: true, pinned: true, expected: actual, actual, name };
4953
4997
  }
4998
+ var LockFileError;
4954
4999
  var init_store = __esm(() => {
4955
5000
  init_db();
4956
5001
  init_config();
5002
+ LockFileError = class LockFileError extends Error {
5003
+ constructor(message) {
5004
+ super(message);
5005
+ this.name = "LockFileError";
5006
+ }
5007
+ };
4957
5008
  });
4958
5009
 
4959
5010
  // src/lib/installer.ts
@@ -5006,7 +5057,7 @@ function getGlobalSettingsPathOverride(target) {
5006
5057
  function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
5007
5058
  if (target === "codewith" && codewithConfigPath)
5008
5059
  return codewithConfigPath;
5009
- if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
5060
+ if (target === "codewith" && scope === "global" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
5010
5061
  return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
5011
5062
  }
5012
5063
  const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
@@ -5439,8 +5490,8 @@ function escapeRegExp(value) {
5439
5490
  function codewithHasHookEntryInText(configText, name) {
5440
5491
  return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
5441
5492
  }
5442
- function codewithHasHookEntry(name) {
5443
- const path = getSettingsPath("global", "codewith");
5493
+ function codewithHasHookEntry(name, scope = "global") {
5494
+ const path = getSettingsPath(scope, "codewith");
5444
5495
  if (!existsSync8(path))
5445
5496
  return false;
5446
5497
  try {
@@ -5457,7 +5508,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
5457
5508
  const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
5458
5509
  const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
5459
5510
  const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
5460
- const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
5511
+ const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
5461
5512
  if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
5462
5513
  return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
5463
5514
  }
@@ -5474,7 +5525,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
5474
5525
  }
5475
5526
  const registrationsRemaining = [];
5476
5527
  if (target === "codewith" || target === "all") {
5477
- const codewithPath = getSettingsPath("global", "codewith");
5528
+ const codewithPath = getSettingsPath(scope, "codewith");
5478
5529
  if (existsSync8(codewithPath)) {
5479
5530
  const before = readFileSync5(codewithPath, "utf-8");
5480
5531
  const after = removeCodewithHookEntry(before, shortName);
@@ -5566,6 +5617,22 @@ function safeMatcherTest(pattern, value) {
5566
5617
  return false;
5567
5618
  }
5568
5619
  }
5620
+ function countSettingsWiring(settings) {
5621
+ const hooks = settings.hooks;
5622
+ if (!hooks || typeof hooks !== "object")
5623
+ return 0;
5624
+ let count = 0;
5625
+ for (const eventKey of Object.keys(hooks)) {
5626
+ const entries = hooks[eventKey];
5627
+ if (!Array.isArray(entries))
5628
+ continue;
5629
+ for (const entry of entries) {
5630
+ if (Array.isArray(entry?.hooks))
5631
+ count += entry.hooks.length;
5632
+ }
5633
+ }
5634
+ return count;
5635
+ }
5569
5636
  function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
5570
5637
  const separator = eventSpec.indexOf(":");
5571
5638
  const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
@@ -5587,6 +5654,71 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
5587
5654
  }));
5588
5655
  }
5589
5656
 
5657
+ // src/lib/redact.ts
5658
+ function redactText(value) {
5659
+ let out = value;
5660
+ for (const pattern of SECRET_SHAPES) {
5661
+ out = out.replace(pattern, REDACTED);
5662
+ }
5663
+ return out;
5664
+ }
5665
+ function redactValue2(value) {
5666
+ if (typeof value === "string")
5667
+ return redactText(value);
5668
+ if (Array.isArray(value))
5669
+ return value.map(redactValue2);
5670
+ if (value !== null && typeof value === "object") {
5671
+ const out = {};
5672
+ for (const [key, child] of Object.entries(value)) {
5673
+ out[key] = SECRET_KEY_NAMES.test(key) ? REDACTED : redactValue2(child);
5674
+ }
5675
+ return out;
5676
+ }
5677
+ return value;
5678
+ }
5679
+ function redactEventPayload(text) {
5680
+ if (text === null || text === undefined)
5681
+ return null;
5682
+ try {
5683
+ const parsed = JSON.parse(text);
5684
+ return redactText(JSON.stringify(redactValue2(parsed)));
5685
+ } catch {
5686
+ return redactText(text);
5687
+ }
5688
+ }
5689
+ function projectEventRowForRead(row) {
5690
+ const out = { ...row };
5691
+ for (const field of ["tool_input", "error", "metadata"]) {
5692
+ const value = out[field];
5693
+ if (typeof value === "string")
5694
+ out[field] = redactEventPayload(value);
5695
+ else if (value === null)
5696
+ out[field] = null;
5697
+ }
5698
+ return out;
5699
+ }
5700
+ var REDACTED = "[REDACTED]", SECRET_KEY_NAMES, SECRET_SHAPES;
5701
+ var init_redact = __esm(() => {
5702
+ SECRET_KEY_NAMES = /^(key|keys|token|tokens|secret|secrets|password|passwd|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret|refresh[_-]?token|authorization|auth|cookie|credential|credentials|session[_-]?id|otp|mfa[_-]?code|two[_-]?fa[_-]?code|verification[_-]?code|database_url|db_url|connection_string|dsn)$/i;
5703
+ SECRET_SHAPES = [
5704
+ /\bsk-(?:proj|svc|ant)-[A-Za-z0-9_-]{10,}/g,
5705
+ /\bsk-[A-Za-z0-9]{16,}/g,
5706
+ /\btpe_[A-Za-z0-9]{8,}/g,
5707
+ /\b(?:rk|sk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
5708
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
5709
+ /gh[pousr]_[A-Za-z0-9]{20,}/g,
5710
+ /xox[baprs]-[A-Za-z0-9-]{20,}/g,
5711
+ /AKIA[0-9A-Z]{16}/g,
5712
+ /ASIA[0-9A-Z]{16}/g,
5713
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
5714
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
5715
+ /-----BEGIN OPENSSH PRIVATE KEY-----/g,
5716
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
5717
+ /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
5718
+ /\b(?:password|passwd|pwd|token|api[_-]?key|secret|access[_-]?key|client[_-]?secret|authorization|auth|credential|database_url|db_url|connection_string|dsn)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']{6,})/gi
5719
+ ];
5720
+ });
5721
+
5590
5722
  // src/lib/profiles.ts
5591
5723
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
5592
5724
  import { join as join8 } from "path";
@@ -5708,6 +5840,9 @@ function writeHookEvent(event) {
5708
5840
  const db = getDb();
5709
5841
  const id = nanoid2();
5710
5842
  const timestamp = event.timestamp ?? new Date().toISOString();
5843
+ const toolInput = redactEventPayload(event.tool_input);
5844
+ const error = redactEventPayload(event.error);
5845
+ const metadata = redactEventPayload(event.metadata);
5711
5846
  db.run(`INSERT INTO hook_events
5712
5847
  (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
5713
5848
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
@@ -5717,12 +5852,12 @@ function writeHookEvent(event) {
5717
5852
  event.hook_name,
5718
5853
  event.event_type,
5719
5854
  event.tool_name ?? null,
5720
- event.tool_input ? event.tool_input.slice(0, 500) : null,
5855
+ toolInput ? toolInput.slice(0, 500) : null,
5721
5856
  event.result ?? null,
5722
- event.error ?? null,
5857
+ error ?? null,
5723
5858
  event.duration_ms ?? null,
5724
5859
  event.project_dir ?? null,
5725
- event.metadata ?? null
5860
+ metadata ?? null
5726
5861
  ]);
5727
5862
  } catch (err) {
5728
5863
  process.stderr.write(`[hooks db-writer] failed to write event: ${err}
@@ -5772,6 +5907,7 @@ function recordHookRun(record) {
5772
5907
  var ALLOWED_EVENT_TYPES;
5773
5908
  var init_db_writer = __esm(() => {
5774
5909
  init_db();
5910
+ init_redact();
5775
5911
  ALLOWED_EVENT_TYPES = new Set([
5776
5912
  "PreToolUse",
5777
5913
  "PostToolUse",
@@ -5784,13 +5920,73 @@ var init_db_writer = __esm(() => {
5784
5920
  ]);
5785
5921
  });
5786
5922
 
5923
+ // src/lib/hook-env.ts
5924
+ function isDeniedEnvName(name) {
5925
+ const upper = name.toUpperCase();
5926
+ if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
5927
+ return true;
5928
+ if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
5929
+ return true;
5930
+ return DENY_CONTAINS.some((token) => upper.includes(token));
5931
+ }
5932
+ function buildHookEnv(source, extra) {
5933
+ const out = {};
5934
+ for (const name of ALLOWLIST) {
5935
+ const value = source[name] ?? extra?.[name];
5936
+ if (value !== undefined && value !== null)
5937
+ out[name] = value;
5938
+ }
5939
+ for (const [from, to] of CONFIG_PROJECTIONS) {
5940
+ const value = source[from] ?? extra?.[from];
5941
+ if (value !== undefined && value !== null)
5942
+ out[to] = value;
5943
+ }
5944
+ const merged = { ...source, ...extra ?? {} };
5945
+ for (const [name, value] of Object.entries(merged)) {
5946
+ if (out[name] !== undefined)
5947
+ continue;
5948
+ if (isDeniedEnvName(name))
5949
+ continue;
5950
+ if (value !== undefined && value !== null)
5951
+ out[name] = value;
5952
+ }
5953
+ return out;
5954
+ }
5955
+ var ALLOWLIST, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, CONFIG_PROJECTIONS;
5956
+ var init_hook_env = __esm(() => {
5957
+ ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
5958
+ DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
5959
+ DENY_PREFIXES = [
5960
+ "HASNA_",
5961
+ "AWS_",
5962
+ "AZURE_",
5963
+ "GCP_",
5964
+ "VAULT_",
5965
+ "GOOGLE_",
5966
+ "OPENAI_",
5967
+ "ANTHROPIC_",
5968
+ "POSTGRES_",
5969
+ "MYSQL_",
5970
+ "REDIS_",
5971
+ "MONGO_",
5972
+ "MEMENTOS_"
5973
+ ];
5974
+ DENY_CONTAINS = ["DATABASE_URL"];
5975
+ CONFIG_PROJECTIONS = [
5976
+ ["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
5977
+ ["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
5978
+ ["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
5979
+ ["HASNA_HOOKS_CONFIG_PATH", "HOOKS_CONFIG_PATH"]
5980
+ ];
5981
+ });
5982
+
5787
5983
  // src/lib/run.ts
5788
5984
  var exports_run = {};
5789
5985
  __export(exports_run, {
5790
5986
  executeVerifiedScript: () => executeVerifiedScript,
5791
5987
  HookTimeoutError: () => HookTimeoutError
5792
5988
  });
5793
- import { randomBytes } from "crypto";
5989
+ import { randomBytes as randomBytes2 } from "crypto";
5794
5990
  import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
5795
5991
  import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
5796
5992
  function extensionOf(scriptPath) {
@@ -5863,7 +6059,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
5863
6059
  async function executeVerifiedScript(options) {
5864
6060
  const scriptDir = dirname4(options.scriptPath);
5865
6061
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
5866
- const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${interpreter.tempExt}`);
6062
+ const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5867
6063
  let proc = null;
5868
6064
  let timedOut = false;
5869
6065
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -5876,7 +6072,7 @@ async function executeVerifiedScript(options) {
5876
6072
  stdin: new Response(options.stdin),
5877
6073
  stdout: "pipe",
5878
6074
  stderr: "pipe",
5879
- env: options.env ?? process.env,
6075
+ env: buildHookEnv(process.env, options.env),
5880
6076
  detached: true
5881
6077
  });
5882
6078
  const exitCode = await proc.exited;
@@ -5934,6 +6130,7 @@ function killGroup(proc, options) {
5934
6130
  }
5935
6131
  var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
5936
6132
  var init_run = __esm(() => {
6133
+ init_hook_env();
5937
6134
  HookTimeoutError = class HookTimeoutError extends Error {
5938
6135
  timeoutMs;
5939
6136
  constructor(timeoutMs) {
@@ -6028,19 +6225,20 @@ async function installCustomSource(source) {
6028
6225
  return copyManifestDir(source, source, "local");
6029
6226
  }
6030
6227
  async function installManifestUrl(url) {
6031
- const res = await fetch(url, { redirect: "follow" });
6228
+ const res = await fetch(url, { redirect: "error" });
6032
6229
  if (!res.ok) {
6033
6230
  throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
6034
6231
  }
6035
6232
  const manifest = parseManifest(await res.text());
6036
6233
  let script;
6037
- if (manifest.script.includes(`
6038
- `)) {
6234
+ const inline = manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
6235
+ `);
6236
+ if (inline) {
6039
6237
  script = { path: "script.ts", content: manifest.script };
6040
6238
  } else {
6041
6239
  const base = url.substring(0, url.lastIndexOf("/"));
6042
6240
  const scriptUrl = `${base}/${manifest.script}`;
6043
- const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
6241
+ const scriptRes = await fetch(scriptUrl, { redirect: "error" });
6044
6242
  if (!scriptRes.ok) {
6045
6243
  throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
6046
6244
  }
@@ -6064,8 +6262,10 @@ var init_custom_install = __esm(() => {
6064
6262
  var exports_sync = {};
6065
6263
  __export(exports_sync, {
6066
6264
  syncHooks: () => syncHooks,
6265
+ stageSyncArtifacts: () => stageSyncArtifacts,
6067
6266
  planSync: () => planSync,
6068
- fetchPinnedHook: () => fetchPinnedHook
6267
+ fetchPinnedHook: () => fetchPinnedHook,
6268
+ commitSyncArtifacts: () => commitSyncArtifacts
6069
6269
  });
6070
6270
  import { createHash as createHash2 } from "crypto";
6071
6271
  import { readFileSync as readFileSync8 } from "fs";
@@ -6089,7 +6289,7 @@ function collectBundledCatalog() {
6089
6289
  }
6090
6290
  return out;
6091
6291
  }
6092
- async function fetchJson(base, path) {
6292
+ async function fetchJson(base, path, opts) {
6093
6293
  const headers = { accept: "application/json" };
6094
6294
  const apiKey = resolveApiKey();
6095
6295
  if (apiKey)
@@ -6105,7 +6305,11 @@ async function fetchJson(base, path) {
6105
6305
  if (!res.ok) {
6106
6306
  throw new Error(`GET ${path} failed with status ${res.status}`);
6107
6307
  }
6108
- return res.json();
6308
+ const body = await res.json();
6309
+ if (opts?.withHeader) {
6310
+ body[opts.withHeader] = res.headers.get("x-hook-sha256");
6311
+ }
6312
+ return body;
6109
6313
  }
6110
6314
  async function fetchRemoteState(apiUrl) {
6111
6315
  const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
@@ -6118,6 +6322,16 @@ async function fetchRemoteState(apiUrl) {
6118
6322
  }
6119
6323
  return { catalog: catalog.hooks, remoteLock };
6120
6324
  }
6325
+ function classifyRemotePin(pin, remoteVersion, remoteSha) {
6326
+ const pinnedLocally = pin !== undefined && pin.version === remoteVersion && pin.sha256 === remoteSha;
6327
+ if (pinnedLocally)
6328
+ return "unchanged";
6329
+ if (pin !== undefined && pin.pinned === true && pin.version !== remoteVersion)
6330
+ return "unchanged";
6331
+ if (pin !== undefined)
6332
+ return "updated";
6333
+ return "added";
6334
+ }
6121
6335
  function computeDiff(catalog, remoteLock) {
6122
6336
  const local = readLock();
6123
6337
  const diff = { added: [], updated: [], unchanged: [], skipped: [] };
@@ -6125,30 +6339,30 @@ function computeDiff(catalog, remoteLock) {
6125
6339
  if (!entry.name || !entry.version || !entry.sha256)
6126
6340
  continue;
6127
6341
  const pin = local.hooks[entry.name];
6128
- const remotePin = remoteLock ? remoteLock.hooks[entry.name] : entry;
6129
- if (remoteLock && !remotePin) {
6130
- diff.skipped.push(entry.name);
6131
- continue;
6132
- }
6133
- const pinnedLocally = pin && pin.version === entry.version && pin.sha256 === entry.sha256;
6134
- if (pinnedLocally) {
6135
- diff.unchanged.push(entry.name);
6136
- } else if (pin) {
6137
- diff.updated.push(entry.name);
6342
+ if (remoteLock) {
6343
+ const remotePin = remoteLock.hooks[entry.name];
6344
+ if (!remotePin) {
6345
+ diff.skipped.push(entry.name);
6346
+ continue;
6347
+ }
6348
+ if (remotePin.version !== entry.version) {
6349
+ throw new Error(`remote registry is ambiguous for '${entry.name}': lock pins ${remotePin.version} but catalog latest is ${entry.version}. ` + `Fix the registry (or its lock) before syncing \u2014 nothing was changed.`);
6350
+ }
6351
+ diff[classifyRemotePin(pin, remotePin.version, remotePin.sha256)].push(entry.name);
6138
6352
  } else {
6139
- diff.added.push(entry.name);
6353
+ diff[classifyRemotePin(pin, entry.version, entry.sha256)].push(entry.name);
6140
6354
  }
6141
6355
  }
6142
6356
  return diff;
6143
6357
  }
6144
- async function planSync() {
6358
+ async function planSync(options = {}) {
6145
6359
  const apiUrl = resolveApiUrl();
6146
6360
  if (apiUrl) {
6147
6361
  const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
6148
- return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
6362
+ return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
6149
6363
  }
6150
6364
  const diff = computeDiff(collectBundledCatalog(), null);
6151
- return { apiUrl: null, dryRun: false, diff };
6365
+ return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
6152
6366
  }
6153
6367
  async function syncHooks(options = {}) {
6154
6368
  const apiUrl = resolveApiUrl();
@@ -6156,10 +6370,10 @@ async function syncHooks(options = {}) {
6156
6370
  if (options.dryRun)
6157
6371
  return { ...plan, dryRun: true };
6158
6372
  if (!apiUrl) {
6159
- const db2 = getDb();
6373
+ const db = getDb();
6160
6374
  for (const entry of collectBundledCatalog()) {
6161
6375
  setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
6162
- upsertHookRecord(db2, {
6376
+ upsertHookRecord(db, {
6163
6377
  name: entry.name,
6164
6378
  version: entry.version,
6165
6379
  sha256: entry.sha256,
@@ -6172,13 +6386,17 @@ async function syncHooks(options = {}) {
6172
6386
  }
6173
6387
  const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
6174
6388
  const diff = computeDiff(catalog, remoteLock);
6175
- const toFetch = [...diff.added, ...diff.updated];
6176
- const artifacts = new Map;
6177
- for (const name of toFetch) {
6389
+ const staged = await stageSyncArtifacts(apiUrl, [...diff.added, ...diff.updated], remoteLock);
6390
+ await commitSyncArtifacts(staged, apiUrl, remoteLock);
6391
+ return { apiUrl, dryRun: false, diff };
6392
+ }
6393
+ async function stageSyncArtifacts(apiUrl, names, remoteLock) {
6394
+ const staged = [];
6395
+ for (const name of names) {
6178
6396
  const entry = remoteLock.hooks[name];
6179
6397
  if (!entry)
6180
6398
  continue;
6181
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
6399
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
6182
6400
  if (!artifact.manifest || typeof artifact.script !== "string") {
6183
6401
  throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
6184
6402
  }
@@ -6186,34 +6404,64 @@ async function syncHooks(options = {}) {
6186
6404
  if (actualSha !== entry.sha256) {
6187
6405
  throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
6188
6406
  }
6189
- artifacts.set(name, artifact);
6190
- }
6191
- const db = getDb();
6192
- for (const name of toFetch) {
6193
- const artifact = artifacts.get(name);
6194
- const entry = remoteLock.hooks[name];
6195
- if (!artifact || !entry)
6196
- continue;
6197
6407
  const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
6198
- const scriptRel = artifact.manifest.script.includes(`
6199
- `) ? "script.ts" : artifact.manifest.script;
6200
- writeCustomHook(name, manifest, artifact.script, scriptRel);
6201
- const scriptPath = resolveScriptPath(name);
6202
- const actual = await sha256File(scriptPath);
6203
- if (actual !== entry.sha256) {
6204
- throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
6408
+ if (manifest.version !== entry.version) {
6409
+ throw new Error(`artifact for '${name}@${entry.version}' declares a different version ('${manifest.version}')`);
6205
6410
  }
6206
- setPinnedHook(name, { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" });
6207
- upsertHookRecord(db, {
6411
+ const scriptRel = scriptRelFor(manifest);
6412
+ staged.push({
6208
6413
  name,
6209
6414
  version: entry.version,
6210
6415
  sha256: entry.sha256,
6211
- source_type: entry.source ?? "remote",
6212
- source_ref: apiUrl,
6213
- last_verified_at: new Date().toISOString()
6416
+ source: entry.source ?? "remote",
6417
+ manifest,
6418
+ scriptContent: artifact.script,
6419
+ scriptRel
6214
6420
  });
6215
6421
  }
6216
- return { apiUrl, dryRun: false, diff };
6422
+ return staged;
6423
+ }
6424
+ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
6425
+ const db = getDb();
6426
+ for (const item of staged) {
6427
+ const entry = remoteLock.hooks[item.name];
6428
+ if (!entry)
6429
+ continue;
6430
+ writeCustomHook(item.name, item.manifest, item.scriptContent, item.scriptRel);
6431
+ const scriptPath = resolveScriptPath(item.name);
6432
+ const actual = await sha256File(scriptPath);
6433
+ if (actual !== entry.sha256) {
6434
+ throw new Error(`sha256 mismatch after write for '${item.name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
6435
+ }
6436
+ }
6437
+ const lock = readLock();
6438
+ for (const item of staged) {
6439
+ const entry = remoteLock.hooks[item.name];
6440
+ if (!entry)
6441
+ continue;
6442
+ lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
6443
+ }
6444
+ writeLock(lock);
6445
+ db.exec("BEGIN");
6446
+ try {
6447
+ for (const item of staged) {
6448
+ const entry = remoteLock.hooks[item.name];
6449
+ if (!entry)
6450
+ continue;
6451
+ upsertHookRecord(db, {
6452
+ name: item.name,
6453
+ version: entry.version,
6454
+ sha256: entry.sha256,
6455
+ source_type: entry.source ?? "remote",
6456
+ source_ref: apiUrl,
6457
+ last_verified_at: new Date().toISOString()
6458
+ });
6459
+ }
6460
+ db.exec("COMMIT");
6461
+ } catch (err) {
6462
+ db.exec("ROLLBACK");
6463
+ throw err;
6464
+ }
6217
6465
  }
6218
6466
  async function fetchPinnedHook(name, version, apiUrl) {
6219
6467
  const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
@@ -6221,10 +6469,11 @@ async function fetchPinnedHook(name, version, apiUrl) {
6221
6469
  if (!entry) {
6222
6470
  throw new Error(`Hook '${name}' is not in the remote registry lock`);
6223
6471
  }
6224
- if (entry.version !== version) {
6225
- throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${entry.version})`);
6472
+ const isLatest = entry.version === version;
6473
+ if (!isLatest && !(entry.versions ?? []).includes(version)) {
6474
+ throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
6226
6475
  }
6227
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}`);
6476
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
6228
6477
  if (!artifact.manifest || typeof artifact.script !== "string") {
6229
6478
  throw new Error(`artifact for '${name}@${version}' is malformed`);
6230
6479
  }
@@ -6236,24 +6485,31 @@ async function fetchPinnedHook(name, version, apiUrl) {
6236
6485
  throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
6237
6486
  }
6238
6487
  const actualSha = sha256OfText(artifact.script);
6239
- if (actualSha !== entry.sha256) {
6240
- throw new Error(`sha256 mismatch for '${name}@${version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
6488
+ let expectedSha = entry.sha256;
6489
+ if (!isLatest) {
6490
+ const headerSha = artifact._headerSha;
6491
+ if (!headerSha) {
6492
+ throw new Error(`registry did not return a sha256 header for '${name}@${version}' (older pin); refusing to trust an unverified artifact`);
6493
+ }
6494
+ expectedSha = headerSha;
6495
+ }
6496
+ if (actualSha !== expectedSha) {
6497
+ throw new Error(`sha256 mismatch for '${name}@${version}': expected ${expectedSha}, artifact has ${actualSha}`);
6241
6498
  }
6242
6499
  const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
6243
- const scriptRel = artifact.manifest.script.includes(`
6244
- `) ? "script.ts" : artifact.manifest.script;
6500
+ const scriptRel = scriptRelFor(manifest);
6245
6501
  writeCustomHook(name, manifest, artifact.script, scriptRel);
6246
6502
  const scriptPath = resolveScriptPath(name);
6247
6503
  const actual = await sha256File(scriptPath);
6248
- if (actual !== entry.sha256) {
6249
- throw new Error(`sha256 mismatch after write for '${name}@${version}': lock says ${entry.sha256}, on disk ${actual}`);
6504
+ if (actual !== expectedSha) {
6505
+ throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
6250
6506
  }
6251
6507
  const db = getDb();
6252
- setPinnedHook(name, { version, sha256: entry.sha256, source: entry.source ?? "remote" });
6508
+ setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
6253
6509
  upsertHookRecord(db, {
6254
6510
  name,
6255
6511
  version,
6256
- sha256: entry.sha256,
6512
+ sha256: expectedSha,
6257
6513
  source_type: entry.source ?? "remote",
6258
6514
  source_ref: apiUrl,
6259
6515
  last_verified_at: new Date().toISOString()
@@ -6261,7 +6517,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
6261
6517
  return {
6262
6518
  name,
6263
6519
  version,
6264
- sha256: entry.sha256,
6520
+ sha256: expectedSha,
6265
6521
  source: entry.source ?? "remote",
6266
6522
  source_ref: apiUrl,
6267
6523
  artifact,
@@ -6286,7 +6542,7 @@ var init_pg_migrations = __esm(() => {
6286
6542
  timestamp TEXT NOT NULL,
6287
6543
  session_id TEXT NOT NULL,
6288
6544
  hook_name TEXT NOT NULL,
6289
- event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
6545
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
6290
6546
  tool_name TEXT,
6291
6547
  tool_input TEXT,
6292
6548
  result TEXT CHECK (result IN ('continue', 'block', NULL)),
@@ -6319,7 +6575,10 @@ var init_pg_migrations = __esm(() => {
6319
6575
  )`,
6320
6576
  `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
6321
6577
  `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
6322
- CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`
6578
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`,
6579
+ `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
6580
+ `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
6581
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart'))`
6323
6582
  ];
6324
6583
  });
6325
6584
 
@@ -6334,7 +6593,34 @@ function normalizeParams(params) {
6334
6593
  return flat.map((value) => value === undefined ? null : value);
6335
6594
  }
6336
6595
  function sslConfigFor(connectionString) {
6337
- return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
6596
+ const wantsSsl = wantsSslFor(connectionString);
6597
+ if (!wantsSsl)
6598
+ return;
6599
+ const insecure = process.env.HASNA_HOOKS_PG_INSECURE_TLS === "1" || process.env.HOOKS_PG_INSECURE_TLS === "1";
6600
+ if (insecure && false) {}
6601
+ return insecure ? { rejectUnauthorized: false } : { rejectUnauthorized: true };
6602
+ }
6603
+ function wantsSslFor(connectionString) {
6604
+ let raw;
6605
+ try {
6606
+ raw = new URL(connectionString).search.replace(/^\?/, "");
6607
+ } catch {
6608
+ raw = connectionString;
6609
+ }
6610
+ try {
6611
+ raw = decodeURIComponent(raw);
6612
+ } catch {}
6613
+ const paramValue = (name) => {
6614
+ const match = raw.match(new RegExp(`(?:^|[&\\s])${name}\\s*=\\s*([^&\\s]+)`, "i"));
6615
+ return match ? match[1].toLowerCase() : undefined;
6616
+ };
6617
+ const sslmode = paramValue("sslmode");
6618
+ if (sslmode !== undefined)
6619
+ return sslmode !== "disable" && sslmode !== "allow";
6620
+ const ssl = paramValue("ssl");
6621
+ if (ssl !== undefined)
6622
+ return ssl !== "false" && ssl !== "0" && ssl !== "no";
6623
+ return false;
6338
6624
  }
6339
6625
 
6340
6626
  class PgAdapterAsync {
@@ -6496,9 +6782,15 @@ async function pushTable(db, remote, table) {
6496
6782
  result.rowsRead = rows.length;
6497
6783
  if (rows.length === 0)
6498
6784
  return result;
6785
+ const projected = table === "hook_events" ? rows.map((row) => ({
6786
+ ...row,
6787
+ tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
6788
+ error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
6789
+ metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
6790
+ })) : rows;
6499
6791
  const remoteColumns = await getRemoteColumns(remote, table);
6500
- const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
6501
- result.rowsWritten = await upsertPg(remote, table, columns, rows, remoteColumns);
6792
+ const columns = filterRemoteColumns(remoteColumns, Object.keys(projected[0]));
6793
+ result.rowsWritten = await upsertPg(remote, table, columns, projected, remoteColumns);
6502
6794
  } catch (error) {
6503
6795
  result.errors.push(error instanceof Error ? error.message : String(error));
6504
6796
  }
@@ -6513,8 +6805,14 @@ async function pullTable(remote, db, table) {
6513
6805
  result.rowsRead = rows.length;
6514
6806
  if (rows.length === 0)
6515
6807
  return result;
6516
- const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
6517
- result.rowsWritten = upsertSqlite(db, table, columns, rows);
6808
+ const projected = table === "hook_events" ? rows.map((row) => ({
6809
+ ...row,
6810
+ tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
6811
+ error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
6812
+ metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
6813
+ })) : rows;
6814
+ const columns = filterLocalColumns(db, table, Object.keys(projected[0]));
6815
+ result.rowsWritten = upsertSqlite(db, table, columns, projected);
6518
6816
  } catch (error) {
6519
6817
  result.errors.push(error instanceof Error ? error.message : String(error));
6520
6818
  }
@@ -6637,6 +6935,7 @@ var init_storage_sync = __esm(() => {
6637
6935
  init_db();
6638
6936
  init_pg_migrations();
6639
6937
  init_remote_storage();
6938
+ init_redact();
6640
6939
  STORAGE_TABLES = [
6641
6940
  "hook_events",
6642
6941
  "schema_migrations",
@@ -6735,24 +7034,29 @@ async function buildCatalog() {
6735
7034
  const scriptPath = resolveHook(meta.name)?.scriptPath;
6736
7035
  if (!scriptPath)
6737
7036
  continue;
7037
+ const sha = await sha256File(scriptPath);
6738
7038
  byName.set(meta.name, {
6739
7039
  name: meta.name,
6740
7040
  version: meta.version,
6741
- sha256: await sha256File(scriptPath),
7041
+ sha256: sha,
6742
7042
  events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
6743
7043
  description: meta.description,
6744
- source: "bundled"
7044
+ source: "bundled",
7045
+ versions: [meta.version]
6745
7046
  });
6746
7047
  }
6747
7048
  for (const custom of listCustomHooks()) {
6748
7049
  const name = shortManifestName(custom.manifest.name);
7050
+ const sha = await sha256File(custom.scriptPath);
7051
+ const existing = byName.get(name);
6749
7052
  byName.set(name, {
6750
7053
  name,
6751
7054
  version: custom.manifest.version,
6752
- sha256: await sha256File(custom.scriptPath),
7055
+ sha256: sha,
6753
7056
  events: custom.manifest.events,
6754
7057
  description: custom.manifest.description ?? "Custom hook",
6755
- source: byName.has(name) ? "custom-overrides-bundled" : "custom"
7058
+ source: existing ? "custom-overrides-bundled" : "custom",
7059
+ versions: existing ? [...new Set([...existing.versions, custom.manifest.version])] : [custom.manifest.version]
6756
7060
  });
6757
7061
  }
6758
7062
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
@@ -6770,6 +7074,7 @@ async function artifactFor(name, version) {
6770
7074
  description: resolved.description,
6771
7075
  events: resolved.events,
6772
7076
  script: custom ? custom.manifest.script : `src/hook.ts`,
7077
+ script_kind: custom?.manifest.script_kind,
6773
7078
  args: custom?.manifest.args,
6774
7079
  timeout_ms: custom?.manifest.timeout_ms
6775
7080
  };
@@ -6799,11 +7104,32 @@ function handleServeRequest(req, apiKey) {
6799
7104
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
6800
7105
  }
6801
7106
  if (url.pathname === "/api/v1/lock" && req.method === "GET") {
6802
- return Promise.resolve(json(readLock()));
7107
+ return buildCatalog().then((catalog) => {
7108
+ const lock = readLock();
7109
+ const byVersion = {};
7110
+ for (const entry of catalog)
7111
+ byVersion[entry.name] = entry.versions;
7112
+ const hooks = {};
7113
+ for (const [name, pin] of Object.entries(lock.hooks)) {
7114
+ hooks[name] = { ...pin, versions: byVersion[name] ?? [pin.version] };
7115
+ }
7116
+ return json({ hooks });
7117
+ });
6803
7118
  }
6804
- const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
7119
+ const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
6805
7120
  if (artifactMatch && req.method === "GET") {
6806
- const [, name, version] = artifactMatch;
7121
+ const [, rawName, rawVersion] = artifactMatch;
7122
+ let name;
7123
+ let version;
7124
+ try {
7125
+ name = decodeURIComponent(rawName);
7126
+ version = decodeURIComponent(rawVersion);
7127
+ } catch {
7128
+ return Promise.resolve(json({ error: "invalid URL encoding" }, 400));
7129
+ }
7130
+ if (!SEMVER_PATTERN.test(version)) {
7131
+ return Promise.resolve(json({ error: `invalid semver version '${version}'` }, 400));
7132
+ }
6807
7133
  return artifactFor(name, version).then(async (artifact) => {
6808
7134
  if (!artifact)
6809
7135
  return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
@@ -6834,7 +7160,7 @@ function handleServeRequest(req, apiKey) {
6834
7160
  function startServeServer(options) {
6835
7161
  const port = options.port ?? DEFAULT_SERVE_PORT;
6836
7162
  const host = options.host ?? SERVE_HOST;
6837
- const apiKey = resolveApiKey(options.apiKey);
7163
+ const apiKey = resolveApiKey();
6838
7164
  const server = Bun.serve({
6839
7165
  hostname: host,
6840
7166
  port,
@@ -6852,6 +7178,7 @@ var init_serve = __esm(() => {
6852
7178
  init_store();
6853
7179
  init_resolve();
6854
7180
  init_config();
7181
+ init_semver();
6855
7182
  if (false) {}
6856
7183
  });
6857
7184
 
@@ -6941,7 +7268,9 @@ var exports_server = {};
6941
7268
  __export(exports_server, {
6942
7269
  startStdioServer: () => startStdioServer,
6943
7270
  startSSEServer: () => startSSEServer,
7271
+ sseAuthToken: () => sseAuthToken,
6944
7272
  createHooksServer: () => createHooksServer,
7273
+ MCP_SSE_HOST: () => MCP_SSE_HOST,
6945
7274
  MCP_PORT: () => MCP_PORT
6946
7275
  });
6947
7276
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -6969,7 +7298,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
6969
7298
  const started = Date.now();
6970
7299
  let ran;
6971
7300
  try {
6972
- ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, env: process.env, timeout: timeoutMs });
7301
+ ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, timeout: timeoutMs });
6973
7302
  } catch (err) {
6974
7303
  try {
6975
7304
  const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
@@ -7075,6 +7404,9 @@ function compactEvent(row) {
7075
7404
  tool_input_preview: row.tool_input ? truncateText(row.tool_input, 180) : undefined
7076
7405
  };
7077
7406
  }
7407
+ function projectLogRows(rows) {
7408
+ return rows.map((row) => projectEventRowForRead({ ...row }));
7409
+ }
7078
7410
  function createHooksServer() {
7079
7411
  const server = new McpServer({
7080
7412
  name: "@hasna/hooks",
@@ -7456,7 +7788,13 @@ function createHooksServer() {
7456
7788
  } catch (err) {
7457
7789
  const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
7458
7790
  if (err instanceof HookTimeoutError2) {
7459
- return { name, decision: "approve", timedOut: true, timeout_ms: effectiveTimeout };
7791
+ return {
7792
+ name,
7793
+ decision: "block",
7794
+ timedOut: true,
7795
+ timeout_ms: effectiveTimeout,
7796
+ error: `hook timed out after ${effectiveTimeout}ms \u2014 preview blocked (never auto-approved)`
7797
+ };
7460
7798
  }
7461
7799
  return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
7462
7800
  }
@@ -7482,15 +7820,25 @@ function createHooksServer() {
7482
7820
  hooks: z2.array(z2.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
7483
7821
  scope: z2.enum(["global", "project"]).default("global").describe("Install scope")
7484
7822
  }, async ({ agent_type, name, hooks, scope }) => {
7823
+ if (agent_type !== "claude" && agent_type !== "gemini" && agent_type !== "custom") {
7824
+ return {
7825
+ content: [{
7826
+ type: "text",
7827
+ text: JSON.stringify({ error: `unsupported agent_type '${String(agent_type)}' (supported: claude, gemini, custom)` })
7828
+ }],
7829
+ isError: true
7830
+ };
7831
+ }
7485
7832
  const profile = createProfile({ agent_type, name });
7833
+ const target = agent_type === "gemini" ? "gemini" : "claude";
7486
7834
  const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
7487
- const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id }));
7835
+ const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id, target }));
7488
7836
  const installed = results.filter((r) => r.success).map((r) => r.hook);
7489
7837
  const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
7490
7838
  return {
7491
7839
  content: [{
7492
7840
  type: "text",
7493
- text: JSON.stringify({ profile, installed, failed, scope, run_with: `hooks run <name> --profile ${profile.agent_id}` })
7841
+ text: JSON.stringify({ profile, installed, failed, scope, target, run_with: `hooks run <name> --profile ${profile.agent_id}` })
7494
7842
  }]
7495
7843
  };
7496
7844
  });
@@ -7642,12 +7990,13 @@ function createHooksServer() {
7642
7990
  sql += " ORDER BY timestamp DESC LIMIT ?";
7643
7991
  params.push(maxRows);
7644
7992
  const rows = db.query(sql).all(...params);
7993
+ const projected = projectLogRows(rows);
7645
7994
  return {
7646
7995
  content: [{
7647
7996
  type: "text",
7648
7997
  text: JSON.stringify({
7649
- events: compact ? rows.map(compactEvent) : rows,
7650
- count: rows.length,
7998
+ events: compact ? projected.map(compactEvent) : projected,
7999
+ count: projected.length,
7651
8000
  compact,
7652
8001
  hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
7653
8002
  })
@@ -7662,12 +8011,13 @@ function createHooksServer() {
7662
8011
  const db = getDb2();
7663
8012
  const maxRows = boundedLimit(n, 20, compact ? 100 : 500);
7664
8013
  const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(maxRows);
8014
+ const projected = projectLogRows(rows);
7665
8015
  return {
7666
8016
  content: [{
7667
8017
  type: "text",
7668
8018
  text: JSON.stringify({
7669
- events: compact ? rows.map(compactEvent) : rows,
7670
- count: rows.length,
8019
+ events: compact ? projected.map(compactEvent) : projected,
8020
+ count: projected.length,
7671
8021
  compact,
7672
8022
  hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
7673
8023
  })
@@ -7692,12 +8042,13 @@ function createHooksServer() {
7692
8042
  }
7693
8043
  const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
7694
8044
  const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, maxRows);
8045
+ const projected = projectLogRows(rows);
7695
8046
  return {
7696
8047
  content: [{
7697
8048
  type: "text",
7698
8049
  text: JSON.stringify({
7699
- events: compact ? rows.map(compactEvent) : rows,
7700
- count: rows.length,
8050
+ events: compact ? projected.map(compactEvent) : projected,
8051
+ count: projected.length,
7701
8052
  compact,
7702
8053
  hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
7703
8054
  })
@@ -7825,11 +8176,48 @@ function createHooksServer() {
7825
8176
  });
7826
8177
  return server;
7827
8178
  }
7828
- async function startSSEServer(port = MCP_PORT) {
8179
+ function isLoopbackHost(host) {
8180
+ return host === "" || host === "127.0.0.1" || host === "::1" || host === "localhost";
8181
+ }
8182
+ function sseAuthToken(explicit) {
8183
+ if (explicit && explicit.length > 0)
8184
+ return explicit;
8185
+ for (const name of MCP_SSE_AUTH_ENV) {
8186
+ const value = process.env[name];
8187
+ if (value && value.length > 0)
8188
+ return value;
8189
+ }
8190
+ return;
8191
+ }
8192
+ async function startSSEServer(options = {}) {
8193
+ const port = options.port ?? MCP_PORT;
8194
+ const host = options.host ?? MCP_SSE_HOST;
8195
+ const authToken = sseAuthToken(options.authToken);
8196
+ const loopback = isLoopbackHost(host);
8197
+ if (!loopback && !authToken) {
8198
+ throw new Error(`Refusing to serve MCP SSE on non-loopback host '${host}' without an auth token. Set HASNA_HOOKS_MCP_TOKEN (or HOOKS_MCP_TOKEN) to authorize a non-loopback bind.`);
8199
+ }
7829
8200
  const server = createHooksServer();
7830
8201
  const transports = new Map;
8202
+ function authorized2(headers) {
8203
+ if (!authToken)
8204
+ return false;
8205
+ const rawAuth = headers["authorization"];
8206
+ const header = (Array.isArray(rawAuth) ? rawAuth[0] : rawAuth) ?? "";
8207
+ if (header.startsWith("Bearer "))
8208
+ return secureEqual(header.slice("Bearer ".length), authToken);
8209
+ const rawKey = headers["x-api-key"];
8210
+ return secureEqual((Array.isArray(rawKey) ? rawKey[0] : rawKey) ?? "", authToken);
8211
+ }
7831
8212
  const httpServer = createServer(async (req, res) => {
7832
8213
  const url = new URL(req.url || "/", `http://localhost:${port}`);
8214
+ if (url.pathname === "/sse" || url.pathname === "/messages") {
8215
+ if (authToken && !authorized2(req.headers)) {
8216
+ res.writeHead(401);
8217
+ res.end("unauthorized: valid MCP token required");
8218
+ return;
8219
+ }
8220
+ }
7833
8221
  if (url.pathname === "/sse") {
7834
8222
  const transport = new SSEServerTransport("/messages", res);
7835
8223
  transports.set(transport.sessionId, transport);
@@ -7852,9 +8240,9 @@ async function startSSEServer(port = MCP_PORT) {
7852
8240
  res.end(JSON.stringify({ name: "@hasna/hooks", version: pkg.version, transport: "sse", port }));
7853
8241
  }
7854
8242
  });
7855
- httpServer.listen(port, () => {
7856
- console.error(`@hasna/hooks MCP server running on http://localhost:${port}`);
7857
- console.error(`SSE endpoint: http://localhost:${port}/sse`);
8243
+ httpServer.listen(port, host, () => {
8244
+ console.error(`@hasna/hooks MCP server running on http://${host}:${port}`);
8245
+ console.error(`SSE endpoint: http://${host}:${port}/sse`);
7858
8246
  });
7859
8247
  }
7860
8248
  async function startStdioServer() {
@@ -7868,7 +8256,7 @@ async function startStdioServer() {
7868
8256
  process.exit(1);
7869
8257
  }
7870
8258
  }
7871
- var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents;
8259
+ var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents, MCP_SSE_HOST = "127.0.0.1", MCP_SSE_AUTH_ENV;
7872
8260
  var init_server = __esm(() => {
7873
8261
  init_registry();
7874
8262
  init_installer();
@@ -7876,6 +8264,7 @@ var init_server = __esm(() => {
7876
8264
  init_manifest();
7877
8265
  init_resolve();
7878
8266
  init_store();
8267
+ init_redact();
7879
8268
  init_storage();
7880
8269
  __dirname3 = dirname5(fileURLToPath3(import.meta.url));
7881
8270
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
@@ -7889,6 +8278,7 @@ var init_server = __esm(() => {
7889
8278
  }
7890
8279
  } catch {}
7891
8280
  _hooksAgents = new Map;
8281
+ MCP_SSE_AUTH_ENV = ["HASNA_HOOKS_MCP_TOKEN", "HOOKS_MCP_TOKEN"];
7892
8282
  });
7893
8283
 
7894
8284
  // src/mcp/http.ts
@@ -7950,7 +8340,7 @@ function startMcpHttpServer(options) {
7950
8340
  var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
7951
8341
  var init_http = () => {};
7952
8342
 
7953
- // ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
8343
+ // node_modules/@hasna/events/dist/commander.js
7954
8344
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
7955
8345
  import { Buffer as Buffer2 } from "buffer";
7956
8346
  import { existsSync } from "fs";
@@ -9135,7 +9525,7 @@ function replaySummary(events, deliveries, nextCursor) {
9135
9525
  // src/cli/index.tsx
9136
9526
  import { render } from "ink";
9137
9527
 
9138
- // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/esm.mjs
9528
+ // node_modules/commander/esm.mjs
9139
9529
  var import__ = __toESM(require_commander(), 1);
9140
9530
  var {
9141
9531
  program,
@@ -9161,11 +9551,11 @@ import { fileURLToPath as fileURLToPath4 } from "url";
9161
9551
  import { useState as useState7 } from "react";
9162
9552
  import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
9163
9553
 
9164
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
9554
+ // node_modules/ink-select-input/build/Indicator.js
9165
9555
  import React from "react";
9166
9556
  import { Box, Text } from "ink";
9167
9557
 
9168
- // ../../node_modules/.bun/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js
9558
+ // node_modules/is-unicode-supported/index.js
9169
9559
  import process2 from "process";
9170
9560
  function isUnicodeSupported() {
9171
9561
  const { env } = process2;
@@ -9176,7 +9566,7 @@ function isUnicodeSupported() {
9176
9566
  return Boolean(env.WT_SESSION) || Boolean(env.TERMINUS_SUBLIME) || env.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
9177
9567
  }
9178
9568
 
9179
- // ../../node_modules/.bun/figures@6.1.0/node_modules/figures/index.js
9569
+ // node_modules/figures/index.js
9180
9570
  var common = {
9181
9571
  circleQuestionMark: "(?)",
9182
9572
  questionMarkPrefix: "(?)",
@@ -9452,23 +9842,23 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
9452
9842
  var figures_default = figures;
9453
9843
  var replacements = Object.entries(specialMainSymbols);
9454
9844
 
9455
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
9845
+ // node_modules/ink-select-input/build/Indicator.js
9456
9846
  function Indicator({ isSelected = false }) {
9457
9847
  return React.createElement(Box, { marginRight: 1 }, isSelected ? React.createElement(Text, { color: "blue" }, figures_default.pointer) : React.createElement(Text, null, " "));
9458
9848
  }
9459
9849
  var Indicator_default = Indicator;
9460
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Item.js
9850
+ // node_modules/ink-select-input/build/Item.js
9461
9851
  import * as React2 from "react";
9462
9852
  import { Text as Text2 } from "ink";
9463
9853
  function Item({ isSelected = false, label }) {
9464
9854
  return React2.createElement(Text2, { color: isSelected ? "blue" : undefined }, label);
9465
9855
  }
9466
9856
  var Item_default = Item;
9467
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
9857
+ // node_modules/ink-select-input/build/SelectInput.js
9468
9858
  import { isDeepStrictEqual } from "util";
9469
9859
  import React3, { useState, useEffect, useRef, useCallback } from "react";
9470
9860
 
9471
- // ../../node_modules/.bun/to-rotated@1.0.0/node_modules/to-rotated/index.js
9861
+ // node_modules/to-rotated/index.js
9472
9862
  function toRotated(array, steps) {
9473
9863
  if (!Array.isArray(array)) {
9474
9864
  throw new TypeError(`Expected an array, got \`${typeof array}\`.`);
@@ -9490,7 +9880,7 @@ function toRotated(array, steps) {
9490
9880
  ];
9491
9881
  }
9492
9882
 
9493
- // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
9883
+ // node_modules/ink-select-input/build/SelectInput.js
9494
9884
  import { Box as Box2, useInput } from "ink";
9495
9885
  function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent = Indicator_default, itemComponent = Item_default, limit: customLimit, onSelect, onHighlight }) {
9496
9886
  const hasLimit = typeof customLimit === "number" && items.length > customLimit;
@@ -9786,7 +10176,7 @@ function HookSelect({
9786
10176
  import { useState as useState4, useEffect as useEffect3 } from "react";
9787
10177
  import { Box as Box6, Text as Text7, useInput as useInput4 } from "ink";
9788
10178
 
9789
- // ../../node_modules/.bun/ink-text-input@6.0.0+fa875fef9f949ebd/node_modules/ink-text-input/build/index.js
10179
+ // node_modules/ink-text-input/build/index.js
9790
10180
  import React5, { useState as useState3, useEffect as useEffect2 } from "react";
9791
10181
  import { Text as Text6, useInput as useInput3 } from "ink";
9792
10182
  import chalk from "chalk";
@@ -9964,7 +10354,7 @@ function SearchView({
9964
10354
  import { useState as useState6, useEffect as useEffect5 } from "react";
9965
10355
  import { Box as Box7, Text as Text9 } from "ink";
9966
10356
 
9967
- // ../../node_modules/.bun/ink-spinner@5.0.0+fa875fef9f949ebd/node_modules/ink-spinner/build/index.js
10357
+ // node_modules/ink-spinner/build/index.js
9968
10358
  var import_cli_spinners = __toESM(require_cli_spinners(), 1);
9969
10359
  import React7, { useState as useState5, useEffect as useEffect4 } from "react";
9970
10360
  import { Text as Text8 } from "ink";
@@ -10329,10 +10719,12 @@ function App({ initialHooks, overwrite = false }) {
10329
10719
  // src/cli/index.tsx
10330
10720
  init_registry();
10331
10721
  init_installer();
10722
+ init_redact();
10332
10723
  init_profiles();
10333
10724
  init_manifest();
10334
10725
  init_resolve();
10335
10726
  init_store();
10727
+ init_semver();
10336
10728
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
10337
10729
  var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
10338
10730
  var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
@@ -10364,6 +10756,9 @@ function truncateText2(value, max = 96) {
10364
10756
  return text;
10365
10757
  return `${text.slice(0, Math.max(0, max - 3))}...`;
10366
10758
  }
10759
+ function projectLogRows2(rows) {
10760
+ return rows.map((row) => projectEventRowForRead({ ...row }));
10761
+ }
10367
10762
  function readToken(tokenFile) {
10368
10763
  if (tokenFile) {
10369
10764
  try {
@@ -10625,6 +11020,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
10625
11020
  console.log(chalk2.red(`Unknown category: ${options.category}`));
10626
11021
  console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
10627
11022
  }
11023
+ process.exitCode = 1;
10628
11024
  return;
10629
11025
  }
10630
11026
  toInstall = getHooksByCategory(category).map((h) => h.name);
@@ -10657,7 +11053,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
10657
11053
  const version = arg.slice(at + 1);
10658
11054
  if (!HOOK_NAME_RE2.test(name))
10659
11055
  return null;
10660
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
11056
+ if (!SEMVER_PATTERN.test(version))
10661
11057
  return null;
10662
11058
  return { name, version };
10663
11059
  }
@@ -10791,7 +11187,7 @@ Dry run \u2014 would install (${scope}, ${target}):
10791
11187
  target,
10792
11188
  applied: results.some((r) => r.applied)
10793
11189
  }));
10794
- if (results.length > 0 && results.every((r) => !r.success)) {
11190
+ if (results.some((r) => !r.success)) {
10795
11191
  process.exitCode = 1;
10796
11192
  }
10797
11193
  return;
@@ -10829,6 +11225,11 @@ Installing hooks (${scope}, ${target})...
10829
11225
  process.exitCode = 1;
10830
11226
  return;
10831
11227
  }
11228
+ if (successCount < results.length) {
11229
+ console.log(chalk2.red(`
11230
+ \u2717 ${results.length - successCount} of ${results.length} hook(s) failed to install.`));
11231
+ process.exitCode = 1;
11232
+ }
10832
11233
  console.log(chalk2.dim(`
10833
11234
  Registered in ${settingsFile}`));
10834
11235
  });
@@ -11024,6 +11425,7 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
11024
11425
  } else {
11025
11426
  console.log(chalk2.red(`Hook '${hook}' not found${hint}`));
11026
11427
  }
11428
+ process.exitCode = 1;
11027
11429
  return;
11028
11430
  }
11029
11431
  const globalInstalled = getRegisteredHooks("global").includes(meta.name);
@@ -11070,6 +11472,13 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11070
11472
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
11071
11473
  }
11072
11474
  const registered = getRegisteredHooks(scope);
11475
+ let wiringCount = 0;
11476
+ if (settingsExist) {
11477
+ try {
11478
+ const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
11479
+ wiringCount = countSettingsWiring(settings);
11480
+ } catch {}
11481
+ }
11073
11482
  for (const name of registered) {
11074
11483
  const custom = readCustomManifest(name);
11075
11484
  const meta = custom ? resolveHookMeta(name) : getHook(name);
@@ -11107,7 +11516,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11107
11516
  }
11108
11517
  }
11109
11518
  if (options.json) {
11110
- console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }));
11519
+ console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, wiring_count: wiringCount, scope }));
11111
11520
  return;
11112
11521
  }
11113
11522
  console.log(chalk2.bold(`
@@ -11116,6 +11525,9 @@ Hook Health Check (${scope})
11116
11525
  if (registered.length === 0) {
11117
11526
  console.log(chalk2.dim(" No hooks registered."));
11118
11527
  console.log(chalk2.dim(" Run: hooks install gitguard"));
11528
+ console.log(chalk2.dim(` (checked 0 registered \`hooks run\` entries of ${wiringCount} settings wiring entries; direct-path wiring outside the registered surface is not covered by this check)`));
11529
+ if (issues.length > 0)
11530
+ process.exitCode = 1;
11119
11531
  return;
11120
11532
  }
11121
11533
  if (healthy.length > 0) {
@@ -11124,6 +11536,9 @@ Hook Health Check (${scope})
11124
11536
  console.log(chalk2.green(` ${name}`));
11125
11537
  }
11126
11538
  }
11539
+ if (registered.length > 0) {
11540
+ console.log(chalk2.dim(` (checked ${registered.length} registered \`hooks run\` entries of ${wiringCount} settings wiring entries; direct-path wiring outside the registered surface is not covered by this check)`));
11541
+ }
11127
11542
  if (issues.length > 0) {
11128
11543
  console.log();
11129
11544
  for (const issue of issues) {
@@ -11131,9 +11546,11 @@ Hook Health Check (${scope})
11131
11546
  console.log(` ${icon} ${chalk2.cyan(issue.hook)}: ${issue.issue}`);
11132
11547
  }
11133
11548
  }
11134
- if (issues.length === 0) {
11549
+ if (issues.length === 0 && registered.length > 0) {
11135
11550
  console.log(chalk2.green(`
11136
- All hooks healthy!`));
11551
+ All ${healthy.length} registered hook(s) healthy`));
11552
+ } else if (issues.some((i) => i.severity === "error")) {
11553
+ process.exitCode = 1;
11137
11554
  }
11138
11555
  console.log();
11139
11556
  });
@@ -11166,7 +11583,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
11166
11583
  const version = arg.slice(at + 1);
11167
11584
  if (!HOOK_NAME_RE2.test(name))
11168
11585
  return null;
11169
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
11586
+ if (!SEMVER_PATTERN.test(version))
11170
11587
  return null;
11171
11588
  return { name, version };
11172
11589
  }
@@ -11249,6 +11666,7 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
11249
11666
  } else {
11250
11667
  console.log(chalk2.red(`Hook '${hook}' not found`));
11251
11668
  }
11669
+ process.exitCode = 1;
11252
11670
  return;
11253
11671
  }
11254
11672
  const hookPath = getHookPath(hook);
@@ -11503,18 +11921,19 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
11503
11921
  sql += " ORDER BY timestamp DESC LIMIT ?";
11504
11922
  params.push(String(limit));
11505
11923
  const rows = db.query(sql).all(...params);
11924
+ const projected = projectLogRows2(rows);
11506
11925
  if (options.json) {
11507
- console.log(JSON.stringify(rows, null, 2));
11926
+ console.log(JSON.stringify(projected, null, 2));
11508
11927
  return;
11509
11928
  }
11510
- if (rows.length === 0) {
11929
+ if (projected.length === 0) {
11511
11930
  console.log(chalk2.dim("No events found."));
11512
11931
  return;
11513
11932
  }
11514
11933
  console.log(chalk2.bold(`
11515
- Hook Events (${rows.length})
11934
+ Hook Events (${projected.length})
11516
11935
  `));
11517
- for (const row of rows) {
11936
+ for (const row of projected) {
11518
11937
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11519
11938
  const err = row.error ? chalk2.red(` ERR: ${truncateText2(row.error, 60)}`) : "";
11520
11939
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
@@ -11529,18 +11948,19 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
11529
11948
  const limit = parseInt(options.limit) || 50;
11530
11949
  const q = `%${text}%`;
11531
11950
  const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
11951
+ const projected = projectLogRows2(rows);
11532
11952
  if (options.json) {
11533
- console.log(JSON.stringify(rows, null, 2));
11953
+ console.log(JSON.stringify(projected, null, 2));
11534
11954
  return;
11535
11955
  }
11536
- if (rows.length === 0) {
11956
+ if (projected.length === 0) {
11537
11957
  console.log(chalk2.dim(`No events matching "${text}".`));
11538
11958
  return;
11539
11959
  }
11540
11960
  console.log(chalk2.bold(`
11541
- Search results for "${text}" (${rows.length})
11961
+ Search results for "${text}" (${projected.length})
11542
11962
  `));
11543
- for (const row of rows) {
11963
+ for (const row of projected) {
11544
11964
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11545
11965
  const snippet = truncateText2(row.tool_input || row.error || "", 80);
11546
11966
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
@@ -11553,18 +11973,19 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
11553
11973
  const db = getDb2();
11554
11974
  const limit = parseInt(options.n) || 20;
11555
11975
  const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
11976
+ const projected = projectLogRows2(rows);
11556
11977
  if (options.json) {
11557
- console.log(JSON.stringify(rows, null, 2));
11978
+ console.log(JSON.stringify(projected, null, 2));
11558
11979
  return;
11559
11980
  }
11560
- if (rows.length === 0) {
11981
+ if (projected.length === 0) {
11561
11982
  console.log(chalk2.dim("No events yet."));
11562
11983
  return;
11563
11984
  }
11564
11985
  console.log(chalk2.bold(`
11565
- Last ${rows.length} events
11986
+ Last ${projected.length} events
11566
11987
  `));
11567
- for (const row of rows) {
11988
+ for (const row of projected) {
11568
11989
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11569
11990
  const err = row.error ? chalk2.red(` \u2717 ${truncateText2(row.error, 60)}`) : "";
11570
11991
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
@@ -11597,18 +12018,19 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
11597
12018
  }
11598
12019
  const since = new Date(Date.now() - parseDuration(options.since)).toISOString();
11599
12020
  const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(since, limit);
12021
+ const projected = projectLogRows2(rows);
11600
12022
  if (options.json) {
11601
- console.log(JSON.stringify(rows, null, 2));
12023
+ console.log(JSON.stringify(projected, null, 2));
11602
12024
  return;
11603
12025
  }
11604
- if (rows.length === 0) {
12026
+ if (projected.length === 0) {
11605
12027
  console.log(chalk2.dim(`No errors in the last ${options.since}.`));
11606
12028
  return;
11607
12029
  }
11608
12030
  console.log(chalk2.bold(`
11609
- Errors (last ${options.since}, ${rows.length} found)
12031
+ Errors (last ${options.since}, ${projected.length} found)
11610
12032
  `));
11611
- for (const row of rows) {
12033
+ for (const row of projected) {
11612
12034
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11613
12035
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(truncateText2(row.error, 100))}`);
11614
12036
  }
@@ -11730,18 +12152,17 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
11730
12152
  console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
11731
12153
  console.log(chalk2.dim(` sha256 ${result.actual}`));
11732
12154
  });
11733
- program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").option("--api-key <key>", "API key for publish (defaults to HASNA_HOOKS_API_KEY / HOOKS_API_KEY)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock)").action(async (options) => {
12155
+ program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock). The publish API key resolves from HASNA_HOOKS_API_KEY / HOOKS_API_KEY only \u2014 there is deliberately no --api-key flag (P1-8).").action(async (options) => {
11734
12156
  const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
11735
12157
  startServeServer2({
11736
12158
  port: options.port ? parseInt(options.port, 10) : undefined,
11737
- host: options.host,
11738
- apiKey: options.apiKey
12159
+ host: options.host
11739
12160
  });
11740
12161
  });
11741
12162
  program2.command("sync").option("--dry-run", "Print the plan without changing anything", false).option("-j, --json", "Output as JSON", false).description("Sync local hooks with the remote registry (or bundled registry when no API URL is configured)").action(async (options) => {
11742
12163
  const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
11743
12164
  try {
11744
- const plan = options.dryRun ? await planSync2() : await syncHooks2();
12165
+ const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
11745
12166
  const diff = plan.diff;
11746
12167
  if (options.json) {
11747
12168
  console.log(JSON.stringify({ dry_run: plan.dryRun, api_url: plan.apiUrl ?? null, diff }));
@@ -11837,7 +12258,7 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
11837
12258
  await startStdioServer2();
11838
12259
  } else if (options.sse) {
11839
12260
  const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
11840
- await startSSEServer2(options.port ? parseInt(options.port) : 39427);
12261
+ await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
11841
12262
  } else {
11842
12263
  const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
11843
12264
  const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));