@hasna/hooks 0.10.6 → 0.10.7

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/README.md CHANGED
@@ -92,6 +92,49 @@ Repair unsafe installation permissions with a verified package installer, then
92
92
  check again. This check alone does not prove hosted authentication, native hook
93
93
  trust or a running agent's interception.
94
94
 
95
+ ## Planned safety registration
96
+
97
+ For repeatable global setup, use the owning installer through its plan/apply
98
+ commands. Planning leaves native settings unchanged and returns a compact digest.
99
+
100
+ ```bash
101
+ hooks safety registration-plan --target codex
102
+ hooks safety registration-apply --target codex --expected-plan-digest PLAN_SHA256
103
+ # The same two commands support --target claude.
104
+ ```
105
+
106
+ Apply checks the exact predecessor, retains an owner-only original and intent,
107
+ and reads back the result. An identical registration returns unchanged without
108
+ rewriting its settings. The planner honors `CODEX_HOME` and `CLAUDE_CONFIG_DIR`;
109
+ conflicting Hooks path overrides refuse. Missing native settings directories are
110
+ created only on apply. Disabled or asynchronous guards, unsafe paths, duplicate
111
+ JSON keys and numbers that cannot be preserved exactly refuse before a write.
112
+ Managed default Claude settings continue through Skills' coordinated writer.
113
+
114
+ The SDK exports `planNativeSafetyRegistration` and
115
+ `applyNativeSafetyRegistration`. Both return `nativeAdoptionVerified: false`.
116
+ A changed or uncertain apply requires a fresh read-only plan and inspection of
117
+ its recorded operation; no automatic retry or direct overwrite is performed.
118
+
119
+ After registration, Codex trust uses its supported native configuration writer:
120
+
121
+ ```bash
122
+ hooks safety trust-plan --target codex --cwd /path/to/project --codex-command /absolute/path/to/native/codex
123
+ hooks safety trust-apply --target codex --cwd /path/to/project --codex-command /absolute/path/to/native/codex --expected-plan-digest PLAN_SHA256
124
+ ```
125
+
126
+ The exact definition is verified before trusting and enabling only that guard.
127
+ Unrelated configuration is preserved, and an already trusted, enabled guard
128
+ does not cause a write. A disabled native hooks feature is a refusal, not an
129
+ implicit feature change. Neither registration nor trust proves that an existing
130
+ agent has adopted the change.
131
+
132
+ For Claude, `hooks safety verify --target claude --cwd /path/to/project` checks
133
+ the selected settings and bundled supervisor. Its result explicitly leaves
134
+ runtime-policy verification, native discovery and native adoption false;
135
+ MDM/server policy, workspace trust and launch flags require native acceptance.
136
+
137
+
95
138
  ## Codex safety readiness
96
139
 
97
140
  ```bash
package/bin/hooks-mcp.js CHANGED
@@ -3104,7 +3104,7 @@ function checkScriptHash(name, actual) {
3104
3104
  // src/lib/codex-settings.ts
3105
3105
  import { closeSync as closeSync2, existsSync as existsSync8, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
3106
3106
  import { dirname as dirname4 } from "path";
3107
- import { randomUUID } from "crypto";
3107
+ import { createHash as createHash3, randomUUID } from "crypto";
3108
3108
  var snapshots = new WeakMap;
3109
3109
  function current(path) {
3110
3110
  try {
@@ -3139,11 +3139,24 @@ function readCodexSettings(path) {
3139
3139
  snapshots.set(settings, { path, raw });
3140
3140
  return settings;
3141
3141
  }
3142
- function writeCodexSettings(path, settings) {
3142
+ function assertSettingsPreimage(raw, expected) {
3143
+ if (expected === undefined)
3144
+ return;
3145
+ if (!/^(?:absent|[a-f0-9]{64})$/.test(expected) || (raw === null ? "absent" : createHash3("sha256").update(raw).digest("hex")) !== expected) {
3146
+ throw new Error("Hook settings changed since planning; no settings were written.");
3147
+ }
3148
+ }
3149
+ function writeCodexSettings(path, settings, expectedSha256) {
3143
3150
  const snapshot = snapshots.get(settings);
3144
3151
  if (!snapshot || snapshot.path !== path)
3145
3152
  throw new Error("Codex settings require a complete current read before writing.");
3146
- mkdirSync4(dirname4(path), { recursive: true });
3153
+ assertSettingsPreimage(snapshot.raw, expectedSha256);
3154
+ writeHookSettingsPreimage(path, settings, snapshot.raw);
3155
+ snapshots.set(settings, { path, raw: JSON.stringify(settings, null, 2) + `
3156
+ ` });
3157
+ }
3158
+ function writeHookSettingsPreimage(path, settings, before) {
3159
+ mkdirSync4(dirname4(path), { recursive: true, mode: 448 });
3147
3160
  const lock = `${path}.hooks-write.lock`;
3148
3161
  let fd;
3149
3162
  try {
@@ -3153,15 +3166,14 @@ function writeCodexSettings(path, settings) {
3153
3166
  }
3154
3167
  const temp = `${path}.${randomUUID()}.tmp`;
3155
3168
  try {
3156
- if (current(path) !== snapshot.raw)
3157
- throw new Error("Codex settings changed since reading; re-read before retrying.");
3169
+ if (current(path) !== before)
3170
+ throw new Error("Hook settings changed since reading; re-read before retrying.");
3158
3171
  const next = JSON.stringify(settings, null, 2) + `
