@hasna/skills 0.10.34 → 0.10.35

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.
@@ -133,6 +133,33 @@ export declare function planAgentIntegration(options: {
133
133
  reviewedCacheAlias?: string;
134
134
  codexNativeCatalog?: CodexNativeSkillCatalog;
135
135
  }): AgentIntegrationPlan;
136
+ export interface ClaudeManagedHookProjection {
137
+ schema: "hasna.claude-managed-hook-projection.v1";
138
+ targetSha256: string;
139
+ sourceSha256: string;
140
+ replacements: Array<{
141
+ event: string;
142
+ groupIndex: number;
143
+ hookIndex: number;
144
+ beforeSha256: string;
145
+ command: string;
146
+ }>;
147
+ }
148
+ export interface ClaudeManagedHookProjectionOptions {
149
+ home?: string;
150
+ dataDir?: string;
151
+ projectDir?: string;
152
+ /** Exact caller-owned bytes, never a new native home or a path to be written. */
153
+ targetSettings: string;
154
+ expectedTargetSha256: string;
155
+ /** Supply the preceding plan's source witness when checking it before use. */
156
+ expectedSourceSha256?: string;
157
+ }
158
+ /** Read-only projection for a copied Claude configuration. This does not install
159
+ * hooks, adopt target discovery, change policy, or confer write/admission authority.
160
+ * The caller must preserve the full target, hold its owning writer lease, replan
161
+ * against both witnesses, CAS the full bytes and revalidate after application. */
162
+ export declare function planClaudeManagedHookProjection(options: ClaudeManagedHookProjectionOptions): ClaudeManagedHookProjection;
136
163
  /** Plan an explicitly authorized Stop hook replacement without adopting drift.
137
164
  * The caller owns authorization of its exact Stop hook delta; Skills refuses
138
165
  * every other settings change and owns discovery witnesses and guarded writes.
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.10.34",
32376
+ version: "0.10.35",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -42695,12 +42695,15 @@ function disableCodexBundledSkills(text2) {
42695
42695
  // src/lib/agent-integration.ts
42696
42696
  var sha4 = (data) => createHash18("sha256").update(data).digest("hex");
42697
42697
  var ROOTS = NATIVE_SKILL_ROOTS;
42698
- function hasNoDarwinAcl(fd) {
42698
+ function hasTrustedDarwinAcl(fd, allowDenyOnly = false) {
42699
42699
  try {
42700
- const { dlopen, read: read3 } = __require("bun:ffi");
42700
+ const { dlopen, read: read3, ptr } = __require("bun:ffi");
42701
42701
  const library = dlopen("/usr/lib/libSystem.B.dylib", {
42702
42702
  acl_get_fd: { args: ["i32"], returns: "ptr" },
42703
42703
  acl_free: { args: ["ptr"], returns: "i32" },
42704
+ acl_valid: { args: ["ptr"], returns: "i32" },
42705
+ acl_get_entry: { args: ["ptr", "i32", "ptr"], returns: "i32" },
42706
+ acl_get_tag_type: { args: ["ptr", "ptr"], returns: "i32" },
42704
42707
  __error: { args: [], returns: "ptr" }
42705
42708
  });
42706
42709
  try {
@@ -42709,8 +42712,23 @@ function hasNoDarwinAcl(fd) {
42709
42712
  return false;
42710
42713
  const acl = library.symbols.acl_get_fd(fd);
42711
42714
  if (acl) {
42712
- library.symbols.acl_free(acl);
42713
- return false;
42715
+ try {
42716
+ if (!allowDenyOnly || library.symbols.acl_valid(acl) !== 0)
42717
+ return false;
42718
+ const entry = new BigUint64Array(1), tag = new Int32Array(1);
42719
+ for (let count = 0;count <= 128; count++) {
42720
+ const status = library.symbols.acl_get_entry(acl, count === 0 ? 0 : -1, ptr(entry));
42721
+ if (status !== 0) {
42722
+ return status === -1 && read3.i32(errno) === 22 && library.symbols.acl_valid(acl) === 0;
42723
+ }
42724
+ const value = read3.ptr(ptr(entry));
42725
+ if (count === 128 || !value || library.symbols.acl_get_tag_type(value, ptr(tag)) !== 0 || tag[0] !== 2)
42726
+ return false;
42727
+ }
42728
+ return false;
42729
+ } finally {
42730
+ library.symbols.acl_free(acl);
42731
+ }
42714
42732
  }
42715
42733
  return (acl === null || acl === 0) && read3.i32(errno) === 2;
42716
42734
  } finally {
@@ -42740,7 +42758,7 @@ function isSystemRootAlias(path) {
42740
42758
  throw new Error("Unprotected system temporary directory");
42741
42759
  const fd = openSync16(directory, constants12.O_RDONLY | constants12.O_DIRECTORY | constants12.O_NOFOLLOW);
42742
42760
  try {
42743
- if (identity4(fstatSync16(fd, { bigint: true })) !== identity4(stat2) || !hasNoDarwinAcl(fd) || identity4(fstatSync16(fd, { bigint: true })) !== identity4(stat2))
42761
+ if (identity4(fstatSync16(fd, { bigint: true })) !== identity4(stat2) || !hasTrustedDarwinAcl(fd) || identity4(fstatSync16(fd, { bigint: true })) !== identity4(stat2))
42744
42762
  throw new Error("Unverified system alias directory ACL");
42745
42763
  } finally {
42746
42764
  closeSync16(fd);
@@ -43388,6 +43406,171 @@ function planAgentIntegration(options) {
43388
43406
  recheckRootAliases(aliases);
43389
43407
  return { dataDir: dataDir2, profileId, changes, nativeSkills, ...observedNativeSources.length ? { observedNativeSources } : {}, ...nextPolicy.bridge.codexPluginSkillReview ? { codexPluginSkillReview: nextPolicy.bridge.codexPluginSkillReview } : {}, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {}, ...retainedAgents.length ? { retainedReviewChecks: { home, projectDir: options.projectDir ?? home, agents: retainedAgents } } : {} };
43390
43408
  }
43409
+ function hookProjectionRefusal(reason) {
43410
+ throw new Error(`CLAUDE_MANAGED_HOOK_${reason}`);
43411
+ }
43412
+ function projectedHookAcl(path, observed) {
43413
+ if (process.platform !== "darwin")
43414
+ return;
43415
+ const identity4 = (stat2) => `${stat2.dev}:${stat2.ino}:${stat2.ctimeNs}:${stat2.uid}:${stat2.mode}:${stat2.nlink}:${stat2.size}:${stat2.mtimeNs}`;
43416
+ const fd = openSync16(path, constants12.O_RDONLY | constants12.O_NOFOLLOW | constants12.O_NONBLOCK | (observed.isDirectory() ? constants12.O_DIRECTORY : 0));
43417
+ try {
43418
+ if (identity4(fstatSync16(fd, { bigint: true })) !== identity4(observed) || !hasTrustedDarwinAcl(fd, true) || identity4(fstatSync16(fd, { bigint: true })) !== identity4(observed) || identity4(lstatSync20(path, { bigint: true })) !== identity4(observed))
43419
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43420
+ } finally {
43421
+ closeSync16(fd);
43422
+ }
43423
+ }
43424
+ function projectedHookExecutable(command, expectedResolved) {
43425
+ try {
43426
+ if (!isAbsolute17(command) || /[\0\r\n]/.test(command) || resolve16(command) !== command)
43427
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43428
+ command = canonicalSystemPath(command);
43429
+ const parents3 = (file = command) => {
43430
+ const result = [];
43431
+ for (let path = dirname15(file);; path = dirname15(path)) {
43432
+ const stat2 = lstatSync20(path, { bigint: true });
43433
+ if (!stat2.isDirectory() || (stat2.mode & 0o022n) !== 0n && !(stat2.uid === 0n && (stat2.mode & 0o1000n) !== 0n))
43434
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43435
+ projectedHookAcl(path, stat2);
43436
+ result.push(`${path}:${stat2.dev}:${stat2.ino}:${stat2.uid}:${stat2.mode}`);
43437
+ if (dirname15(path) === path)
43438
+ break;
43439
+ }
43440
+ return result;
43441
+ };
43442
+ const parentIdentity = parents3(), link = lstatSync20(command, { bigint: true });
43443
+ if (link.uid !== BigInt(process.getuid()) || !link.isFile() && !link.isSymbolicLink())
43444
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43445
+ const linkIdentity = (stat2) => `${stat2.dev}:${stat2.ino}:${stat2.mode}:${stat2.uid}:${stat2.ctimeNs}`;
43446
+ const resolved = realpathSync8(command);
43447
+ if (expectedResolved !== undefined && resolved !== expectedResolved)
43448
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43449
+ const resolvedParentIdentity = parents3(resolved);
43450
+ const manifest = snapshot(join23(dirname15(dirname15(resolved)), "package.json"), false, { readOnlyPackage: true });
43451
+ projectedHookAcl(manifest.file, manifest.stat);
43452
+ const pkg = JSON.parse(manifest.text);
43453
+ if (pkg.name !== "@hasna/skills" || !/^\d+\.\d+\.\d+$/.test(pkg.version) || pkg.bin?.skills !== "bin/index.js" || resolved !== join23(dirname15(manifest.file), "bin/index.js"))
43454
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43455
+ const cli = snapshot(resolved, false, { readOnlyPackage: true });
43456
+ projectedHookAcl(cli.file, cli.stat);
43457
+ if ((cli.stat.mode & 0o100n) === 0n)
43458
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43459
+ const recheck = () => {
43460
+ try {
43461
+ if (realpathSync8(command) !== resolved || linkIdentity(lstatSync20(command, { bigint: true })) !== linkIdentity(link) || !isDeepStrictEqual6(parents3(), parentIdentity) || !isDeepStrictEqual6(parents3(resolved), resolvedParentIdentity))
43462
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43463
+ projectedHookAcl(cli.file, cli.stat);
43464
+ projectedHookAcl(manifest.file, manifest.stat);
43465
+ unchanged2(cli);
43466
+ unchanged2(manifest);
43467
+ } catch {
43468
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43469
+ }
43470
+ };
43471
+ recheck();
43472
+ return { resolved, sha256: cli.sha256, manifestSha256: manifest.sha256, recheck };
43473
+ } catch {
43474
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43475
+ }
43476
+ }
43477
+ function planClaudeManagedHookProjection(options) {
43478
+ if (typeof options.targetSettings !== "string" || Buffer.byteLength(options.targetSettings) > 1024 * 1024)
43479
+ hookProjectionRefusal("TARGET_INVALID");
43480
+ const targetSha256 = sha4(options.targetSettings);
43481
+ if (!/^[a-f0-9]{64}$/.test(options.expectedTargetSha256) || options.expectedTargetSha256 !== targetSha256)
43482
+ hookProjectionRefusal("TARGET_CHANGED");
43483
+ let target;
43484
+ try {
43485
+ target = jsonObject(options.targetSettings, "target");
43486
+ } catch {
43487
+ hookProjectionRefusal("TARGET_INVALID");
43488
+ }
43489
+ const home = resolve16(options.home ?? homedir3()), dataDir2 = options.dataDir ?? getDataDirReadOnly();
43490
+ const policyPath = join23(dataDir2, "agent-policy.json");
43491
+ let policyBytes, settingsBytes, settingsPath, command, profile;
43492
+ try {
43493
+ policyBytes = readNativeBytes(policyPath, 1024 * 1024);
43494
+ const snapshot2 = readManagedSkillPolicySnapshot(dataDir2);
43495
+ if (!snapshot2 || snapshot2.text !== policyBytes.toString("utf8"))
43496
+ hookProjectionRefusal("SOURCE_UNVERIFIED");
43497
+ const binding = snapshot2.value.bridge;
43498
+ settingsPath = canonicalAgentPath(join23(home, AGENT_ADAPTERS.claude.config), binding?.rootAliases ?? []);
43499
+ settingsBytes = readNativeBytes(settingsPath, 1024 * 1024);
43500
+ command = binding?.commands?.claude;
43501
+ profile = binding?.profiles?.claude;
43502
+ if (typeof command !== "string" || typeof profile !== "string")
43503
+ hookProjectionRefusal("SOURCE_UNVERIFIED");
43504
+ assertManagedAgentBridge("claude", { home, dataDir: dataDir2, projectDir: options.projectDir, profileId: profile });
43505
+ } catch {
43506
+ hookProjectionRefusal("SOURCE_UNVERIFIED");
43507
+ }
43508
+ const executable = projectedHookExecutable(command);
43509
+ const sourceSha256 = sha4(JSON.stringify({
43510
+ home,
43511
+ policyPath,
43512
+ policy: sha4(policyBytes),
43513
+ settingsPath,
43514
+ settings: sha4(settingsBytes),
43515
+ executable: { path: executable.resolved, sha256: executable.sha256, manifestSha256: executable.manifestSha256 }
43516
+ }));
43517
+ if (options.expectedSourceSha256 !== undefined && options.expectedSourceSha256 !== sourceSha256)
43518
+ hookProjectionRefusal("SOURCE_CHANGED");
43519
+ const replacements = [];
43520
+ const observed = [executable];
43521
+ if (target.hooks !== undefined) {
43522
+ if (!target.hooks || typeof target.hooks !== "object" || Array.isArray(target.hooks))
43523
+ hookProjectionRefusal("TARGET_INVALID");
43524
+ for (const event of [...AGENT_ADAPTERS.claude.events, "PreToolUse"]) {
43525
+ const groups = target.hooks[event];
43526
+ if (groups === undefined)
43527
+ continue;
43528
+ if (!Array.isArray(groups))
43529
+ hookProjectionRefusal("TARGET_INVALID");
43530
+ let recognized = 0;
43531
+ for (const [groupIndex, group] of groups.entries()) {
43532
+ if (!group || !Array.isArray(group.hooks))
43533
+ hookProjectionRefusal("TARGET_INVALID");
43534
+ for (const [hookIndex, hook] of group.hooks.entries()) {
43535
+ if (hook?.type !== "command" || typeof hook.command !== "string")
43536
+ continue;
43537
+ if (!hook.command.includes("hook user-prompt --agent claude"))
43538
+ continue;
43539
+ const match = /^('(?:[^']|'\\'')*') hook user-prompt --agent claude --selection-profile ([A-Za-z0-9][A-Za-z0-9._-]{0,127}) --event ([A-Za-z]+)$/.exec(hook.command);
43540
+ if (!match || match[3] !== event)
43541
+ hookProjectionRefusal("UNRECOGNIZED");
43542
+ const oldCommand = match[1].slice(1, -1).replaceAll("'\\''", "'");
43543
+ let rendered;
43544
+ try {
43545
+ rendered = renderAgentHookCommand(oldCommand, "claude", match[2], event);
43546
+ } catch {
43547
+ hookProjectionRefusal("UNRECOGNIZED");
43548
+ }
43549
+ if (rendered !== hook.command)
43550
+ hookProjectionRefusal("UNRECOGNIZED");
43551
+ const legacy = projectedHookExecutable(oldCommand, executable.resolved);
43552
+ if (legacy.resolved !== executable.resolved || legacy.sha256 !== executable.sha256 || legacy.manifestSha256 !== executable.manifestSha256)
43553
+ hookProjectionRefusal("EXECUTABLE_UNVERIFIED");
43554
+ observed.push(legacy);
43555
+ if (++recognized > 1)
43556
+ hookProjectionRefusal("AMBIGUOUS");
43557
+ const replacement = renderAgentHookCommand(command, "claude", profile, event);
43558
+ if (replacement !== hook.command)
43559
+ replacements.push({ event, groupIndex, hookIndex, beforeSha256: sha4(hook.command), command: replacement });
43560
+ }
43561
+ }
43562
+ }
43563
+ }
43564
+ try {
43565
+ if (!readNativeBytes(policyPath, 1024 * 1024).equals(policyBytes) || !readNativeBytes(settingsPath, 1024 * 1024).equals(settingsBytes))
43566
+ hookProjectionRefusal("SOURCE_CHANGED");
43567
+ } catch {
43568
+ hookProjectionRefusal("SOURCE_CHANGED");
43569
+ }
43570
+ for (const item of observed)
43571
+ item.recheck();
43572
+ return { schema: "hasna.claude-managed-hook-projection.v1", targetSha256, sourceSha256, replacements };
43573
+ }
43391
43574
  function planClaudeStopHookUpdate(options) {
43392
43575
  return planClaudeHookEventsUpdate({ ...options, events: ["Stop"] });
43393
43576
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.34",
3
+ "version": "0.10.35",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {