@hasna/hooks 0.6.5 → 0.6.8

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
@@ -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 NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
4129
+ var init_semver = __esm(() => {
4130
+ SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[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,15 +4279,18 @@ 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
- timeout_ms: z.number().int().positive().optional()
4292
+ timeout_ms: z.number().int().positive().optional(),
4293
+ env: z.record(z.string(), z.string()).optional()
4276
4294
  });
4277
4295
  ScriptContainmentError = class ScriptContainmentError extends Error {
4278
4296
  constructor(path, hookDir, raw) {
@@ -4799,11 +4817,13 @@ __export(exports_store, {
4799
4817
  listHookRecords: () => listHookRecords,
4800
4818
  getPinnedHook: () => getPinnedHook,
4801
4819
  getHookRecord: () => getHookRecord,
4802
- checkScriptHash: () => checkScriptHash
4820
+ checkScriptHash: () => checkScriptHash,
4821
+ LockFileError: () => LockFileError
4803
4822
  });
4804
4823
  import { createHash } from "crypto";
4805
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4824
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync } from "fs";
4806
4825
  import { readFile as readFile2 } from "fs/promises";
4826
+ import { randomBytes } from "crypto";
4807
4827
  function sha256Of(input) {
4808
4828
  return createHash("sha256").update(input).digest("hex");
4809
4829
  }
@@ -4846,16 +4866,39 @@ function removeHookRecord(db, name) {
4846
4866
  const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
4847
4867
  return res.changes > 0;
4848
4868
  }
4869
+ function assertLockShape(parsed, path) {
4870
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4871
+ 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.`);
4872
+ }
4873
+ const hooks = parsed.hooks;
4874
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
4875
+ 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.`);
4876
+ }
4877
+ for (const [name, entry] of Object.entries(hooks)) {
4878
+ const e = entry;
4879
+ if (e === null || typeof e !== "object" || typeof e.version !== "string" || typeof e.sha256 !== "string") {
4880
+ 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.`);
4881
+ }
4882
+ }
4883
+ }
4849
4884
  function readLock() {
4885
+ const path = getLockPath();
4886
+ if (!existsSync7(path))
4887
+ return { hooks: {} };
4888
+ let raw;
4850
4889
  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 ?? {} };
4890
+ raw = readFileSync4(path, "utf-8");
4856
4891
  } catch {
4857
- return { hooks: {} };
4892
+ throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
4893
+ }
4894
+ let parsed;
4895
+ try {
4896
+ parsed = JSON.parse(raw);
4897
+ } catch {
4898
+ 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.`);
4858
4899
  }
4900
+ assertLockShape(parsed, path);
4901
+ return parsed;
4859
4902
  }
4860
4903
  function writeLock(lock) {
4861
4904
  const path = getLockPath();
@@ -4864,8 +4907,10 @@ function writeLock(lock) {
4864
4907
  for (const name of Object.keys(lock.hooks).sort()) {
4865
4908
  sorted[name] = lock.hooks[name];
4866
4909
  }
4867
- writeFileSync3(path, JSON.stringify({ hooks: sorted }, null, 2) + `
4910
+ const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
4911
+ writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
4868
4912
  `, "utf-8");
4913
+ renameSync(tmp, path);
4869
4914
  return path;
4870
4915
  }
4871
4916
  function setPinnedHook(name, entry) {
@@ -4951,9 +4996,16 @@ function retrustHook(name, scriptPath, version, source) {
4951
4996
  setPinnedHook(name, { version, sha256: actual, source });
4952
4997
  return { ok: true, pinned: true, expected: actual, actual, name };
4953
4998
  }
4999
+ var LockFileError;
4954
5000
  var init_store = __esm(() => {
4955
5001
  init_db();
4956
5002
  init_config();
5003
+ LockFileError = class LockFileError extends Error {
5004
+ constructor(message) {
5005
+ super(message);
5006
+ this.name = "LockFileError";
5007
+ }
5008
+ };
4957
5009
  });
4958
5010
 
4959
5011
  // src/lib/installer.ts
@@ -5006,7 +5058,7 @@ function getGlobalSettingsPathOverride(target) {
5006
5058
  function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
5007
5059
  if (target === "codewith" && codewithConfigPath)
5008
5060
  return codewithConfigPath;
5009
- if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
5061
+ if (target === "codewith" && scope === "global" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
5010
5062
  return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
5011
5063
  }
5012
5064
  const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
@@ -5439,8 +5491,8 @@ function escapeRegExp(value) {
5439
5491
  function codewithHasHookEntryInText(configText, name) {
5440
5492
  return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
5441
5493
  }
5442
- function codewithHasHookEntry(name) {
5443
- const path = getSettingsPath("global", "codewith");
5494
+ function codewithHasHookEntry(name, scope = "global") {
5495
+ const path = getSettingsPath(scope, "codewith");
5444
5496
  if (!existsSync8(path))
5445
5497
  return false;
5446
5498
  try {
@@ -5457,7 +5509,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
5457
5509
  const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
5458
5510
  const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
5459
5511
  const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
5460
- const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
5512
+ const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
5461
5513
  if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
5462
5514
  return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
5463
5515
  }
@@ -5474,7 +5526,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
5474
5526
  }
5475
5527
  const registrationsRemaining = [];
5476
5528
  if (target === "codewith" || target === "all") {
5477
- const codewithPath = getSettingsPath("global", "codewith");
5529
+ const codewithPath = getSettingsPath(scope, "codewith");
5478
5530
  if (existsSync8(codewithPath)) {
5479
5531
  const before = readFileSync5(codewithPath, "utf-8");
5480
5532
  const after = removeCodewithHookEntry(before, shortName);
@@ -5566,6 +5618,22 @@ function safeMatcherTest(pattern, value) {
5566
5618
  return false;
5567
5619
  }
5568
5620
  }
5621
+ function countSettingsWiring(settings) {
5622
+ const hooks = settings.hooks;
5623
+ if (!hooks || typeof hooks !== "object")
5624
+ return 0;
5625
+ let count = 0;
5626
+ for (const eventKey of Object.keys(hooks)) {
5627
+ const entries = hooks[eventKey];
5628
+ if (!Array.isArray(entries))
5629
+ continue;
5630
+ for (const entry of entries) {
5631
+ if (Array.isArray(entry?.hooks))
5632
+ count += entry.hooks.length;
5633
+ }
5634
+ }
5635
+ return count;
5636
+ }
5569
5637
  function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
5570
5638
  const separator = eventSpec.indexOf(":");
5571
5639
  const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
@@ -5587,6 +5655,71 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
5587
5655
  }));
5588
5656
  }
5589
5657
 
5658
+ // src/lib/redact.ts
5659
+ function redactText(value) {
5660
+ let out = value;
5661
+ for (const pattern of SECRET_SHAPES) {
5662
+ out = out.replace(pattern, REDACTED);
5663
+ }
5664
+ return out;
5665
+ }
5666
+ function redactValue2(value) {
5667
+ if (typeof value === "string")
5668
+ return redactText(value);
5669
+ if (Array.isArray(value))
5670
+ return value.map(redactValue2);
5671
+ if (value !== null && typeof value === "object") {
5672
+ const out = {};
5673
+ for (const [key, child] of Object.entries(value)) {
5674
+ out[key] = SECRET_KEY_NAMES.test(key) ? REDACTED : redactValue2(child);
5675
+ }
5676
+ return out;
5677
+ }
5678
+ return value;
5679
+ }
5680
+ function redactEventPayload(text) {
5681
+ if (text === null || text === undefined)
5682
+ return null;
5683
+ try {
5684
+ const parsed = JSON.parse(text);
5685
+ return redactText(JSON.stringify(redactValue2(parsed)));
5686
+ } catch {
5687
+ return redactText(text);
5688
+ }
5689
+ }
5690
+ function projectEventRowForRead(row) {
5691
+ const out = { ...row };
5692
+ for (const field of ["tool_input", "error", "metadata"]) {
5693
+ const value = out[field];
5694
+ if (typeof value === "string")
5695
+ out[field] = redactEventPayload(value);
5696
+ else if (value === null)
5697
+ out[field] = null;
5698
+ }
5699
+ return out;
5700
+ }
5701
+ var REDACTED = "[REDACTED]", SECRET_KEY_NAMES, SECRET_SHAPES;
5702
+ var init_redact = __esm(() => {
5703
+ 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;
5704
+ SECRET_SHAPES = [
5705
+ /\bsk-(?:proj|svc|ant)-[A-Za-z0-9_-]{10,}/g,
5706
+ /\bsk-[A-Za-z0-9]{16,}/g,
5707
+ /\btpe_[A-Za-z0-9]{8,}/g,
5708
+ /\b(?:rk|sk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
5709
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
5710
+ /gh[pousr]_[A-Za-z0-9]{20,}/g,
5711
+ /xox[baprs]-[A-Za-z0-9-]{20,}/g,
5712
+ /AKIA[0-9A-Z]{16}/g,
5713
+ /ASIA[0-9A-Z]{16}/g,
5714
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
5715
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
5716
+ /-----BEGIN OPENSSH PRIVATE KEY-----/g,
5717
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
5718
+ /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
5719
+ /\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
5720
+ ];
5721
+ });
5722
+
5590
5723
  // src/lib/profiles.ts
5591
5724
  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
5725
  import { join as join8 } from "path";
@@ -5708,6 +5841,9 @@ function writeHookEvent(event) {
5708
5841
  const db = getDb();
5709
5842
  const id = nanoid2();
5710
5843
  const timestamp = event.timestamp ?? new Date().toISOString();
5844
+ const toolInput = redactEventPayload(event.tool_input);
5845
+ const error = redactEventPayload(event.error);
5846
+ const metadata = redactEventPayload(event.metadata);
5711
5847
  db.run(`INSERT INTO hook_events
5712
5848
  (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
5713
5849
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
@@ -5717,12 +5853,12 @@ function writeHookEvent(event) {
5717
5853
  event.hook_name,
5718
5854
  event.event_type,
5719
5855
  event.tool_name ?? null,
5720
- event.tool_input ? event.tool_input.slice(0, 500) : null,
5856
+ toolInput ? toolInput.slice(0, 500) : null,
5721
5857
  event.result ?? null,
5722
- event.error ?? null,
5858
+ error ?? null,
5723
5859
  event.duration_ms ?? null,
5724
5860
  event.project_dir ?? null,
5725
- event.metadata ?? null
5861
+ metadata ?? null
5726
5862
  ]);
5727
5863
  } catch (err) {
5728
5864
  process.stderr.write(`[hooks db-writer] failed to write event: ${err}
@@ -5772,6 +5908,7 @@ function recordHookRun(record) {
5772
5908
  var ALLOWED_EVENT_TYPES;
5773
5909
  var init_db_writer = __esm(() => {
5774
5910
  init_db();
5911
+ init_redact();
5775
5912
  ALLOWED_EVENT_TYPES = new Set([
5776
5913
  "PreToolUse",
5777
5914
  "PostToolUse",
@@ -5784,15 +5921,169 @@ var init_db_writer = __esm(() => {
5784
5921
  ]);
5785
5922
  });
5786
5923
 
5924
+ // src/lib/hook-env.ts
5925
+ import { dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
5926
+ import { existsSync as existsSync10, statSync } from "fs";
5927
+ function isDeniedEnvName(name) {
5928
+ const upper = name.toUpperCase();
5929
+ if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
5930
+ return true;
5931
+ if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
5932
+ return true;
5933
+ return DENY_CONTAINS.some((token) => upper.includes(token));
5934
+ }
5935
+ function isInterpreterInjectionEnvName(name) {
5936
+ if (INTERPRETER_INJECTION.has(name))
5937
+ return true;
5938
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
5939
+ }
5940
+ function isBashFunctionEnvName(name) {
5941
+ return name.startsWith("BASH_FUNC_");
5942
+ }
5943
+ function isUnsafePathEntry(entry, home) {
5944
+ if (entry === "" || !isAbsolute2(entry))
5945
+ return true;
5946
+ if (home && (entry === home || entry.startsWith(home + "/")))
5947
+ return true;
5948
+ if (entry === "/tmp" || entry.startsWith("/tmp/"))
5949
+ return true;
5950
+ if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
5951
+ return true;
5952
+ try {
5953
+ if ((statSync(entry).mode & 2) !== 0)
5954
+ return true;
5955
+ } catch {}
5956
+ return false;
5957
+ }
5958
+ function sanitizeHookPath(pathValue, home) {
5959
+ const baseline = PATH_BASELINE.slice();
5960
+ if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
5961
+ baseline.push("/opt/homebrew/bin");
5962
+ const bunDir = dirname4(process.execPath);
5963
+ const seen = new Set;
5964
+ const out = [];
5965
+ for (const dir of baseline) {
5966
+ if (seen.has(dir))
5967
+ continue;
5968
+ seen.add(dir);
5969
+ out.push(dir);
5970
+ }
5971
+ if (!seen.has(bunDir)) {
5972
+ seen.add(bunDir);
5973
+ out.push(bunDir);
5974
+ }
5975
+ for (const entry of pathValue.split(":")) {
5976
+ if (seen.has(entry))
5977
+ continue;
5978
+ if (isUnsafePathEntry(entry, home))
5979
+ continue;
5980
+ seen.add(entry);
5981
+ out.push(entry);
5982
+ }
5983
+ return out.join(":");
5984
+ }
5985
+ function buildHookEnv(source, extra) {
5986
+ const out = {};
5987
+ for (const name of ALLOWLIST) {
5988
+ if (name === "PATH")
5989
+ continue;
5990
+ const value = source[name] ?? extra?.[name];
5991
+ if (value !== undefined && value !== null)
5992
+ out[name] = value;
5993
+ }
5994
+ const pathOverride = extra?.PATH;
5995
+ if (pathOverride !== undefined && pathOverride !== null) {
5996
+ out.PATH = pathOverride;
5997
+ } else {
5998
+ const parentPath = source.PATH;
5999
+ if (parentPath !== undefined && parentPath !== null) {
6000
+ out.PATH = sanitizeHookPath(parentPath, source.HOME ?? extra?.HOME);
6001
+ }
6002
+ }
6003
+ for (const [from, to] of CONFIG_PROJECTIONS) {
6004
+ const value = source[from] ?? extra?.[from];
6005
+ if (value !== undefined && value !== null)
6006
+ out[to] = value;
6007
+ }
6008
+ const merged = { ...source, ...extra ?? {} };
6009
+ for (const [name, value] of Object.entries(merged)) {
6010
+ if (out[name] !== undefined)
6011
+ continue;
6012
+ if (isDeniedEnvName(name))
6013
+ continue;
6014
+ if (isInterpreterInjectionEnvName(name))
6015
+ continue;
6016
+ if (isBashFunctionEnvName(name))
6017
+ continue;
6018
+ if (value !== undefined && value !== null)
6019
+ out[name] = value;
6020
+ }
6021
+ return out;
6022
+ }
6023
+ var ALLOWLIST, INTERPRETER_INJECTION, INTERPRETER_INJECTION_PREFIXES, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, PATH_BASELINE, CONFIG_PROJECTIONS;
6024
+ var init_hook_env = __esm(() => {
6025
+ ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
6026
+ INTERPRETER_INJECTION = new Set([
6027
+ "BASH_ENV",
6028
+ "ENV",
6029
+ "BASHOPTS",
6030
+ "SHELLOPTS",
6031
+ "NODE_OPTIONS",
6032
+ "NODE_PATH",
6033
+ "PYTHONSTARTUP",
6034
+ "PYTHONINSPECT",
6035
+ "PYTHONPATH",
6036
+ "PYTHONHOME",
6037
+ "LD_PRELOAD",
6038
+ "LD_LIBRARY_PATH",
6039
+ "GCONV_PATH",
6040
+ "LOCPATH",
6041
+ "PERL5OPT",
6042
+ "RUBYOPT",
6043
+ "NODE_EXTRA_CA_CERTS",
6044
+ "NODE_TLS_REJECT_UNAUTHORIZED",
6045
+ "SSL_CERT_FILE",
6046
+ "SSL_CERT_DIR",
6047
+ "CURL_CA_BUNDLE",
6048
+ "GIT_SSL_CAINFO",
6049
+ "AWS_CA_BUNDLE"
6050
+ ]);
6051
+ INTERPRETER_INJECTION_PREFIXES = ["GIT_CONFIG_"];
6052
+ DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
6053
+ DENY_PREFIXES = [
6054
+ "HASNA_",
6055
+ "AWS_",
6056
+ "AZURE_",
6057
+ "GCP_",
6058
+ "VAULT_",
6059
+ "GOOGLE_",
6060
+ "OPENAI_",
6061
+ "ANTHROPIC_",
6062
+ "POSTGRES_",
6063
+ "MYSQL_",
6064
+ "REDIS_",
6065
+ "MONGO_",
6066
+ "MEMENTOS_"
6067
+ ];
6068
+ DENY_CONTAINS = ["DATABASE_URL"];
6069
+ PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
6070
+ CONFIG_PROJECTIONS = [
6071
+ ["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
6072
+ ["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
6073
+ ["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
6074
+ ["HASNA_HOOKS_CONFIG_PATH", "HOOKS_CONFIG_PATH"]
6075
+ ];
6076
+ });
6077
+
5787
6078
  // src/lib/run.ts
5788
6079
  var exports_run = {};
5789
6080
  __export(exports_run, {
5790
6081
  executeVerifiedScript: () => executeVerifiedScript,
5791
6082
  HookTimeoutError: () => HookTimeoutError
5792
6083
  });
5793
- import { randomBytes } from "crypto";
6084
+ import { randomBytes as randomBytes2 } from "crypto";
5794
6085
  import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
5795
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
6086
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join9 } from "path";
5796
6087
  function extensionOf(scriptPath) {
5797
6088
  const base = basename2(scriptPath);
5798
6089
  const dot = base.lastIndexOf(".");
@@ -5819,13 +6110,13 @@ function interpreterFor(name, scriptPath, content) {
5819
6110
  if (interp === "bash" || interp === "sh")
5820
6111
  return { command: ["/bin/bash"], tempExt: "sh" };
5821
6112
  if (interp === "node" || interp === "bun")
5822
- return { command: ["bun", "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
6113
+ return { command: [process.execPath, "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
5823
6114
  throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
5824
6115
  }
5825
6116
  if (BASH_EXTENSIONS.has(ext))
5826
6117
  return { command: ["/bin/bash"], tempExt: "sh" };
5827
6118
  if (BUN_EXTENSIONS.has(ext))
5828
- return { command: ["bun", "run"], tempExt: ext };
6119
+ return { command: [process.execPath, "run"], tempExt: ext };
5829
6120
  throw new Error(`Refusing to run hook '${name}': unsupported script extension '.${ext}' (supported: .sh .bash .ts .tsx .js .jsx .mjs .cjs .mts .cts, or a recognized shebang)`);
5830
6121
  }
5831
6122
  async function readPipeWithDeadline(stream, deadlineMs) {
@@ -5861,9 +6152,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
5861
6152
  return chunks.join("");
5862
6153
  }
5863
6154
  async function executeVerifiedScript(options) {
5864
- const scriptDir = dirname4(options.scriptPath);
6155
+ const scriptDir = dirname5(options.scriptPath);
5865
6156
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
5866
- const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${interpreter.tempExt}`);
6157
+ const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5867
6158
  let proc = null;
5868
6159
  let timedOut = false;
5869
6160
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -5872,11 +6163,11 @@ async function executeVerifiedScript(options) {
5872
6163
  }, options.timeout) : null;
5873
6164
  try {
5874
6165
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
5875
- proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6166
+ proc = Bun.spawn([...interpreter.command, isAbsolute3(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5876
6167
  stdin: new Response(options.stdin),
5877
6168
  stdout: "pipe",
5878
6169
  stderr: "pipe",
5879
- env: options.env ?? process.env,
6170
+ env: buildHookEnv(process.env, options.env),
5880
6171
  detached: true
5881
6172
  });
5882
6173
  const exitCode = await proc.exited;
@@ -5934,6 +6225,7 @@ function killGroup(proc, options) {
5934
6225
  }
5935
6226
  var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
5936
6227
  var init_run = __esm(() => {
6228
+ init_hook_env();
5937
6229
  HookTimeoutError = class HookTimeoutError extends Error {
5938
6230
  timeoutMs;
5939
6231
  constructor(timeoutMs) {
@@ -5953,12 +6245,12 @@ __export(exports_custom_install, {
5953
6245
  installCustomSource: () => installCustomSource,
5954
6246
  ensureCustomDir: () => ensureCustomDir
5955
6247
  });
5956
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
6248
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
5957
6249
  import { join as join10 } from "path";
5958
6250
  import { tmpdir } from "os";
5959
6251
  function manifestAt(dir) {
5960
6252
  const path = join10(dir, "manifest.json");
5961
- if (!existsSync10(path))
6253
+ if (!existsSync11(path))
5962
6254
  return;
5963
6255
  try {
5964
6256
  return parseManifest(readFileSync7(path, "utf-8"));
@@ -6004,7 +6296,7 @@ function isCustomSource(value) {
6004
6296
  return true;
6005
6297
  if (value.startsWith("http://") || value.startsWith("https://"))
6006
6298
  return true;
6007
- return existsSync10(value);
6299
+ return existsSync11(value);
6008
6300
  }
6009
6301
  async function installCustomSource(source) {
6010
6302
  if (isGitUrl(source)) {
@@ -6018,7 +6310,7 @@ async function installCustomSource(source) {
6018
6310
  if (source.startsWith("http://") || source.startsWith("https://")) {
6019
6311
  return installManifestUrl(source);
6020
6312
  }
6021
- if (!existsSync10(source)) {
6313
+ if (!existsSync11(source)) {
6022
6314
  throw new Error(`No such local path: ${source}`);
6023
6315
  }
6024
6316
  if (source.endsWith("manifest.json")) {
@@ -6028,19 +6320,20 @@ async function installCustomSource(source) {
6028
6320
  return copyManifestDir(source, source, "local");
6029
6321
  }
6030
6322
  async function installManifestUrl(url) {
6031
- const res = await fetch(url, { redirect: "follow" });
6323
+ const res = await fetch(url, { redirect: "error" });
6032
6324
  if (!res.ok) {
6033
6325
  throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
6034
6326
  }
6035
6327
  const manifest = parseManifest(await res.text());
6036
6328
  let script;
6037
- if (manifest.script.includes(`
6038
- `)) {
6329
+ const inline = manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
6330
+ `);
6331
+ if (inline) {
6039
6332
  script = { path: "script.ts", content: manifest.script };
6040
6333
  } else {
6041
6334
  const base = url.substring(0, url.lastIndexOf("/"));
6042
6335
  const scriptUrl = `${base}/${manifest.script}`;
6043
- const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
6336
+ const scriptRes = await fetch(scriptUrl, { redirect: "error" });
6044
6337
  if (!scriptRes.ok) {
6045
6338
  throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
6046
6339
  }
@@ -6064,8 +6357,10 @@ var init_custom_install = __esm(() => {
6064
6357
  var exports_sync = {};
6065
6358
  __export(exports_sync, {
6066
6359
  syncHooks: () => syncHooks,
6360
+ stageSyncArtifacts: () => stageSyncArtifacts,
6067
6361
  planSync: () => planSync,
6068
- fetchPinnedHook: () => fetchPinnedHook
6362
+ fetchPinnedHook: () => fetchPinnedHook,
6363
+ commitSyncArtifacts: () => commitSyncArtifacts
6069
6364
  });
6070
6365
  import { createHash as createHash2 } from "crypto";
6071
6366
  import { readFileSync as readFileSync8 } from "fs";
@@ -6089,7 +6384,7 @@ function collectBundledCatalog() {
6089
6384
  }
6090
6385
  return out;
6091
6386
  }
6092
- async function fetchJson(base, path) {
6387
+ async function fetchJson(base, path, opts) {
6093
6388
  const headers = { accept: "application/json" };
6094
6389
  const apiKey = resolveApiKey();
6095
6390
  if (apiKey)
@@ -6105,7 +6400,11 @@ async function fetchJson(base, path) {
6105
6400
  if (!res.ok) {
6106
6401
  throw new Error(`GET ${path} failed with status ${res.status}`);
6107
6402
  }
6108
- return res.json();
6403
+ const body = await res.json();
6404
+ if (opts?.withHeader) {
6405
+ body[opts.withHeader] = res.headers.get("x-hook-sha256");
6406
+ }
6407
+ return body;
6109
6408
  }
6110
6409
  async function fetchRemoteState(apiUrl) {
6111
6410
  const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
@@ -6118,6 +6417,16 @@ async function fetchRemoteState(apiUrl) {
6118
6417
  }
6119
6418
  return { catalog: catalog.hooks, remoteLock };
6120
6419
  }
6420
+ function classifyRemotePin(pin, remoteVersion, remoteSha) {
6421
+ const pinnedLocally = pin !== undefined && pin.version === remoteVersion && pin.sha256 === remoteSha;
6422
+ if (pinnedLocally)
6423
+ return "unchanged";
6424
+ if (pin !== undefined && pin.pinned === true && pin.version !== remoteVersion)
6425
+ return "unchanged";
6426
+ if (pin !== undefined)
6427
+ return "updated";
6428
+ return "added";
6429
+ }
6121
6430
  function computeDiff(catalog, remoteLock) {
6122
6431
  const local = readLock();
6123
6432
  const diff = { added: [], updated: [], unchanged: [], skipped: [] };
@@ -6125,30 +6434,30 @@ function computeDiff(catalog, remoteLock) {
6125
6434
  if (!entry.name || !entry.version || !entry.sha256)
6126
6435
  continue;
6127
6436
  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);
6437
+ if (remoteLock) {
6438
+ const remotePin = remoteLock.hooks[entry.name];
6439
+ if (!remotePin) {
6440
+ diff.skipped.push(entry.name);
6441
+ continue;
6442
+ }
6443
+ if (remotePin.version !== entry.version) {
6444
+ 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.`);
6445
+ }
6446
+ diff[classifyRemotePin(pin, remotePin.version, remotePin.sha256)].push(entry.name);
6138
6447
  } else {
6139
- diff.added.push(entry.name);
6448
+ diff[classifyRemotePin(pin, entry.version, entry.sha256)].push(entry.name);
6140
6449
  }
6141
6450
  }
6142
6451
  return diff;
6143
6452
  }
6144
- async function planSync() {
6453
+ async function planSync(options = {}) {
6145
6454
  const apiUrl = resolveApiUrl();
6146
6455
  if (apiUrl) {
6147
6456
  const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
6148
- return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
6457
+ return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
6149
6458
  }
6150
6459
  const diff = computeDiff(collectBundledCatalog(), null);
6151
- return { apiUrl: null, dryRun: false, diff };
6460
+ return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
6152
6461
  }
6153
6462
  async function syncHooks(options = {}) {
6154
6463
  const apiUrl = resolveApiUrl();
@@ -6156,10 +6465,10 @@ async function syncHooks(options = {}) {
6156
6465
  if (options.dryRun)
6157
6466
  return { ...plan, dryRun: true };
6158
6467
  if (!apiUrl) {
6159
- const db2 = getDb();
6468
+ const db = getDb();
6160
6469
  for (const entry of collectBundledCatalog()) {
6161
6470
  setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
6162
- upsertHookRecord(db2, {
6471
+ upsertHookRecord(db, {
6163
6472
  name: entry.name,
6164
6473
  version: entry.version,
6165
6474
  sha256: entry.sha256,
@@ -6172,13 +6481,17 @@ async function syncHooks(options = {}) {
6172
6481
  }
6173
6482
  const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
6174
6483
  const diff = computeDiff(catalog, remoteLock);
6175
- const toFetch = [...diff.added, ...diff.updated];
6176
- const artifacts = new Map;
6177
- for (const name of toFetch) {
6484
+ const staged = await stageSyncArtifacts(apiUrl, [...diff.added, ...diff.updated], remoteLock);
6485
+ await commitSyncArtifacts(staged, apiUrl, remoteLock);
6486
+ return { apiUrl, dryRun: false, diff };
6487
+ }
6488
+ async function stageSyncArtifacts(apiUrl, names, remoteLock) {
6489
+ const staged = [];
6490
+ for (const name of names) {
6178
6491
  const entry = remoteLock.hooks[name];
6179
6492
  if (!entry)
6180
6493
  continue;
6181
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
6494
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
6182
6495
  if (!artifact.manifest || typeof artifact.script !== "string") {
6183
6496
  throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
6184
6497
  }
@@ -6186,34 +6499,64 @@ async function syncHooks(options = {}) {
6186
6499
  if (actualSha !== entry.sha256) {
6187
6500
  throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
6188
6501
  }
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
6502
  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}`);
6503
+ if (manifest.version !== entry.version) {
6504
+ throw new Error(`artifact for '${name}@${entry.version}' declares a different version ('${manifest.version}')`);
6205
6505
  }
6206
- setPinnedHook(name, { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" });
6207
- upsertHookRecord(db, {
6506
+ const scriptRel = scriptRelFor(manifest);
6507
+ staged.push({
6208
6508
  name,
6209
6509
  version: entry.version,
6210
6510
  sha256: entry.sha256,
6211
- source_type: entry.source ?? "remote",
6212
- source_ref: apiUrl,
6213
- last_verified_at: new Date().toISOString()
6511
+ source: entry.source ?? "remote",
6512
+ manifest,
6513
+ scriptContent: artifact.script,
6514
+ scriptRel
6214
6515
  });
6215
6516
  }
6216
- return { apiUrl, dryRun: false, diff };
6517
+ return staged;
6518
+ }
6519
+ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
6520
+ const db = getDb();
6521
+ for (const item of staged) {
6522
+ const entry = remoteLock.hooks[item.name];
6523
+ if (!entry)
6524
+ continue;
6525
+ writeCustomHook(item.name, item.manifest, item.scriptContent, item.scriptRel);
6526
+ const scriptPath = resolveScriptPath(item.name);
6527
+ const actual = await sha256File(scriptPath);
6528
+ if (actual !== entry.sha256) {
6529
+ throw new Error(`sha256 mismatch after write for '${item.name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
6530
+ }
6531
+ }
6532
+ const lock = readLock();
6533
+ for (const item of staged) {
6534
+ const entry = remoteLock.hooks[item.name];
6535
+ if (!entry)
6536
+ continue;
6537
+ lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
6538
+ }
6539
+ writeLock(lock);
6540
+ db.exec("BEGIN");
6541
+ try {
6542
+ for (const item of staged) {
6543
+ const entry = remoteLock.hooks[item.name];
6544
+ if (!entry)
6545
+ continue;
6546
+ upsertHookRecord(db, {
6547
+ name: item.name,
6548
+ version: entry.version,
6549
+ sha256: entry.sha256,
6550
+ source_type: entry.source ?? "remote",
6551
+ source_ref: apiUrl,
6552
+ last_verified_at: new Date().toISOString()
6553
+ });
6554
+ }
6555
+ db.exec("COMMIT");
6556
+ } catch (err) {
6557
+ db.exec("ROLLBACK");
6558
+ throw err;
6559
+ }
6217
6560
  }
6218
6561
  async function fetchPinnedHook(name, version, apiUrl) {
6219
6562
  const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
@@ -6221,10 +6564,11 @@ async function fetchPinnedHook(name, version, apiUrl) {
6221
6564
  if (!entry) {
6222
6565
  throw new Error(`Hook '${name}' is not in the remote registry lock`);
6223
6566
  }
6224
- if (entry.version !== version) {
6225
- throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${entry.version})`);
6567
+ const isLatest = entry.version === version;
6568
+ if (!isLatest && !(entry.versions ?? []).includes(version)) {
6569
+ throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
6226
6570
  }
6227
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}`);
6571
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
6228
6572
  if (!artifact.manifest || typeof artifact.script !== "string") {
6229
6573
  throw new Error(`artifact for '${name}@${version}' is malformed`);
6230
6574
  }
@@ -6236,24 +6580,31 @@ async function fetchPinnedHook(name, version, apiUrl) {
6236
6580
  throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
6237
6581
  }
6238
6582
  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}`);
6583
+ let expectedSha = entry.sha256;
6584
+ if (!isLatest) {
6585
+ const headerSha = artifact._headerSha;
6586
+ if (!headerSha) {
6587
+ throw new Error(`registry did not return a sha256 header for '${name}@${version}' (older pin); refusing to trust an unverified artifact`);
6588
+ }
6589
+ expectedSha = headerSha;
6590
+ }
6591
+ if (actualSha !== expectedSha) {
6592
+ throw new Error(`sha256 mismatch for '${name}@${version}': expected ${expectedSha}, artifact has ${actualSha}`);
6241
6593
  }
6242
6594
  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;
6595
+ const scriptRel = scriptRelFor(manifest);
6245
6596
  writeCustomHook(name, manifest, artifact.script, scriptRel);
6246
6597
  const scriptPath = resolveScriptPath(name);
6247
6598
  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}`);
6599
+ if (actual !== expectedSha) {
6600
+ throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
6250
6601
  }
6251
6602
  const db = getDb();
6252
- setPinnedHook(name, { version, sha256: entry.sha256, source: entry.source ?? "remote" });
6603
+ setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
6253
6604
  upsertHookRecord(db, {
6254
6605
  name,
6255
6606
  version,
6256
- sha256: entry.sha256,
6607
+ sha256: expectedSha,
6257
6608
  source_type: entry.source ?? "remote",
6258
6609
  source_ref: apiUrl,
6259
6610
  last_verified_at: new Date().toISOString()
@@ -6261,7 +6612,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
6261
6612
  return {
6262
6613
  name,
6263
6614
  version,
6264
- sha256: entry.sha256,
6615
+ sha256: expectedSha,
6265
6616
  source: entry.source ?? "remote",
6266
6617
  source_ref: apiUrl,
6267
6618
  artifact,
@@ -6286,7 +6637,7 @@ var init_pg_migrations = __esm(() => {
6286
6637
  timestamp TEXT NOT NULL,
6287
6638
  session_id TEXT NOT NULL,
6288
6639
  hook_name TEXT NOT NULL,
6289
- event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
6640
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
6290
6641
  tool_name TEXT,
6291
6642
  tool_input TEXT,
6292
6643
  result TEXT CHECK (result IN ('continue', 'block', NULL)),
@@ -6319,7 +6670,10 @@ var init_pg_migrations = __esm(() => {
6319
6670
  )`,
6320
6671
  `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
6321
6672
  `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
6322
- CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`
6673
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`,
6674
+ `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
6675
+ `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
6676
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart'))`
6323
6677
  ];
6324
6678
  });
6325
6679
 
@@ -6334,7 +6688,34 @@ function normalizeParams(params) {
6334
6688
  return flat.map((value) => value === undefined ? null : value);
6335
6689
  }
6336
6690
  function sslConfigFor(connectionString) {
6337
- return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
6691
+ const wantsSsl = wantsSslFor(connectionString);
6692
+ if (!wantsSsl)
6693
+ return;
6694
+ const insecure = process.env.HASNA_HOOKS_PG_INSECURE_TLS === "1" || process.env.HOOKS_PG_INSECURE_TLS === "1";
6695
+ if (insecure && false) {}
6696
+ return insecure ? { rejectUnauthorized: false } : { rejectUnauthorized: true };
6697
+ }
6698
+ function wantsSslFor(connectionString) {
6699
+ let raw;
6700
+ try {
6701
+ raw = new URL(connectionString).search.replace(/^\?/, "");
6702
+ } catch {
6703
+ raw = connectionString;
6704
+ }
6705
+ try {
6706
+ raw = decodeURIComponent(raw);
6707
+ } catch {}
6708
+ const paramValue = (name) => {
6709
+ const match = raw.match(new RegExp(`(?:^|[&\\s])${name}\\s*=\\s*([^&\\s]+)`, "i"));
6710
+ return match ? match[1].toLowerCase() : undefined;
6711
+ };
6712
+ const sslmode = paramValue("sslmode");
6713
+ if (sslmode !== undefined)
6714
+ return sslmode !== "disable" && sslmode !== "allow";
6715
+ const ssl = paramValue("ssl");
6716
+ if (ssl !== undefined)
6717
+ return ssl !== "false" && ssl !== "0" && ssl !== "no";
6718
+ return false;
6338
6719
  }
6339
6720
 
6340
6721
  class PgAdapterAsync {
@@ -6496,9 +6877,15 @@ async function pushTable(db, remote, table) {
6496
6877
  result.rowsRead = rows.length;
6497
6878
  if (rows.length === 0)
6498
6879
  return result;
6880
+ const projected = table === "hook_events" ? rows.map((row) => ({
6881
+ ...row,
6882
+ tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
6883
+ error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
6884
+ metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
6885
+ })) : rows;
6499
6886
  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);
6887
+ const columns = filterRemoteColumns(remoteColumns, Object.keys(projected[0]));
6888
+ result.rowsWritten = await upsertPg(remote, table, columns, projected, remoteColumns);
6502
6889
  } catch (error) {
6503
6890
  result.errors.push(error instanceof Error ? error.message : String(error));
6504
6891
  }
@@ -6513,8 +6900,14 @@ async function pullTable(remote, db, table) {
6513
6900
  result.rowsRead = rows.length;
6514
6901
  if (rows.length === 0)
6515
6902
  return result;
6516
- const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
6517
- result.rowsWritten = upsertSqlite(db, table, columns, rows);
6903
+ const projected = table === "hook_events" ? rows.map((row) => ({
6904
+ ...row,
6905
+ tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
6906
+ error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
6907
+ metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
6908
+ })) : rows;
6909
+ const columns = filterLocalColumns(db, table, Object.keys(projected[0]));
6910
+ result.rowsWritten = upsertSqlite(db, table, columns, projected);
6518
6911
  } catch (error) {
6519
6912
  result.errors.push(error instanceof Error ? error.message : String(error));
6520
6913
  }
@@ -6637,6 +7030,7 @@ var init_storage_sync = __esm(() => {
6637
7030
  init_db();
6638
7031
  init_pg_migrations();
6639
7032
  init_remote_storage();
7033
+ init_redact();
6640
7034
  STORAGE_TABLES = [
6641
7035
  "hook_events",
6642
7036
  "schema_migrations",
@@ -6735,24 +7129,29 @@ async function buildCatalog() {
6735
7129
  const scriptPath = resolveHook(meta.name)?.scriptPath;
6736
7130
  if (!scriptPath)
6737
7131
  continue;
7132
+ const sha = await sha256File(scriptPath);
6738
7133
  byName.set(meta.name, {
6739
7134
  name: meta.name,
6740
7135
  version: meta.version,
6741
- sha256: await sha256File(scriptPath),
7136
+ sha256: sha,
6742
7137
  events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
6743
7138
  description: meta.description,
6744
- source: "bundled"
7139
+ source: "bundled",
7140
+ versions: [meta.version]
6745
7141
  });
6746
7142
  }
6747
7143
  for (const custom of listCustomHooks()) {
6748
7144
  const name = shortManifestName(custom.manifest.name);
7145
+ const sha = await sha256File(custom.scriptPath);
7146
+ const existing = byName.get(name);
6749
7147
  byName.set(name, {
6750
7148
  name,
6751
7149
  version: custom.manifest.version,
6752
- sha256: await sha256File(custom.scriptPath),
7150
+ sha256: sha,
6753
7151
  events: custom.manifest.events,
6754
7152
  description: custom.manifest.description ?? "Custom hook",
6755
- source: byName.has(name) ? "custom-overrides-bundled" : "custom"
7153
+ source: existing ? "custom-overrides-bundled" : "custom",
7154
+ versions: existing ? [...new Set([...existing.versions, custom.manifest.version])] : [custom.manifest.version]
6756
7155
  });
6757
7156
  }
6758
7157
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
@@ -6770,6 +7169,7 @@ async function artifactFor(name, version) {
6770
7169
  description: resolved.description,
6771
7170
  events: resolved.events,
6772
7171
  script: custom ? custom.manifest.script : `src/hook.ts`,
7172
+ script_kind: custom?.manifest.script_kind,
6773
7173
  args: custom?.manifest.args,
6774
7174
  timeout_ms: custom?.manifest.timeout_ms
6775
7175
  };
@@ -6799,11 +7199,32 @@ function handleServeRequest(req, apiKey) {
6799
7199
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
6800
7200
  }
6801
7201
  if (url.pathname === "/api/v1/lock" && req.method === "GET") {
6802
- return Promise.resolve(json(readLock()));
7202
+ return buildCatalog().then((catalog) => {
7203
+ const lock = readLock();
7204
+ const byVersion = {};
7205
+ for (const entry of catalog)
7206
+ byVersion[entry.name] = entry.versions;
7207
+ const hooks = {};
7208
+ for (const [name, pin] of Object.entries(lock.hooks)) {
7209
+ hooks[name] = { ...pin, versions: byVersion[name] ?? [pin.version] };
7210
+ }
7211
+ return json({ hooks });
7212
+ });
6803
7213
  }
6804
- const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
7214
+ const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
6805
7215
  if (artifactMatch && req.method === "GET") {
6806
- const [, name, version] = artifactMatch;
7216
+ const [, rawName, rawVersion] = artifactMatch;
7217
+ let name;
7218
+ let version;
7219
+ try {
7220
+ name = decodeURIComponent(rawName);
7221
+ version = decodeURIComponent(rawVersion);
7222
+ } catch {
7223
+ return Promise.resolve(json({ error: "invalid URL encoding" }, 400));
7224
+ }
7225
+ if (!SEMVER_PATTERN.test(version)) {
7226
+ return Promise.resolve(json({ error: `invalid semver version '${version}'` }, 400));
7227
+ }
6807
7228
  return artifactFor(name, version).then(async (artifact) => {
6808
7229
  if (!artifact)
6809
7230
  return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
@@ -6834,7 +7255,7 @@ function handleServeRequest(req, apiKey) {
6834
7255
  function startServeServer(options) {
6835
7256
  const port = options.port ?? DEFAULT_SERVE_PORT;
6836
7257
  const host = options.host ?? SERVE_HOST;
6837
- const apiKey = resolveApiKey(options.apiKey);
7258
+ const apiKey = resolveApiKey();
6838
7259
  const server = Bun.serve({
6839
7260
  hostname: host,
6840
7261
  port,
@@ -6852,6 +7273,7 @@ var init_serve = __esm(() => {
6852
7273
  init_store();
6853
7274
  init_resolve();
6854
7275
  init_config();
7276
+ init_semver();
6855
7277
  if (false) {}
6856
7278
  });
6857
7279
 
@@ -6941,7 +7363,9 @@ var exports_server = {};
6941
7363
  __export(exports_server, {
6942
7364
  startStdioServer: () => startStdioServer,
6943
7365
  startSSEServer: () => startSSEServer,
7366
+ sseAuthToken: () => sseAuthToken,
6944
7367
  createHooksServer: () => createHooksServer,
7368
+ MCP_SSE_HOST: () => MCP_SSE_HOST,
6945
7369
  MCP_PORT: () => MCP_PORT
6946
7370
  });
6947
7371
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -6949,14 +7373,14 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
6949
7373
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6950
7374
  import { z as z2 } from "zod";
6951
7375
  import { createServer } from "http";
6952
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
6953
- import { join as join11, dirname as dirname5 } from "path";
7376
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
7377
+ import { join as join11, dirname as dirname6 } from "path";
6954
7378
  import { fileURLToPath as fileURLToPath3 } from "url";
6955
7379
  async function runVerifiedHook(name, stdin, timeoutMs) {
6956
7380
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
6957
7381
  const resolved = resolveHook2(name);
6958
7382
  const hookScript = resolveScriptPath2(name);
6959
- if (!resolved || !hookScript || !existsSync11(hookScript)) {
7383
+ if (!resolved || !hookScript || !existsSync12(hookScript)) {
6960
7384
  throw new Error(`Hook '${name}' not found`);
6961
7385
  }
6962
7386
  const content = readFileSync9(hookScript);
@@ -6969,7 +7393,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
6969
7393
  const started = Date.now();
6970
7394
  let ran;
6971
7395
  try {
6972
- ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, env: process.env, timeout: timeoutMs });
7396
+ ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, timeout: timeoutMs });
6973
7397
  } catch (err) {
6974
7398
  try {
6975
7399
  const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
@@ -7075,6 +7499,9 @@ function compactEvent(row) {
7075
7499
  tool_input_preview: row.tool_input ? truncateText(row.tool_input, 180) : undefined
7076
7500
  };
7077
7501
  }
7502
+ function projectLogRows(rows) {
7503
+ return rows.map((row) => projectEventRowForRead({ ...row }));
7504
+ }
7078
7505
  function createHooksServer() {
7079
7506
  const server = new McpServer({
7080
7507
  name: "@hasna/hooks",
@@ -7161,7 +7588,7 @@ function createHooksServer() {
7161
7588
  const settingsPath = getSettingsPath(scope);
7162
7589
  const issues = [];
7163
7590
  const healthy = [];
7164
- const settingsExist = existsSync11(settingsPath);
7591
+ const settingsExist = existsSync12(settingsPath);
7165
7592
  if (!settingsExist) {
7166
7593
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
7167
7594
  }
@@ -7171,7 +7598,7 @@ function createHooksServer() {
7171
7598
  const meta = custom ? resolveHookMeta(name) : getHook(name);
7172
7599
  let hookHealthy = true;
7173
7600
  if (custom) {
7174
- if (!existsSync11(custom.scriptPath)) {
7601
+ if (!existsSync12(custom.scriptPath)) {
7175
7602
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
7176
7603
  hookHealthy = false;
7177
7604
  }
@@ -7181,7 +7608,7 @@ function createHooksServer() {
7181
7608
  continue;
7182
7609
  }
7183
7610
  const hookDir = getHookPath(name);
7184
- if (!existsSync11(join11(hookDir, "src", "hook.ts"))) {
7611
+ if (!existsSync12(join11(hookDir, "src", "hook.ts"))) {
7185
7612
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
7186
7613
  hookHealthy = false;
7187
7614
  }
@@ -7220,7 +7647,7 @@ function createHooksServer() {
7220
7647
  const hookPath = getHookPath(name);
7221
7648
  const readmePath = join11(hookPath, "README.md");
7222
7649
  let readme = "";
7223
- if (existsSync11(readmePath)) {
7650
+ if (existsSync12(readmePath)) {
7224
7651
  readme = readFileSync9(readmePath, "utf-8");
7225
7652
  }
7226
7653
  if (verbose) {
@@ -7404,7 +7831,7 @@ function createHooksServer() {
7404
7831
  const ctx = {
7405
7832
  scope,
7406
7833
  settings_path: settingsPath,
7407
- settings_exists: existsSync11(settingsPath),
7834
+ settings_exists: existsSync12(settingsPath),
7408
7835
  registered_hooks: hooks,
7409
7836
  hook_count: hooks.length,
7410
7837
  healthy,
@@ -7456,7 +7883,13 @@ function createHooksServer() {
7456
7883
  } catch (err) {
7457
7884
  const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
7458
7885
  if (err instanceof HookTimeoutError2) {
7459
- return { name, decision: "approve", timedOut: true, timeout_ms: effectiveTimeout };
7886
+ return {
7887
+ name,
7888
+ decision: "block",
7889
+ timedOut: true,
7890
+ timeout_ms: effectiveTimeout,
7891
+ error: `hook timed out after ${effectiveTimeout}ms \u2014 preview blocked (never auto-approved)`
7892
+ };
7460
7893
  }
7461
7894
  return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
7462
7895
  }
@@ -7482,15 +7915,25 @@ function createHooksServer() {
7482
7915
  hooks: z2.array(z2.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
7483
7916
  scope: z2.enum(["global", "project"]).default("global").describe("Install scope")
7484
7917
  }, async ({ agent_type, name, hooks, scope }) => {
7918
+ if (agent_type !== "claude" && agent_type !== "gemini" && agent_type !== "custom") {
7919
+ return {
7920
+ content: [{
7921
+ type: "text",
7922
+ text: JSON.stringify({ error: `unsupported agent_type '${String(agent_type)}' (supported: claude, gemini, custom)` })
7923
+ }],
7924
+ isError: true
7925
+ };
7926
+ }
7485
7927
  const profile = createProfile({ agent_type, name });
7928
+ const target = agent_type === "gemini" ? "gemini" : "claude";
7486
7929
  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 }));
7930
+ const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id, target }));
7488
7931
  const installed = results.filter((r) => r.success).map((r) => r.hook);
7489
7932
  const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
7490
7933
  return {
7491
7934
  content: [{
7492
7935
  type: "text",
7493
- text: JSON.stringify({ profile, installed, failed, scope, run_with: `hooks run <name> --profile ${profile.agent_id}` })
7936
+ text: JSON.stringify({ profile, installed, failed, scope, target, run_with: `hooks run <name> --profile ${profile.agent_id}` })
7494
7937
  }]
7495
7938
  };
7496
7939
  });
@@ -7534,7 +7977,7 @@ function createHooksServer() {
7534
7977
  const settingsPath = getSettingsPath(scope);
7535
7978
  let settings = {};
7536
7979
  try {
7537
- if (existsSync11(settingsPath))
7980
+ if (existsSync12(settingsPath))
7538
7981
  settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
7539
7982
  } catch {}
7540
7983
  if (!settings.hooks)
@@ -7544,8 +7987,8 @@ function createHooksServer() {
7544
7987
  disabled.push(name);
7545
7988
  settings.hooks.__disabled = disabled;
7546
7989
  const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
7547
- const { dirname: dirname6 } = await import("path");
7548
- mkdirSync8(dirname6(settingsPath), { recursive: true });
7990
+ const { dirname: dirname7 } = await import("path");
7991
+ mkdirSync8(dirname7(settingsPath), { recursive: true });
7549
7992
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
7550
7993
  `);
7551
7994
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -7557,7 +8000,7 @@ function createHooksServer() {
7557
8000
  const settingsPath = getSettingsPath(scope);
7558
8001
  let settings = {};
7559
8002
  try {
7560
- if (existsSync11(settingsPath))
8003
+ if (existsSync12(settingsPath))
7561
8004
  settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
7562
8005
  } catch {}
7563
8006
  if (settings.hooks?.__disabled) {
@@ -7642,12 +8085,13 @@ function createHooksServer() {
7642
8085
  sql += " ORDER BY timestamp DESC LIMIT ?";
7643
8086
  params.push(maxRows);
7644
8087
  const rows = db.query(sql).all(...params);
8088
+ const projected = projectLogRows(rows);
7645
8089
  return {
7646
8090
  content: [{
7647
8091
  type: "text",
7648
8092
  text: JSON.stringify({
7649
- events: compact ? rows.map(compactEvent) : rows,
7650
- count: rows.length,
8093
+ events: compact ? projected.map(compactEvent) : projected,
8094
+ count: projected.length,
7651
8095
  compact,
7652
8096
  hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
7653
8097
  })
@@ -7662,12 +8106,13 @@ function createHooksServer() {
7662
8106
  const db = getDb2();
7663
8107
  const maxRows = boundedLimit(n, 20, compact ? 100 : 500);
7664
8108
  const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(maxRows);
8109
+ const projected = projectLogRows(rows);
7665
8110
  return {
7666
8111
  content: [{
7667
8112
  type: "text",
7668
8113
  text: JSON.stringify({
7669
- events: compact ? rows.map(compactEvent) : rows,
7670
- count: rows.length,
8114
+ events: compact ? projected.map(compactEvent) : projected,
8115
+ count: projected.length,
7671
8116
  compact,
7672
8117
  hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
7673
8118
  })
@@ -7692,12 +8137,13 @@ function createHooksServer() {
7692
8137
  }
7693
8138
  const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
7694
8139
  const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, maxRows);
8140
+ const projected = projectLogRows(rows);
7695
8141
  return {
7696
8142
  content: [{
7697
8143
  type: "text",
7698
8144
  text: JSON.stringify({
7699
- events: compact ? rows.map(compactEvent) : rows,
7700
- count: rows.length,
8145
+ events: compact ? projected.map(compactEvent) : projected,
8146
+ count: projected.length,
7701
8147
  compact,
7702
8148
  hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
7703
8149
  })
@@ -7825,11 +8271,48 @@ function createHooksServer() {
7825
8271
  });
7826
8272
  return server;
7827
8273
  }
7828
- async function startSSEServer(port = MCP_PORT) {
8274
+ function isLoopbackHost(host) {
8275
+ return host === "" || host === "127.0.0.1" || host === "::1" || host === "localhost";
8276
+ }
8277
+ function sseAuthToken(explicit) {
8278
+ if (explicit && explicit.length > 0)
8279
+ return explicit;
8280
+ for (const name of MCP_SSE_AUTH_ENV) {
8281
+ const value = process.env[name];
8282
+ if (value && value.length > 0)
8283
+ return value;
8284
+ }
8285
+ return;
8286
+ }
8287
+ async function startSSEServer(options = {}) {
8288
+ const port = options.port ?? MCP_PORT;
8289
+ const host = options.host ?? MCP_SSE_HOST;
8290
+ const authToken = sseAuthToken(options.authToken);
8291
+ const loopback = isLoopbackHost(host);
8292
+ if (!loopback && !authToken) {
8293
+ 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.`);
8294
+ }
7829
8295
  const server = createHooksServer();
7830
8296
  const transports = new Map;
8297
+ function authorized2(headers) {
8298
+ if (!authToken)
8299
+ return false;
8300
+ const rawAuth = headers["authorization"];
8301
+ const header = (Array.isArray(rawAuth) ? rawAuth[0] : rawAuth) ?? "";
8302
+ if (header.startsWith("Bearer "))
8303
+ return secureEqual(header.slice("Bearer ".length), authToken);
8304
+ const rawKey = headers["x-api-key"];
8305
+ return secureEqual((Array.isArray(rawKey) ? rawKey[0] : rawKey) ?? "", authToken);
8306
+ }
7831
8307
  const httpServer = createServer(async (req, res) => {
7832
8308
  const url = new URL(req.url || "/", `http://localhost:${port}`);
8309
+ if (url.pathname === "/sse" || url.pathname === "/messages") {
8310
+ if (authToken && !authorized2(req.headers)) {
8311
+ res.writeHead(401);
8312
+ res.end("unauthorized: valid MCP token required");
8313
+ return;
8314
+ }
8315
+ }
7833
8316
  if (url.pathname === "/sse") {
7834
8317
  const transport = new SSEServerTransport("/messages", res);
7835
8318
  transports.set(transport.sessionId, transport);
@@ -7852,9 +8335,9 @@ async function startSSEServer(port = MCP_PORT) {
7852
8335
  res.end(JSON.stringify({ name: "@hasna/hooks", version: pkg.version, transport: "sse", port }));
7853
8336
  }
7854
8337
  });
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`);
8338
+ httpServer.listen(port, host, () => {
8339
+ console.error(`@hasna/hooks MCP server running on http://${host}:${port}`);
8340
+ console.error(`SSE endpoint: http://${host}:${port}/sse`);
7858
8341
  });
7859
8342
  }
7860
8343
  async function startStdioServer() {
@@ -7868,7 +8351,7 @@ async function startStdioServer() {
7868
8351
  process.exit(1);
7869
8352
  }
7870
8353
  }
7871
- var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents;
8354
+ var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents, MCP_SSE_HOST = "127.0.0.1", MCP_SSE_AUTH_ENV;
7872
8355
  var init_server = __esm(() => {
7873
8356
  init_registry();
7874
8357
  init_installer();
@@ -7876,19 +8359,21 @@ var init_server = __esm(() => {
7876
8359
  init_manifest();
7877
8360
  init_resolve();
7878
8361
  init_store();
8362
+ init_redact();
7879
8363
  init_storage();
7880
- __dirname3 = dirname5(fileURLToPath3(import.meta.url));
8364
+ __dirname3 = dirname6(fileURLToPath3(import.meta.url));
7881
8365
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
7882
8366
  try {
7883
8367
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
7884
8368
  const p = join11(__dirname3, rel);
7885
- if (existsSync11(p)) {
8369
+ if (existsSync12(p)) {
7886
8370
  pkg = JSON.parse(readFileSync9(p, "utf-8"));
7887
8371
  break;
7888
8372
  }
7889
8373
  }
7890
8374
  } catch {}
7891
8375
  _hooksAgents = new Map;
8376
+ MCP_SSE_AUTH_ENV = ["HASNA_HOOKS_MCP_TOKEN", "HOOKS_MCP_TOKEN"];
7892
8377
  });
7893
8378
 
7894
8379
  // src/mcp/http.ts
@@ -7950,7 +8435,7 @@ function startMcpHttpServer(options) {
7950
8435
  var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
7951
8436
  var init_http = () => {};
7952
8437
 
7953
- // ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
8438
+ // ../events/dist/commander.js
7954
8439
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
7955
8440
  import { Buffer as Buffer2 } from "buffer";
7956
8441
  import { existsSync } from "fs";
@@ -9153,8 +9638,8 @@ var {
9153
9638
 
9154
9639
  // src/cli/index.tsx
9155
9640
  import chalk2 from "chalk";
9156
- import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync5 } from "fs";
9157
- import { join as join12, dirname as dirname6 } from "path";
9641
+ import { existsSync as existsSync13, readFileSync as readFileSync10, rmSync as rmSync5 } from "fs";
9642
+ import { join as join12, dirname as dirname7 } from "path";
9158
9643
  import { fileURLToPath as fileURLToPath4 } from "url";
9159
9644
 
9160
9645
  // src/cli/components/App.tsx
@@ -10329,13 +10814,15 @@ function App({ initialHooks, overwrite = false }) {
10329
10814
  // src/cli/index.tsx
10330
10815
  init_registry();
10331
10816
  init_installer();
10817
+ init_redact();
10332
10818
  init_profiles();
10333
10819
  init_manifest();
10334
10820
  init_resolve();
10335
10821
  init_store();
10822
+ init_semver();
10336
10823
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
10337
- var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
10338
- var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
10824
+ var __dirname4 = dirname7(fileURLToPath4(import.meta.url));
10825
+ var pkgPath = existsSync13(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
10339
10826
  var pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
10340
10827
  var program2 = new Command;
10341
10828
  function resolveScope(options) {
@@ -10364,6 +10851,9 @@ function truncateText2(value, max = 96) {
10364
10851
  return text;
10365
10852
  return `${text.slice(0, Math.max(0, max - 3))}...`;
10366
10853
  }
10854
+ function projectLogRows2(rows) {
10855
+ return rows.map((row) => projectEventRowForRead({ ...row }));
10856
+ }
10367
10857
  function readToken(tokenFile) {
10368
10858
  if (tokenFile) {
10369
10859
  try {
@@ -10500,7 +10990,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
10500
10990
  process.exit(1);
10501
10991
  }
10502
10992
  const hookScript = resolveScriptPath2(hook);
10503
- if (!hookScript || !existsSync12(hookScript)) {
10993
+ if (!hookScript || !existsSync13(hookScript)) {
10504
10994
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
10505
10995
  process.exit(1);
10506
10996
  }
@@ -10625,6 +11115,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
10625
11115
  console.log(chalk2.red(`Unknown category: ${options.category}`));
10626
11116
  console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
10627
11117
  }
11118
+ process.exitCode = 1;
10628
11119
  return;
10629
11120
  }
10630
11121
  toInstall = getHooksByCategory(category).map((h) => h.name);
@@ -10657,7 +11148,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
10657
11148
  const version = arg.slice(at + 1);
10658
11149
  if (!HOOK_NAME_RE2.test(name))
10659
11150
  return null;
10660
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
11151
+ if (!SEMVER_PATTERN.test(version))
10661
11152
  return null;
10662
11153
  return { name, version };
10663
11154
  }
@@ -10791,7 +11282,7 @@ Dry run \u2014 would install (${scope}, ${target}):
10791
11282
  target,
10792
11283
  applied: results.some((r) => r.applied)
10793
11284
  }));
10794
- if (results.length > 0 && results.every((r) => !r.success)) {
11285
+ if (results.some((r) => !r.success)) {
10795
11286
  process.exitCode = 1;
10796
11287
  }
10797
11288
  return;
@@ -10829,6 +11320,11 @@ Installing hooks (${scope}, ${target})...
10829
11320
  process.exitCode = 1;
10830
11321
  return;
10831
11322
  }
11323
+ if (successCount < results.length) {
11324
+ console.log(chalk2.red(`
11325
+ \u2717 ${results.length - successCount} of ${results.length} hook(s) failed to install.`));
11326
+ process.exitCode = 1;
11327
+ }
10832
11328
  console.log(chalk2.dim(`
10833
11329
  Registered in ${settingsFile}`));
10834
11330
  });
@@ -11024,6 +11520,7 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
11024
11520
  } else {
11025
11521
  console.log(chalk2.red(`Hook '${hook}' not found${hint}`));
11026
11522
  }
11523
+ process.exitCode = 1;
11027
11524
  return;
11028
11525
  }
11029
11526
  const globalInstalled = getRegisteredHooks("global").includes(meta.name);
@@ -11065,17 +11562,24 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11065
11562
  const settingsPath = getSettingsPath(scope);
11066
11563
  const issues = [];
11067
11564
  const healthy = [];
11068
- const settingsExist = existsSync12(settingsPath);
11565
+ const settingsExist = existsSync13(settingsPath);
11069
11566
  if (!settingsExist) {
11070
11567
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
11071
11568
  }
11072
11569
  const registered = getRegisteredHooks(scope);
11570
+ let wiringCount = 0;
11571
+ if (settingsExist) {
11572
+ try {
11573
+ const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
11574
+ wiringCount = countSettingsWiring(settings);
11575
+ } catch {}
11576
+ }
11073
11577
  for (const name of registered) {
11074
11578
  const custom = readCustomManifest(name);
11075
11579
  const meta = custom ? resolveHookMeta(name) : getHook(name);
11076
11580
  let hookHealthy = true;
11077
11581
  if (custom) {
11078
- if (!existsSync12(custom.scriptPath)) {
11582
+ if (!existsSync13(custom.scriptPath)) {
11079
11583
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
11080
11584
  hookHealthy = false;
11081
11585
  }
@@ -11087,7 +11591,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11087
11591
  }
11088
11592
  const hookDir = getHookPath(name);
11089
11593
  const hookScript = join12(hookDir, "src", "hook.ts");
11090
- if (!existsSync12(hookScript)) {
11594
+ if (!existsSync13(hookScript)) {
11091
11595
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
11092
11596
  hookHealthy = false;
11093
11597
  }
@@ -11107,7 +11611,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11107
11611
  }
11108
11612
  }
11109
11613
  if (options.json) {
11110
- console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }));
11614
+ console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, wiring_count: wiringCount, scope }));
11111
11615
  return;
11112
11616
  }
11113
11617
  console.log(chalk2.bold(`
@@ -11116,6 +11620,9 @@ Hook Health Check (${scope})
11116
11620
  if (registered.length === 0) {
11117
11621
  console.log(chalk2.dim(" No hooks registered."));
11118
11622
  console.log(chalk2.dim(" Run: hooks install gitguard"));
11623
+ 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)`));
11624
+ if (issues.length > 0)
11625
+ process.exitCode = 1;
11119
11626
  return;
11120
11627
  }
11121
11628
  if (healthy.length > 0) {
@@ -11124,6 +11631,9 @@ Hook Health Check (${scope})
11124
11631
  console.log(chalk2.green(` ${name}`));
11125
11632
  }
11126
11633
  }
11634
+ if (registered.length > 0) {
11635
+ 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)`));
11636
+ }
11127
11637
  if (issues.length > 0) {
11128
11638
  console.log();
11129
11639
  for (const issue of issues) {
@@ -11131,9 +11641,11 @@ Hook Health Check (${scope})
11131
11641
  console.log(` ${icon} ${chalk2.cyan(issue.hook)}: ${issue.issue}`);
11132
11642
  }
11133
11643
  }
11134
- if (issues.length === 0) {
11644
+ if (issues.length === 0 && registered.length > 0) {
11135
11645
  console.log(chalk2.green(`
11136
- All hooks healthy!`));
11646
+ All ${healthy.length} registered hook(s) healthy`));
11647
+ } else if (issues.some((i) => i.severity === "error")) {
11648
+ process.exitCode = 1;
11137
11649
  }
11138
11650
  console.log();
11139
11651
  });
@@ -11166,7 +11678,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
11166
11678
  const version = arg.slice(at + 1);
11167
11679
  if (!HOOK_NAME_RE2.test(name))
11168
11680
  return null;
11169
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
11681
+ if (!SEMVER_PATTERN.test(version))
11170
11682
  return null;
11171
11683
  return { name, version };
11172
11684
  }
@@ -11249,12 +11761,13 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
11249
11761
  } else {
11250
11762
  console.log(chalk2.red(`Hook '${hook}' not found`));
11251
11763
  }
11764
+ process.exitCode = 1;
11252
11765
  return;
11253
11766
  }
11254
11767
  const hookPath = getHookPath(hook);
11255
11768
  const readmePath = join12(hookPath, "README.md");
11256
11769
  let readme = "";
11257
- if (existsSync12(readmePath)) {
11770
+ if (existsSync13(readmePath)) {
11258
11771
  readme = readFileSync10(readmePath, "utf-8");
11259
11772
  }
11260
11773
  if (options.json) {
@@ -11503,18 +12016,19 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
11503
12016
  sql += " ORDER BY timestamp DESC LIMIT ?";
11504
12017
  params.push(String(limit));
11505
12018
  const rows = db.query(sql).all(...params);
12019
+ const projected = projectLogRows2(rows);
11506
12020
  if (options.json) {
11507
- console.log(JSON.stringify(rows, null, 2));
12021
+ console.log(JSON.stringify(projected, null, 2));
11508
12022
  return;
11509
12023
  }
11510
- if (rows.length === 0) {
12024
+ if (projected.length === 0) {
11511
12025
  console.log(chalk2.dim("No events found."));
11512
12026
  return;
11513
12027
  }
11514
12028
  console.log(chalk2.bold(`
11515
- Hook Events (${rows.length})
12029
+ Hook Events (${projected.length})
11516
12030
  `));
11517
- for (const row of rows) {
12031
+ for (const row of projected) {
11518
12032
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11519
12033
  const err = row.error ? chalk2.red(` ERR: ${truncateText2(row.error, 60)}`) : "";
11520
12034
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
@@ -11529,18 +12043,19 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
11529
12043
  const limit = parseInt(options.limit) || 50;
11530
12044
  const q = `%${text}%`;
11531
12045
  const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
12046
+ const projected = projectLogRows2(rows);
11532
12047
  if (options.json) {
11533
- console.log(JSON.stringify(rows, null, 2));
12048
+ console.log(JSON.stringify(projected, null, 2));
11534
12049
  return;
11535
12050
  }
11536
- if (rows.length === 0) {
12051
+ if (projected.length === 0) {
11537
12052
  console.log(chalk2.dim(`No events matching "${text}".`));
11538
12053
  return;
11539
12054
  }
11540
12055
  console.log(chalk2.bold(`
11541
- Search results for "${text}" (${rows.length})
12056
+ Search results for "${text}" (${projected.length})
11542
12057
  `));
11543
- for (const row of rows) {
12058
+ for (const row of projected) {
11544
12059
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11545
12060
  const snippet = truncateText2(row.tool_input || row.error || "", 80);
11546
12061
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
@@ -11553,18 +12068,19 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
11553
12068
  const db = getDb2();
11554
12069
  const limit = parseInt(options.n) || 20;
11555
12070
  const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
12071
+ const projected = projectLogRows2(rows);
11556
12072
  if (options.json) {
11557
- console.log(JSON.stringify(rows, null, 2));
12073
+ console.log(JSON.stringify(projected, null, 2));
11558
12074
  return;
11559
12075
  }
11560
- if (rows.length === 0) {
12076
+ if (projected.length === 0) {
11561
12077
  console.log(chalk2.dim("No events yet."));
11562
12078
  return;
11563
12079
  }
11564
12080
  console.log(chalk2.bold(`
11565
- Last ${rows.length} events
12081
+ Last ${projected.length} events
11566
12082
  `));
11567
- for (const row of rows) {
12083
+ for (const row of projected) {
11568
12084
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11569
12085
  const err = row.error ? chalk2.red(` \u2717 ${truncateText2(row.error, 60)}`) : "";
11570
12086
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
@@ -11597,18 +12113,19 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
11597
12113
  }
11598
12114
  const since = new Date(Date.now() - parseDuration(options.since)).toISOString();
11599
12115
  const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(since, limit);
12116
+ const projected = projectLogRows2(rows);
11600
12117
  if (options.json) {
11601
- console.log(JSON.stringify(rows, null, 2));
12118
+ console.log(JSON.stringify(projected, null, 2));
11602
12119
  return;
11603
12120
  }
11604
- if (rows.length === 0) {
12121
+ if (projected.length === 0) {
11605
12122
  console.log(chalk2.dim(`No errors in the last ${options.since}.`));
11606
12123
  return;
11607
12124
  }
11608
12125
  console.log(chalk2.bold(`
11609
- Errors (last ${options.since}, ${rows.length} found)
12126
+ Errors (last ${options.since}, ${projected.length} found)
11610
12127
  `));
11611
- for (const row of rows) {
12128
+ for (const row of projected) {
11612
12129
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
11613
12130
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(truncateText2(row.error, 100))}`);
11614
12131
  }
@@ -11730,18 +12247,17 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
11730
12247
  console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
11731
12248
  console.log(chalk2.dim(` sha256 ${result.actual}`));
11732
12249
  });
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) => {
12250
+ 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
12251
  const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
11735
12252
  startServeServer2({
11736
12253
  port: options.port ? parseInt(options.port, 10) : undefined,
11737
- host: options.host,
11738
- apiKey: options.apiKey
12254
+ host: options.host
11739
12255
  });
11740
12256
  });
11741
12257
  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
12258
  const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
11743
12259
  try {
11744
- const plan = options.dryRun ? await planSync2() : await syncHooks2();
12260
+ const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
11745
12261
  const diff = plan.diff;
11746
12262
  if (options.json) {
11747
12263
  console.log(JSON.stringify({ dry_run: plan.dryRun, api_url: plan.apiUrl ?? null, diff }));
@@ -11837,7 +12353,7 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
11837
12353
  await startStdioServer2();
11838
12354
  } else if (options.sse) {
11839
12355
  const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
11840
- await startSSEServer2(options.port ? parseInt(options.port) : 39427);
12356
+ await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
11841
12357
  } else {
11842
12358
  const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
11843
12359
  const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));