3159
3172
  `;
3160
3173
  writeFileSync3(temp, next, { flag: "wx", mode: 384 });
3161
- if (current(path) !== snapshot.raw)
3162
- throw new Error("Codex settings changed since reading; re-read before retrying.");
3174
+ if (current(path) !== before)
3175
+ throw new Error("Hook settings changed since reading; re-read before retrying.");
3163
3176
  renameSync2(temp, path);
3164
- snapshots.set(settings, { path, raw: next });
3165
3177
  } finally {
3166
3178
  if (existsSync8(temp))
3167
3179
  unlinkSync(temp);
@@ -3179,7 +3191,7 @@ import { homedir as homedir4 } from "os";
3179
3191
  import { join as join8, resolve as resolve3 } from "path";
3180
3192
  import { homedir as pathsResolverHomedir2 } from "os";
3181
3193
  import { join as pathsResolverJoin2 } from "path";
3182
- import { createHash as createHash3 } from "crypto";
3194
+ import { createHash as createHash4 } from "crypto";
3183
3195
  import { existsSync as existsSync52, readdirSync as readdirSync3, readFileSync as readFileSync42, statSync as statSync3 } from "fs";
3184
3196
  import { join as join52, sep as sep2 } from "path";
3185
3197
  import { basename as basename2, isAbsolute as isAbsolute22, resolve as resolve22 } from "path";
@@ -3194,7 +3206,7 @@ import { dirname as dirname42, isAbsolute as isAbsolute4, join as join82, resolv
3194
3206
  import { createHash as createHash32 } from "crypto";
3195
3207
  import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, lstatSync as lstatSync5, openSync as openSync3, readSync } from "fs";
3196
3208
  import { dirname as dirname5, isAbsolute as isAbsolute5, resolve as resolve5 } from "path";
3197
- import { createHash as createHash4 } from "crypto";
3209
+ import { createHash as createHash42 } from "crypto";
3198
3210
  import { lstatSync as lstatSync6, readlinkSync } from "fs";
3199
3211
  import { dirname as dirname6, isAbsolute as isAbsolute6, join as join9, parse, resolve as resolve6, sep as sep22 } from "path";
3200
3212
  import { closeSync as closeSync12, constants as constants9, fstatSync as fstatSync12, lstatSync as lstatSync17, openSync as openSync12, opendirSync as opendirSync5, readSync as readSync8 } from "fs";
@@ -10322,7 +10334,7 @@ function* bundleHashParts(files) {
10322
10334
  yield new TextEncoder().encode("\x00");
10323
10335
  }
10324
10336
  function hashBundleFiles(files) {
10325
- const hash = createHash3(CONTENT_HASH_ALGORITHM);
10337
+ const hash = createHash4(CONTENT_HASH_ALGORITHM);
10326
10338
  for (const part of bundleHashParts(files))
10327
10339
  hash.update(part);
10328
10340
  return hash.digest("hex");
@@ -11158,7 +11170,7 @@ function hashDiscoveryPathFile(path, budget, changes) {
11158
11170
  const after = trace(path);
11159
11171
  if (JSON.stringify(before.rows) !== JSON.stringify(after.rows) || before.file !== after.file)
11160
11172
  throw new Error("Discovery path input changed during read");
11161
- return createHash4("sha256").update(JSON.stringify({ version: 1, path, rows: before.rows, content })).digest("hex");
11173
+ return createHash42("sha256").update(JSON.stringify({ version: 1, path, rows: before.rows, content })).digest("hex");
11162
11174
  }
11163
11175
  var package_default = {
11164
11176
  name: "@hasna/skills",
@@ -34389,9 +34401,9 @@ function getTargetSettingsDir(target) {
34389
34401
  }
34390
34402
  function getGlobalSettingsPathOverride(target) {
34391
34403
  if (target === "codex")
34392
- return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
34404
+ return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH ?? (process.env.CODEX_HOME ? resolve7(process.env.CODEX_HOME, "hooks.json") : undefined);
34393
34405
  if (target === "claude")
34394
- return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
34406
+ return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH ?? (process.env.CLAUDE_CONFIG_DIR ? resolve7(process.env.CLAUDE_CONFIG_DIR, "settings.json") : undefined);
34395
34407
  if (target === "gemini")
34396
34408
  return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
34397
34409
  return;
@@ -34435,7 +34447,7 @@ function readSettings2(scope = "global", target = "claude") {
34435
34447
  return value;
34436
34448
  }
34437
34449
  } catch (error61) {
34438
- console.warn(`[hooks] Failed to read settings at ${path}: ${error61 instanceof Error ? error61.message : String(error61)}`);
34450
+ console.warn("[hooks] Cannot read hook settings; repair the selected file before installation.");
34439
34451
  }
34440
34452
  const empty = {};
34441
34453
  if (!existsSync10(path))
@@ -34447,10 +34459,16 @@ function settingsIdentity(path) {
34447
34459
  return realpathSync5(path);
34448
34460
  return existsSync10(dirname7(path)) ? join10(realpathSync5(dirname7(path)), basename3(path)) : resolve7(path);
34449
34461
  }
34450
- function writeSettings(settings, scope = "global", target = "claude") {
34462
+ function writeSettings(settings, scope = "global", target = "claude", expectedSettingsSha256) {
34451
34463
  const path = getSettingsPath(scope, target);
34452
34464
  if (target === "codex")
34453
- return writeCodexSettings(path, settings);
34465
+ return writeCodexSettings(path, settings, expectedSettingsSha256);
34466
+ const captured = settingsPreimages.get(settings);
34467
+ if (expectedSettingsSha256 !== undefined) {
34468
+ if (!captured || captured.path !== path)
34469
+ throw new Error("Hook settings require a valid captured preimage.");
34470
+ assertSettingsPreimage(captured.text, expectedSettingsSha256);
34471
+ }
34454
34472
  if (target === "claude" && settingsIdentity(path) === settingsIdentity(join10(homedir5(), ".claude/settings.json"))) {
34455
34473
  const before = settingsPreimages.get(settings);
34456
34474
  if (!before || before.path !== path)
@@ -34462,6 +34480,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
34462
34480
  `, { home: homedir5() }))
34463
34481
  return;
34464
34482
  }
34483
+ if (expectedSettingsSha256 !== undefined)
34484
+ return writeHookSettingsPreimage(path, settings, captured.text);
34465
34485
  const dir = dirname7(path);
34466
34486
  if (!existsSync10(dir)) {
34467
34487
  mkdirSync6(dir, { recursive: true });
@@ -34619,7 +34639,7 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
34619
34639
  }
34620
34640
  return;
34621
34641
  }
34622
- function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
34642
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256) {
34623
34643
  const shortName = shortHookName(name);
34624
34644
  if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
34625
34645
  return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
@@ -34686,7 +34706,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
34686
34706
  if (rewriteConflict) {
34687
34707
  return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
34688
34708
  }
34689
- registerHook(shortName, scope, target, profile, mementos);
34709
+ registerHook(shortName, scope, target, profile, mementos, (settings, selectedScope, selectedTarget) => writeSettings(settings, selectedScope, selectedTarget, expectedSettingsSha256));
34690
34710
  return {
34691
34711
  hook: shortName,
34692
34712
  success: true,
@@ -34705,7 +34725,10 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
34705
34725
  }
34706
34726
  }
34707
34727
  function installHook(name, options = {}) {
34708
- const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
34728
+ const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256 } = options;
34729
+ if (expectedSettingsSha256 !== undefined && (!/^(?:absent|[a-f0-9]{64})$/.test(expectedSettingsSha256) || !isNativeSafetyName(shortHookName(name)) || target !== "codex" && target !== "claude")) {
34730
+ return { hook: name, success: false, target, error: "An exact settings predecessor requires a Claude or Codex native safety guard." };
34731
+ }
34709
34732
  try {
34710
34733
  validateMementosRegistration(name, mementos);
34711
34734
  } catch (error61) {
@@ -34746,9 +34769,9 @@ function installHook(name, options = {}) {
34746
34769
  }
34747
34770
  return { ...claudeResult, target: "all" };
34748
34771
  }
34749
- return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
34772
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos, expectedSettingsSha256);
34750
34773
  }
34751
- function registerHook(name, scope = "global", target = "claude", profile, mementos) {
34774
+ function registerHook(name, scope = "global", target = "claude", profile, mementos, write = writeSettings) {
34752
34775
  const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
34753
34776
  if (!meta3)
34754
34777
  return;
@@ -34777,7 +34800,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
34777
34800
  }
34778
34801
  const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
34779
34802
  const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
34780
- if (target === "codex") {
34803
+ if (target === "codex" || target === "claude" && isNativeSafetyName(name)) {
34781
34804
  const positions = [];
34782
34805
  for (const [event, groups] of Object.entries(settings.hooks)) {
34783
34806
  for (const entry of groups) {
@@ -34803,7 +34826,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
34803
34826
  if (typeof meta3.timeoutSeconds === "number" && meta3.timeoutSeconds > 0)
34804
34827
  entry.hooks[index].timeout = meta3.timeoutSeconds;
34805
34828
  }
34806
- writeSettings(settings, scope, target);
34829
+ write(settings, scope, target);
34807
34830
  return;
34808
34831
  }
34809
34832
  }
@@ -34823,7 +34846,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
34823
34846
  }
34824
34847
  settings.hooks[eventKey].push(entry);
34825
34848
  }
34826
- writeSettings(settings, scope, target);
34849
+ write(settings, scope, target);
34827
34850
  }
34828
34851
  function removeHookFromAllEvents(settings, name) {
34829
34852
  if (!settings.hooks)