@hasna/skills 0.9.13 → 0.9.14

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
@@ -36868,7 +36868,7 @@ var package_default;
36868
36868
  var init_package = __esm(() => {
36869
36869
  package_default = {
36870
36870
  name: "@hasna/skills",
36871
- version: "0.9.13",
36871
+ version: "0.9.14",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -50548,7 +50548,7 @@ function canonical2(value) {
50548
50548
  function inferencePreference(key, child) {
50549
50549
  if (key === "skipDangerousModePermissionPrompt") {
50550
50550
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
50551
- return true;
50551
+ return false;
50552
50552
  }
50553
50553
  if (key === "effortLevel") {
50554
50554
  need2(child.kind === "string" && PERSISTED_EFFORT_LEVELS.has(child.value), "contains an invalid persisted effort level");
@@ -51233,6 +51233,20 @@ function planAgentIntegration(options) {
51233
51233
  recheckRootAliases(aliases);
51234
51234
  return { dataDir: dataDir3, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
51235
51235
  }
51236
+ function verifyCoordinatedDiscovery(binding, home, aliases) {
51237
+ try {
51238
+ if (binding.method !== "automatic" && binding.method !== "reviewed")
51239
+ throw new Error("Invalid managed discovery method");
51240
+ verifyAgentDiscovery(binding);
51241
+ if (binding.method === "automatic") {
51242
+ const current = resolveAgentDiscovery({ home, agent: binding.agent, canonical: (path) => canonicalAgentPath(path, aliases) });
51243
+ if (JSON.stringify(current) !== JSON.stringify(binding))
51244
+ throw new Error("Configured native discovery roots changed during Claude update");
51245
+ }
51246
+ } catch (error) {
51247
+ throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
51248
+ }
51249
+ }
51236
51250
  function atomicWrite2(path, content) {
51237
51251
  assertSafePath(path);
51238
51252
  mkdirSync5(dirname10(path), { recursive: true, mode: 448 });
@@ -51257,8 +51271,12 @@ function applyAgentIntegration(plan) {
51257
51271
  recheckRootAliases(aliases);
51258
51272
  for (const agent of plan.managedAgentChecks?.agents ?? [])
51259
51273
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
51260
- for (const binding of plan.discoveryBefore ?? [])
51261
- verifyAgentDiscovery(binding);
51274
+ for (const binding of plan.discoveryBefore ?? []) {
51275
+ if (plan.managedAgentChecks)
51276
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
51277
+ else
51278
+ verifyAgentDiscovery(binding);
51279
+ }
51262
51280
  if (plan.observedPolicy && currentText(plan.observedPolicy.path) !== plan.observedPolicy.before)
51263
51281
  throw new Error("Agent policy changed after planning");
51264
51282
  for (const change of plan.changes)
@@ -51290,8 +51308,12 @@ function applyAgentIntegration(plan) {
51290
51308
  written.push({ path: change.path, before: change.before, after: after2 });
51291
51309
  }
51292
51310
  recheckRootAliases(aliases);
51293
- for (const binding of plan.discoveryAfter ?? [])
51294
- verifyAgentDiscovery(binding);
51311
+ for (const binding of plan.discoveryAfter ?? []) {
51312
+ if (plan.managedAgentChecks)
51313
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
51314
+ else
51315
+ verifyAgentDiscovery(binding);
51316
+ }
51295
51317
  for (const agent of plan.managedAgentChecks?.agents ?? [])
51296
51318
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
51297
51319
  atomicWrite2(join20(backupRoot, "receipt.json"), JSON.stringify({ version: 1, changes: written.map((change) => ({ path: change.path, beforeHash: change.before === null ? null : sha(change.before), afterHash: sha(change.after) })), backups, ...aliases.length ? { rootAliases: aliases } : {} }) + `
package/bin/mcp.js CHANGED
@@ -20019,7 +20019,7 @@ var package_default;
20019
20019
  var init_package = __esm(() => {
20020
20020
  package_default = {
20021
20021
  name: "@hasna/skills",
20022
- version: "0.9.13",
20022
+ version: "0.9.14",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -47083,7 +47083,7 @@ function canonical2(value) {
47083
47083
  function inferencePreference(key, child) {
47084
47084
  if (key === "skipDangerousModePermissionPrompt") {
47085
47085
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
47086
- return true;
47086
+ return false;
47087
47087
  }
47088
47088
  if (key === "effortLevel") {
47089
47089
  need2(child.kind === "string" && PERSISTED_EFFORT_LEVELS.has(child.value), "contains an invalid persisted effort level");
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join5 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.9.13",
10
+ version: "0.9.14",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
24122
24122
  // package.json
24123
24123
  var package_default = {
24124
24124
  name: "@hasna/skills",
24125
- version: "0.9.13",
24125
+ version: "0.9.14",
24126
24126
  description: "Skills library for AI coding agents",
24127
24127
  type: "module",
24128
24128
  bin: {
package/bin/worker.js CHANGED
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24125
24125
  // package.json
24126
24126
  var package_default = {
24127
24127
  name: "@hasna/skills",
24128
- version: "0.9.13",
24128
+ version: "0.9.14",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
package/dist/index.d.ts CHANGED
@@ -60,7 +60,7 @@ export type { SkillSelection, SkillProfile, ResolvedSkillSelection, ResolvedSkil
60
60
  export { buildSkillContext } from "./lib/skill-context.js";
61
61
  export { syncSelectionProfile } from "./lib/selection-resolver.js";
62
62
  export { inspectSkillSession, reconcileSkillSession, type SessionReconciliationInput, type SessionReconciliationOptions } from "./lib/session-reconciliation.js";
63
- export { planAgentIntegration, planClaudeStopHookUpdate, applyAgentIntegration, inventoryNativeSkills, archiveNativeSkills } from "./lib/agent-integration.js";
63
+ export { planAgentIntegration, planClaudeStopHookUpdate, planClaudePreToolUseHookUpdate, planClaudeHookEventsUpdate, applyAgentIntegration, inventoryNativeSkills, archiveNativeSkills } from "./lib/agent-integration.js";
64
64
  export { captureDiscoveryDirectories, captureDiscoveryByteSources, captureDiscoveryPathSources, type DiscoveryDirectory, type DiscoverySource } from "./lib/agent-discovery.js";
65
65
  export { captureClaudeMarketplaceRegistry, captureClaudeMarketplaceRegistryV2 } from "./lib/claude-marketplace-registry.js";
66
66
  export { captureClaudeSettings, captureClaudeSettingsV2 } from "./lib/claude-settings-witness.js";
@@ -69,3 +69,4 @@ export { planPluginAdmission, admitPlugin, resolveAdmittedPlugin, pluginResolver
69
69
  export { captureManagedPluginRegistry, type ManagedPluginRegistrationWitness } from "./lib/plugin-discovery.js";
70
70
  export type { RemoteCreditCheckoutOptions, RemoteCreditCheckout, RemoteCreditCheckoutErrorCode } from "./lib/remote-credit-checkout.js";
71
71
  export { assertPortableAuthoringPath, type PortableAuthoringPathOptions } from "./lib/authoring-path.js";
72
+ export { CLAUDE_COORDINATED_HOOK_EVENTS, type ClaudeCoordinatedHookEvent } from "./lib/claude-settings-witness.js";
package/dist/index.js CHANGED
@@ -9610,7 +9610,7 @@ import { isAbsolute as isAbsolute11, join as join23, resolve as resolve11 } from
9610
9610
  // package.json
9611
9611
  var package_default = {
9612
9612
  name: "@hasna/skills",
9613
- version: "0.9.13",
9613
+ version: "0.9.14",
9614
9614
  description: "Skills library for AI coding agents",
9615
9615
  type: "module",
9616
9616
  bin: {
@@ -12846,32 +12846,37 @@ function canonical2(value) {
12846
12846
  return { kind: "array", items: value.items.map(canonical2) };
12847
12847
  return value;
12848
12848
  }
12849
- function assertClaudeStopHookReplacement(before, replacement) {
12849
+ var CLAUDE_COORDINATED_HOOK_EVENTS = ["PreToolUse", "PostToolUse", "Stop", "Notification", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart"];
12850
+ function assertClaudeHookEventsReplacement(before, replacement, events) {
12851
+ need2(Array.isArray(events) && events.length > 0 && events.length <= CLAUDE_COORDINATED_HOOK_EVENTS.length && new Set(events).size === events.length && events.every((event) => CLAUDE_COORDINATED_HOOK_EVENTS.includes(event)), "requires explicit supported hook events");
12852
+ const label = events.join(",");
12850
12853
  function bounded(text2) {
12851
- need2(typeof text2 === "string" && Buffer.byteLength(text2) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "Stop update exceeds its byte limit");
12854
+ need2(typeof text2 === "string" && Buffer.byteLength(text2) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, `${label} update exceeds its byte limit`);
12852
12855
  return parse3(text2);
12853
12856
  }
12854
12857
  const original = bounded(before ?? "{}"), next = bounded(replacement);
12855
12858
  const hadHooks = original.entries.some(([key]) => key === "hooks");
12856
- function withoutStop(value) {
12859
+ function withoutOwnedEvents(value) {
12857
12860
  const hooks = value.entries.find(([key]) => key === "hooks")?.[1];
12858
12861
  if (hooks !== undefined) {
12859
- need2(hooks.kind === "object", "Stop update requires an object of hooks");
12860
- const stop = hooks.entries.find(([key]) => key === "Stop")?.[1];
12861
- need2(stop === undefined || stop.kind === "array", "Stop update requires a Stop array");
12862
- hooks.entries = hooks.entries.filter(([key]) => key !== "Stop");
12862
+ need2(hooks.kind === "object", `${label} update requires an object of hooks`);
12863
+ for (const event of events) {
12864
+ const selected = hooks.entries.find(([key]) => key === event)?.[1];
12865
+ need2(selected === undefined || selected.kind === "array", `${event} update requires a ${event} array`);
12866
+ }
12867
+ hooks.entries = hooks.entries.filter(([key]) => !events.includes(key));
12863
12868
  if (!hadHooks && hooks.entries.length === 0)
12864
12869
  value.entries = value.entries.filter(([key]) => key !== "hooks");
12865
12870
  }
12866
12871
  return canonical2(value);
12867
12872
  }
12868
- need2(JSON.stringify(withoutStop(original)) === JSON.stringify(withoutStop(next)), "Stop update changed settings outside hooks.Stop");
12873
+ need2(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
12869
12874
  }
12870
12875
  var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
12871
12876
  function inferencePreference(key, child) {
12872
12877
  if (key === "skipDangerousModePermissionPrompt") {
12873
12878
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
12874
- return true;
12879
+ return false;
12875
12880
  }
12876
12881
  if (key === "effortLevel") {
12877
12882
  need2(child.kind === "string" && PERSISTED_EFFORT_LEVELS.has(child.value), "contains an invalid persisted effort level");
@@ -13452,6 +13457,34 @@ function planAgentIntegration(options) {
13452
13457
  return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
13453
13458
  }
13454
13459
  function planClaudeStopHookUpdate(options) {
13460
+ return planClaudeHookEventsUpdate({ ...options, events: ["Stop"] });
13461
+ }
13462
+ function planClaudePreToolUseHookUpdate(options) {
13463
+ return planClaudeHookEventsUpdate({ ...options, events: ["PreToolUse"] });
13464
+ }
13465
+ function claudeSettingsConsumers(home, path, discoveries, aliases) {
13466
+ const canonical3 = (value) => canonicalAgentPath(value, aliases);
13467
+ const containsSettings = (root) => {
13468
+ const distance = relative4(canonical3(root), path);
13469
+ return distance === "" || distance !== ".." && !distance.startsWith(`..${sep4}`) && !isAbsolute9(distance);
13470
+ };
13471
+ return discoveries.filter((binding) => binding.agent === "claude" || binding.agent === "opencode" || canonical3(join16(home, AGENT_ADAPTERS[binding.agent].config)) === path || binding.sources.some((source) => canonical3(source.path) === path) || binding.roots.some(containsSettings) || binding.directories?.some((directory) => containsSettings(directory.path))).map((binding) => binding.agent);
13472
+ }
13473
+ function verifyCoordinatedDiscovery(binding, home, aliases) {
13474
+ try {
13475
+ if (binding.method !== "automatic" && binding.method !== "reviewed")
13476
+ throw new Error("Invalid managed discovery method");
13477
+ verifyAgentDiscovery(binding);
13478
+ if (binding.method === "automatic") {
13479
+ const current = resolveAgentDiscovery({ home, agent: binding.agent, canonical: (path) => canonicalAgentPath(path, aliases) });
13480
+ if (JSON.stringify(current) !== JSON.stringify(binding))
13481
+ throw new Error("Configured native discovery roots changed during Claude update");
13482
+ }
13483
+ } catch (error) {
13484
+ throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
13485
+ }
13486
+ }
13487
+ function planClaudeHookEventsUpdate(options) {
13455
13488
  const home = resolve9(options.home ?? homedir2()), dataDir2 = options.dataDir ?? getDataDirReadOnly();
13456
13489
  const policyPath = join16(dataDir2, "agent-policy.json");
13457
13490
  assertSafePath(policyPath);
@@ -13469,31 +13502,42 @@ function planClaudeStopHookUpdate(options) {
13469
13502
  const before = lstatSync14(path, { throwIfNoEntry: false }) ? new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readNativeBytes(path, 1024 * 1024)) : null;
13470
13503
  if ((before === null ? "absent" : sha(before)) !== options.expectedSettingsSha256)
13471
13504
  throw new Error("Claude settings preimage changed");
13472
- assertClaudeStopHookReplacement(before, options.replacement);
13505
+ assertClaudeHookEventsReplacement(before, options.replacement, options.events);
13473
13506
  const agents = bridge.agents;
13474
13507
  const discoveries = Object.entries(bridge.discovery).map(([agent, binding]) => {
13475
- if (!agents.includes(agent) || binding?.agent !== agent)
13476
- throw new Error("Invalid managed discovery owner");
13508
+ if (!agents.includes(agent) || binding?.agent !== agent || typeof bridge.commands?.[agent] !== "string" || typeof bridge.profiles?.[agent] !== "string")
13509
+ throw new Error("Invalid managed discovery owner or command/profile binding");
13477
13510
  return binding;
13478
13511
  });
13479
- if (discoveries.length !== agents.length)
13480
- throw new Error("Missing managed discovery coverage");
13481
- for (const agent of agents)
13512
+ if (discoveries.length !== agents.length || !agents.includes("claude"))
13513
+ throw new Error("Missing managed Claude discovery coverage");
13514
+ for (const binding of discoveries)
13515
+ verifyCoordinatedDiscovery(binding, home, aliases);
13516
+ const consumers = claudeSettingsConsumers(home, path, discoveries, aliases);
13517
+ for (const agent of consumers)
13482
13518
  assertManagedAgentBridge(agent, { home, dataDir: dataDir2, projectDir: home });
13483
13519
  const replacements = new Map(before === options.replacement ? [] : [[path, options.replacement]]);
13484
- const discoveryAfter = discoveries.map((binding) => rebindAgentDiscovery(binding, replacements));
13520
+ const discoveryAfter = discoveries.map((binding) => {
13521
+ const next = rebindAgentDiscovery(binding, replacements);
13522
+ for (const [index, source] of binding.sources.entries()) {
13523
+ if (source.path !== path && JSON.stringify(next.sources[index]) !== JSON.stringify(source)) {
13524
+ throw new Error(`Unrelated native discovery input changed during Claude update: ${source.path}`);
13525
+ }
13526
+ }
13527
+ return JSON.stringify(next) === JSON.stringify(binding) ? binding : next;
13528
+ });
13485
13529
  const nextPolicy = { ...policy, bridge: { ...bridge, discovery: Object.fromEntries(discoveryAfter.map((binding) => [binding.agent, binding])) } };
13486
13530
  const changes = before === options.replacement ? [] : [{ path, before, after: options.replacement }];
13487
13531
  if (JSON.stringify(nextPolicy) !== JSON.stringify(policy))
13488
13532
  changes.push({ path: policyPath, before: snapshot.text, after: serializeManagedSkillPolicy(nextPolicy) });
13489
13533
  recheckRootAliases(aliases);
13490
13534
  const nativeSkills = inventoryNativeSkills(home, {
13491
- agents,
13535
+ agents: consumers,
13492
13536
  projectDir: home,
13493
13537
  includeVendor: true,
13494
- guardHermes: agents.includes("hermes"),
13538
+ guardHermes: consumers.includes("hermes"),
13495
13539
  allowRootAliases: aliases.length > 0,
13496
- agentRoots: discoveries.flatMap((binding) => binding.roots.map((path2) => ({ agent: binding.agent, path: path2 })))
13540
+ agentRoots: discoveries.filter((binding) => consumers.includes(binding.agent)).flatMap((binding) => binding.roots.map((path2) => ({ agent: binding.agent, path: path2 })))
13497
13541
  });
13498
13542
  return {
13499
13543
  dataDir: dataDir2,
@@ -13504,7 +13548,7 @@ function planClaudeStopHookUpdate(options) {
13504
13548
  discoveryBefore: discoveries,
13505
13549
  discoveryAfter,
13506
13550
  rootAliases: aliases,
13507
- managedAgentChecks: { home, agents: [...agents] }
13551
+ managedAgentChecks: { home, agents: consumers }
13508
13552
  };
13509
13553
  }
13510
13554
  function atomicWrite2(path, content) {
@@ -13531,8 +13575,12 @@ function applyAgentIntegration(plan) {
13531
13575
  recheckRootAliases(aliases);
13532
13576
  for (const agent of plan.managedAgentChecks?.agents ?? [])
13533
13577
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
13534
- for (const binding of plan.discoveryBefore ?? [])
13535
- verifyAgentDiscovery(binding);
13578
+ for (const binding of plan.discoveryBefore ?? []) {
13579
+ if (plan.managedAgentChecks)
13580
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
13581
+ else
13582
+ verifyAgentDiscovery(binding);
13583
+ }
13536
13584
  if (plan.observedPolicy && currentText(plan.observedPolicy.path) !== plan.observedPolicy.before)
13537
13585
  throw new Error("Agent policy changed after planning");
13538
13586
  for (const change of plan.changes)
@@ -13564,8 +13612,12 @@ function applyAgentIntegration(plan) {
13564
13612
  written.push({ path: change.path, before: change.before, after });
13565
13613
  }
13566
13614
  recheckRootAliases(aliases);
13567
- for (const binding of plan.discoveryAfter ?? [])
13568
- verifyAgentDiscovery(binding);
13615
+ for (const binding of plan.discoveryAfter ?? []) {
13616
+ if (plan.managedAgentChecks)
13617
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
13618
+ else
13619
+ verifyAgentDiscovery(binding);
13620
+ }
13569
13621
  for (const agent of plan.managedAgentChecks?.agents ?? [])
13570
13622
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
13571
13623
  atomicWrite2(join16(backupRoot, "receipt.json"), JSON.stringify({ version: 1, changes: written.map((change) => ({ path: change.path, beforeHash: change.before === null ? null : sha(change.before), afterHash: sha(change.after) })), backups, ...aliases.length ? { rootAliases: aliases } : {} }) + `
@@ -43658,6 +43710,8 @@ export {
43658
43710
  planSkillsS3SnapshotUpload,
43659
43711
  planPluginAdmission,
43660
43712
  planClaudeStopHookUpdate,
43713
+ planClaudePreToolUseHookUpdate,
43714
+ planClaudeHookEventsUpdate,
43661
43715
  planAgentIntegration,
43662
43716
  pinSkill,
43663
43717
  pinProjectSkill,
@@ -43861,6 +43915,7 @@ export {
43861
43915
  DEFAULT_EXPORT_DIR,
43862
43916
  ContentHashInputError,
43863
43917
  CONTENT_HASH_LIMITS,
43918
+ CLAUDE_COORDINATED_HOOK_EVENTS,
43864
43919
  CATEGORIES,
43865
43920
  BASIC_SKILL_NAMES,
43866
43921
  ARTICLE_GENERATION_SLUG,
@@ -1,6 +1,7 @@
1
1
  import { type AgentDiscoveryBinding, type ReviewedDiscoveryInputs } from "./agent-discovery.js";
2
2
  import { type IntegrationAgent } from "./agent-adapters.js";
3
3
  import type { CodexHookDiscoveryRecovery } from "./codex-hook-discovery-recovery.js";
4
+ import { type ClaudeCoordinatedHookEvent } from "./claude-settings-witness.js";
4
5
  export type { IntegrationAgent } from "./agent-adapters.js";
5
6
  export type ContextHookEvent = "UserPromptSubmit" | "SessionStart" | "SubagentStart";
6
7
  export interface AgentRootAlias {
@@ -96,11 +97,18 @@ export declare function planAgentIntegration(options: {
96
97
  * every other settings change and owns discovery witnesses and guarded writes.
97
98
  * No policy means this installer has no Skills integration to update.
98
99
  */
99
- export declare function planClaudeStopHookUpdate(options: {
100
+ export interface ClaudeHookUpdateOptions {
100
101
  home?: string;
101
102
  dataDir?: string;
102
103
  expectedSettingsSha256: string;
103
104
  replacement: string;
105
+ }
106
+ export declare function planClaudeStopHookUpdate(options: ClaudeHookUpdateOptions): AgentIntegrationPlan | null;
107
+ /** Cooperating safety installers may replace only PreToolUse. The caller owns
108
+ * its exact hook authorization; Skills retains discovery and native guards. */
109
+ export declare function planClaudePreToolUseHookUpdate(options: ClaudeHookUpdateOptions): AgentIntegrationPlan | null;
110
+ export declare function planClaudeHookEventsUpdate(options: ClaudeHookUpdateOptions & {
111
+ events: readonly ClaudeCoordinatedHookEvent[];
104
112
  }): AgentIntegrationPlan | null;
105
113
  export declare function applyAgentIntegration(plan: AgentIntegrationPlan): {
106
114
  changed: string[];
@@ -9,12 +9,16 @@ export declare const CLAUDE_SETTINGS_WITNESS_LIMITS: Readonly<{
9
9
  export interface ClaudeSettingsWitnessBudget {
10
10
  remaining: number;
11
11
  }
12
- /** Cooperating installers may replace Stop only. Compare the strict syntax
12
+ /** Cooperating installers may replace only their explicitly selected event. Compare the strict syntax
13
13
  * trees, not JSON.parse values or the preference-tolerant witness digest: all
14
14
  * other hooks, discovery settings, unknown values and number spellings remain
15
15
  * identical. Whitespace and object-key order have no configuration meaning.
16
16
  */
17
+ export declare const CLAUDE_COORDINATED_HOOK_EVENTS: readonly ["PreToolUse", "PostToolUse", "Stop", "Notification", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart"];
18
+ export type ClaudeCoordinatedHookEvent = typeof CLAUDE_COORDINATED_HOOK_EVENTS[number];
19
+ export declare function assertClaudeHookEventsReplacement(before: string | null, replacement: string, events: readonly ClaudeCoordinatedHookEvent[]): void;
17
20
  export declare function assertClaudeStopHookReplacement(before: string | null, replacement: string): void;
21
+ export declare function assertClaudePreToolUseHookReplacement(before: string | null, replacement: string): void;
18
22
  /** Hash only reviewed installer-rendered settings after the disk preimage has been verified. */
19
23
  export declare function hashClaudeSettingsReplacement(text: string, budget: ClaudeSettingsWitnessBudget): string;
20
24
  /** Explicit capture never converts a stored legacy raw witness or changes settings. */
package/dist/sdk/index.js CHANGED
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
32373
32373
  // package.json
32374
32374
  var package_default = {
32375
32375
  name: "@hasna/skills",
32376
- version: "0.9.13",
32376
+ version: "0.9.14",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -40126,32 +40126,37 @@ function canonical3(value) {
40126
40126
  return { kind: "array", items: value.items.map(canonical3) };
40127
40127
  return value;
40128
40128
  }
40129
- function assertClaudeStopHookReplacement(before, replacement) {
40129
+ var CLAUDE_COORDINATED_HOOK_EVENTS = ["PreToolUse", "PostToolUse", "Stop", "Notification", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart"];
40130
+ function assertClaudeHookEventsReplacement(before, replacement, events) {
40131
+ need3(Array.isArray(events) && events.length > 0 && events.length <= CLAUDE_COORDINATED_HOOK_EVENTS.length && new Set(events).size === events.length && events.every((event) => CLAUDE_COORDINATED_HOOK_EVENTS.includes(event)), "requires explicit supported hook events");
40132
+ const label = events.join(",");
40130
40133
  function bounded(text2) {
40131
- need3(typeof text2 === "string" && Buffer.byteLength(text2) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "Stop update exceeds its byte limit");
40134
+ need3(typeof text2 === "string" && Buffer.byteLength(text2) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, `${label} update exceeds its byte limit`);
40132
40135
  return parse6(text2);
40133
40136
  }
40134
40137
  const original = bounded(before ?? "{}"), next = bounded(replacement);
40135
40138
  const hadHooks = original.entries.some(([key]) => key === "hooks");
40136
- function withoutStop(value) {
40139
+ function withoutOwnedEvents(value) {
40137
40140
  const hooks = value.entries.find(([key]) => key === "hooks")?.[1];
40138
40141
  if (hooks !== undefined) {
40139
- need3(hooks.kind === "object", "Stop update requires an object of hooks");
40140
- const stop = hooks.entries.find(([key]) => key === "Stop")?.[1];
40141
- need3(stop === undefined || stop.kind === "array", "Stop update requires a Stop array");
40142
- hooks.entries = hooks.entries.filter(([key]) => key !== "Stop");
40142
+ need3(hooks.kind === "object", `${label} update requires an object of hooks`);
40143
+ for (const event of events) {
40144
+ const selected = hooks.entries.find(([key]) => key === event)?.[1];
40145
+ need3(selected === undefined || selected.kind === "array", `${event} update requires a ${event} array`);
40146
+ }
40147
+ hooks.entries = hooks.entries.filter(([key]) => !events.includes(key));
40143
40148
  if (!hadHooks && hooks.entries.length === 0)
40144
40149
  value.entries = value.entries.filter(([key]) => key !== "hooks");
40145
40150
  }
40146
40151
  return canonical3(value);
40147
40152
  }
40148
- need3(JSON.stringify(withoutStop(original)) === JSON.stringify(withoutStop(next)), "Stop update changed settings outside hooks.Stop");
40153
+ need3(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
40149
40154
  }
40150
40155
  var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
40151
40156
  function inferencePreference(key, child) {
40152
40157
  if (key === "skipDangerousModePermissionPrompt") {
40153
40158
  need3(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
40154
- return true;
40159
+ return false;
40155
40160
  }
40156
40161
  if (key === "effortLevel") {
40157
40162
  need3(child.kind === "string" && PERSISTED_EFFORT_LEVELS.has(child.value), "contains an invalid persisted effort level");
@@ -41301,6 +41306,34 @@ function planAgentIntegration(options) {
41301
41306
  return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
41302
41307
  }
41303
41308
  function planClaudeStopHookUpdate(options) {
41309
+ return planClaudeHookEventsUpdate({ ...options, events: ["Stop"] });
41310
+ }
41311
+ function planClaudePreToolUseHookUpdate(options) {
41312
+ return planClaudeHookEventsUpdate({ ...options, events: ["PreToolUse"] });
41313
+ }
41314
+ function claudeSettingsConsumers(home, path, discoveries, aliases) {
41315
+ const canonical4 = (value) => canonicalAgentPath(value, aliases);
41316
+ const containsSettings = (root2) => {
41317
+ const distance = relative3(canonical4(root2), path);
41318
+ return distance === "" || distance !== ".." && !distance.startsWith(`..${sep4}`) && !isAbsolute15(distance);
41319
+ };
41320
+ return discoveries.filter((binding) => binding.agent === "claude" || binding.agent === "opencode" || canonical4(join21(home, AGENT_ADAPTERS[binding.agent].config)) === path || binding.sources.some((source) => canonical4(source.path) === path) || binding.roots.some(containsSettings) || binding.directories?.some((directory) => containsSettings(directory.path))).map((binding) => binding.agent);
41321
+ }
41322
+ function verifyCoordinatedDiscovery(binding, home, aliases) {
41323
+ try {
41324
+ if (binding.method !== "automatic" && binding.method !== "reviewed")
41325
+ throw new Error("Invalid managed discovery method");
41326
+ verifyAgentDiscovery(binding);
41327
+ if (binding.method === "automatic") {
41328
+ const current = resolveAgentDiscovery({ home, agent: binding.agent, canonical: (path) => canonicalAgentPath(path, aliases) });
41329
+ if (JSON.stringify(current) !== JSON.stringify(binding))
41330
+ throw new Error("Configured native discovery roots changed during Claude update");
41331
+ }
41332
+ } catch (error) {
41333
+ throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
41334
+ }
41335
+ }
41336
+ function planClaudeHookEventsUpdate(options) {
41304
41337
  const home = resolve14(options.home ?? homedir3()), dataDir2 = options.dataDir ?? getDataDirReadOnly();
41305
41338
  const policyPath = join21(dataDir2, "agent-policy.json");
41306
41339
  assertSafePath(policyPath);
@@ -41318,31 +41351,42 @@ function planClaudeStopHookUpdate(options) {
41318
41351
  const before = lstatSync18(path, { throwIfNoEntry: false }) ? new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readNativeBytes(path, 1024 * 1024)) : null;
41319
41352
  if ((before === null ? "absent" : sha2(before)) !== options.expectedSettingsSha256)
41320
41353
  throw new Error("Claude settings preimage changed");
41321
- assertClaudeStopHookReplacement(before, options.replacement);
41354
+ assertClaudeHookEventsReplacement(before, options.replacement, options.events);
41322
41355
  const agents = bridge.agents;
41323
41356
  const discoveries = Object.entries(bridge.discovery).map(([agent, binding]) => {
41324
- if (!agents.includes(agent) || binding?.agent !== agent)
41325
- throw new Error("Invalid managed discovery owner");
41357
+ if (!agents.includes(agent) || binding?.agent !== agent || typeof bridge.commands?.[agent] !== "string" || typeof bridge.profiles?.[agent] !== "string")
41358
+ throw new Error("Invalid managed discovery owner or command/profile binding");
41326
41359
  return binding;
41327
41360
  });
41328
- if (discoveries.length !== agents.length)
41329
- throw new Error("Missing managed discovery coverage");
41330
- for (const agent of agents)
41361
+ if (discoveries.length !== agents.length || !agents.includes("claude"))
41362
+ throw new Error("Missing managed Claude discovery coverage");
41363
+ for (const binding of discoveries)
41364
+ verifyCoordinatedDiscovery(binding, home, aliases);
41365
+ const consumers = claudeSettingsConsumers(home, path, discoveries, aliases);
41366
+ for (const agent of consumers)
41331
41367
  assertManagedAgentBridge(agent, { home, dataDir: dataDir2, projectDir: home });
41332
41368
  const replacements = new Map(before === options.replacement ? [] : [[path, options.replacement]]);
41333
- const discoveryAfter = discoveries.map((binding) => rebindAgentDiscovery(binding, replacements));
41369
+ const discoveryAfter = discoveries.map((binding) => {
41370
+ const next = rebindAgentDiscovery(binding, replacements);
41371
+ for (const [index, source] of binding.sources.entries()) {
41372
+ if (source.path !== path && JSON.stringify(next.sources[index]) !== JSON.stringify(source)) {
41373
+ throw new Error(`Unrelated native discovery input changed during Claude update: ${source.path}`);
41374
+ }
41375
+ }
41376
+ return JSON.stringify(next) === JSON.stringify(binding) ? binding : next;
41377
+ });
41334
41378
  const nextPolicy = { ...policy, bridge: { ...bridge, discovery: Object.fromEntries(discoveryAfter.map((binding) => [binding.agent, binding])) } };
41335
41379
  const changes = before === options.replacement ? [] : [{ path, before, after: options.replacement }];
41336
41380
  if (JSON.stringify(nextPolicy) !== JSON.stringify(policy))
41337
41381
  changes.push({ path: policyPath, before: snapshot2.text, after: serializeManagedSkillPolicy(nextPolicy) });
41338
41382
  recheckRootAliases(aliases);
41339
41383
  const nativeSkills = inventoryNativeSkills(home, {
41340
- agents,
41384
+ agents: consumers,
41341
41385
  projectDir: home,
41342
41386
  includeVendor: true,
41343
- guardHermes: agents.includes("hermes"),
41387
+ guardHermes: consumers.includes("hermes"),
41344
41388
  allowRootAliases: aliases.length > 0,
41345
- agentRoots: discoveries.flatMap((binding) => binding.roots.map((path2) => ({ agent: binding.agent, path: path2 })))
41389
+ agentRoots: discoveries.filter((binding) => consumers.includes(binding.agent)).flatMap((binding) => binding.roots.map((path2) => ({ agent: binding.agent, path: path2 })))
41346
41390
  });
41347
41391
  return {
41348
41392
  dataDir: dataDir2,
@@ -41353,7 +41397,7 @@ function planClaudeStopHookUpdate(options) {
41353
41397
  discoveryBefore: discoveries,
41354
41398
  discoveryAfter,
41355
41399
  rootAliases: aliases,
41356
- managedAgentChecks: { home, agents: [...agents] }
41400
+ managedAgentChecks: { home, agents: consumers }
41357
41401
  };
41358
41402
  }
41359
41403
  function atomicWrite2(path, content) {
@@ -41380,8 +41424,12 @@ function applyAgentIntegration(plan) {
41380
41424
  recheckRootAliases(aliases);
41381
41425
  for (const agent of plan.managedAgentChecks?.agents ?? [])
41382
41426
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
41383
- for (const binding of plan.discoveryBefore ?? [])
41384
- verifyAgentDiscovery(binding);
41427
+ for (const binding of plan.discoveryBefore ?? []) {
41428
+ if (plan.managedAgentChecks)
41429
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
41430
+ else
41431
+ verifyAgentDiscovery(binding);
41432
+ }
41385
41433
  if (plan.observedPolicy && currentText(plan.observedPolicy.path) !== plan.observedPolicy.before)
41386
41434
  throw new Error("Agent policy changed after planning");
41387
41435
  for (const change of plan.changes)
@@ -41413,8 +41461,12 @@ function applyAgentIntegration(plan) {
41413
41461
  written.push({ path: change.path, before: change.before, after });
41414
41462
  }
41415
41463
  recheckRootAliases(aliases);
41416
- for (const binding of plan.discoveryAfter ?? [])
41417
- verifyAgentDiscovery(binding);
41464
+ for (const binding of plan.discoveryAfter ?? []) {
41465
+ if (plan.managedAgentChecks)
41466
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
41467
+ else
41468
+ verifyAgentDiscovery(binding);
41469
+ }
41418
41470
  for (const agent of plan.managedAgentChecks?.agents ?? [])
41419
41471
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
41420
41472
  atomicWrite2(join21(backupRoot, "receipt.json"), JSON.stringify({ version: 1, changes: written.map((change) => ({ path: change.path, beforeHash: change.before === null ? null : sha2(change.before), afterHash: sha2(change.after) })), backups, ...aliases.length ? { rootAliases: aliases } : {} }) + `
@@ -294,9 +294,11 @@ addition to the v1 display/model selections, it tolerates the documented
294
294
  persisted effort values `low`, `medium`, `high`, and `xhigh`, both at the top
295
295
  level and in known built-in model entries whose only property is `effortLevel`.
296
296
  Custom model entries and entries with any other property remain fully bound.
297
- The boolean `skipDangerousModePermissionPrompt` is Claude's saved dialog
298
- acknowledgment; it does not select a permission mode. Permission settings,
299
- plugins, hooks, environment, ultracode and unknown configuration remain bound.
297
+ The boolean `skipDangerousModePermissionPrompt` remains bound: Claude also uses
298
+ this saved acceptance in bypass-mode and pending project MCP approval decisions.
299
+ Permission settings, plugins, hooks, environment, ultracode and unknown
300
+ configuration remain bound. Version 0.9.14 corrects the 0.9.13 normalization of
301
+ this flag; an existing v2 review containing it must be explicitly reviewed again.
300
302
  Invalid recognized preference types are refused.
301
303
 
302
304
  `captureClaudeMarketplaceRegistryV2(path)` creates a
@@ -322,3 +324,25 @@ preference: its owning installer must coordinate a guarded settings/policy
322
324
  update, or it needs a fresh explicit review.
323
325
 
324
326
  The effort schema follows [Claude's model configuration documentation](https://code.claude.com/docs/en/model-config#adjust-effort-level).
327
+
328
+ Cooperating installers can use `planClaudeHookEventsUpdate` with an explicit
329
+ selection of supported Claude hook events. `planClaudePreToolUseHookUpdate` and
330
+ `planClaudeStopHookUpdate` are single-event convenience wrappers. Supported events
331
+ are `PreToolUse`, `PostToolUse`, `Stop`, `Notification`, `SessionStart`, `SessionEnd`
332
+ `UserPromptSubmit` and `SubagentStart`. The transaction requires the exact settings SHA-256
333
+ preimage and preserves every field outside the selected events, including other
334
+ hooks. Apply the returned plan through `applyAgentIntegration`; this validates
335
+ existing managed discovery and native guards, writes settings and policy with
336
+ precondition checks, and retains recovery backups. An invalid policy or existing
337
+ drift is a refusal, never permission to use an uncoordinated writer. A null plan
338
+ means no managed Skills policy exists. These APIs do not authorize the hook's
339
+ behavior or turn a configuration-changing hook into a preference.
340
+
341
+ The transaction verifies every agent's discovery sources, directories and
342
+ automatic discovery closure. Native runtime and trust checks cover Claude and
343
+ the declared consumers of the settings path, including containing roots or
344
+ directories and OpenCode's Claude compatibility surface. Unrelated agents keep
345
+ their exact witnesses and trust state; they must still pass their own consumer
346
+ checks before use. The plan's `managedAgentChecks.agents` and `nativeSkills`
347
+ describe this scoped runtime check, not a fleet health result. A path-identity
348
+ witness intersecting the write still refuses instead of adopting a new inode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.9.13",
3
+ "version": "0.9.14",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {