@hasna/skills 0.9.16 → 0.9.18

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.16",
36871
+ version: "0.9.18",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -39947,8 +39947,8 @@ function assertAgentPolicyCollections(policy) {
39947
39947
  text(source.path);
39948
39948
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
39949
39949
  if (source.hashMode !== undefined)
39950
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
39951
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
39950
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
39951
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
39952
39952
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute2(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "settings.json");
39953
39953
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
39954
39954
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute2(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
@@ -48622,7 +48622,8 @@ async function resolveSkillsConnection(env3 = process.env, options = {}) {
48622
48622
  }
48623
48623
  let completed;
48624
48624
  try {
48625
- completed = await completePointerCredential(SKILLS_APP, pointer, env3);
48625
+ const explicitVault = Boolean(snapshotEnv.HASNA_SECRETS_API_URL?.trim() && snapshotEnv.HASNA_SECRETS_API_KEY_OVERRIDE?.trim());
48626
+ completed = await completePointerCredential(SKILLS_APP, pointer, explicitVault ? snapshotEnv : env3);
48626
48627
  } catch (error) {
48627
48628
  const translated = asSkillsFleetCredentialError(error, pointer.pointerVaultKey);
48628
48629
  if (translated)
@@ -50545,7 +50546,7 @@ function canonical2(value) {
50545
50546
  return { kind: "array", items: value.items.map(canonical2) };
50546
50547
  return value;
50547
50548
  }
50548
- function inferencePreference(key, child) {
50549
+ function inferencePreference(key, child, version) {
50549
50550
  if (key === "skipDangerousModePermissionPrompt") {
50550
50551
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
50551
50552
  return false;
@@ -50558,7 +50559,7 @@ function inferencePreference(key, child) {
50558
50559
  return false;
50559
50560
  need2(child.kind === "object", "requires an object of model preferences");
50560
50561
  child.entries = child.entries.filter(([model, preferences]) => {
50561
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
50562
+ if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
50562
50563
  return true;
50563
50564
  const effort = preferences.entries[0][1];
50564
50565
  need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -50569,7 +50570,7 @@ function inferencePreference(key, child) {
50569
50570
  function settingsDigest(text2, version = 1) {
50570
50571
  const value = parse3(text2);
50571
50572
  value.entries = value.entries.filter(([key, child]) => {
50572
- if (version === 2 && inferencePreference(key, child))
50573
+ if (version !== 1 && inferencePreference(key, child, version))
50573
50574
  return false;
50574
50575
  if (key === "model") {
50575
50576
  need2(child.kind === "string", "contains an invalid model selection");
@@ -50607,6 +50608,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
50607
50608
  budget.remaining -= bytes;
50608
50609
  return settingsDigest(text2, 2);
50609
50610
  }
50611
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
50612
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
50613
+ }
50614
+ function hashClaudeSettingsReplacementV3(text2, budget) {
50615
+ need2(typeof text2 === "string", "requires settings text");
50616
+ const bytes = Buffer.byteLength(text2);
50617
+ need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
50618
+ budget.remaining -= bytes;
50619
+ return settingsDigest(text2, 3);
50620
+ }
50610
50621
  var CLAUDE_SETTINGS_WITNESS_LIMITS, sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, BOOLEAN_PREFERENCES, ENUM_PREFERENCES, BUILTIN_MODEL_SELECTIONS, PERSISTED_EFFORT_LEVELS;
50611
50622
  var init_claude_settings_witness = __esm(() => {
50612
50623
  CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
@@ -54886,13 +54897,13 @@ function read2(path, changes) {
54886
54897
  return readFileSync17(path, "utf8");
54887
54898
  }
54888
54899
  function projected(source, changes, budget = discoveryByteBudget()) {
54889
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
54900
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
54890
54901
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
54891
54902
  throw new Error("Claude settings witnesses require exact typed metadata");
54892
- const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
54903
+ const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
54893
54904
  if (current !== source.sha256)
54894
54905
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
54895
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
54906
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
54896
54907
  }
54897
54908
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
54898
54909
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -54954,7 +54965,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
54954
54965
  safe4(root2);
54955
54966
  }
54956
54967
  function assertMarketplaceBinding(binding, source) {
54957
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
54968
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
54958
54969
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
54959
54970
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
54960
54971
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -55085,7 +55096,7 @@ function resolveAgentDiscovery(options) {
55085
55096
  verifyAgentDiscovery(retained);
55086
55097
  if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
55087
55098
  unresolved("retained review is missing the full agent configuration source");
55088
- if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
55099
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
55089
55100
  unresolved("retained settings witness names another configuration source");
55090
55101
  if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual7(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
55091
55102
  unresolved("retained review is missing current runtime discovery coverage");
@@ -55101,7 +55112,7 @@ function resolveAgentDiscovery(options) {
55101
55112
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
55102
55113
  throw new Error("Explicit discovery reviews require full source-file hashes");
55103
55114
  verifyAgentDiscovery(supplied);
55104
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
55115
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
55105
55116
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
55106
55117
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
55107
55118
  throw new Error("Discovery review must include the agent configuration source");
@@ -79039,7 +79050,20 @@ class RemoteSkillsClient {
79039
79050
  const url2 = new URL(upload.uploadUrl);
79040
79051
  if (url2.username || url2.password || url2.hash || url2.protocol !== "https:" && !(url2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)))
79041
79052
  throw new Error("Unsafe input upload URL");
79042
- const uploaded = await fetch(url2, { method: "PUT", body: file2.bytes, headers: { "Content-Type": file2.contentType ?? "application/octet-stream" }, redirect: "error", signal: AbortSignal.timeout(60000) });
79053
+ const headers = new Headers({ "Content-Type": file2.contentType ?? "application/octet-stream" });
79054
+ if (upload.uploadHeaders !== undefined) {
79055
+ if (upload.uploadHeaders === null || typeof upload.uploadHeaders !== "object" || Array.isArray(upload.uploadHeaders))
79056
+ throw new Error("Invalid input upload headers");
79057
+ const seen = new Set;
79058
+ for (const [name, value] of Object.entries(upload.uploadHeaders)) {
79059
+ const lowerName = name.toLowerCase();
79060
+ if (seen.has(lowerName) || !["if-none-match", "x-amz-tagging"].includes(lowerName) || typeof value !== "string" || value.length === 0 || value.length > 2048 || !/^[\x20-\x7e]+$/.test(value) || value.trim() !== value || lowerName === "if-none-match" && value !== "*")
79061
+ throw new Error("Invalid input upload headers");
79062
+ seen.add(lowerName);
79063
+ headers.set(name, value);
79064
+ }
79065
+ }
79066
+ const uploaded = await fetch(url2, { method: "PUT", body: file2.bytes, headers, redirect: "error", signal: AbortSignal.timeout(60000) });
79043
79067
  if (!uploaded.ok)
79044
79068
  throw new Error("Input upload failed");
79045
79069
  await uploaded.body?.cancel();
@@ -80156,8 +80180,9 @@ Preserved original receipt: ${result2.archivePath}` : `Planned one session recon
80156
80180
  });
80157
80181
  }
80158
80182
  function reportContextError(error61, json2 = false) {
80159
- const code = error61 instanceof SkillSelectionError ? error61.code : "SKILLS_CONTEXT_FAILED";
80160
- const message = error61 instanceof SkillSelectionError ? error61.message : "Skills context could not be loaded. Check authentication and the selected profile.";
80183
+ const storageFull = error61 instanceof Error && ["ENOSPC", "EDQUOT"].includes(error61.code ?? "");
80184
+ const code = error61 instanceof SkillSelectionError ? error61.code : storageFull ? "SKILLS_STORAGE_FULL" : "SKILLS_CONTEXT_FAILED";
80185
+ const message = error61 instanceof SkillSelectionError ? error61.message : storageFull ? "Skills could not save verified local state because the filesystem is full or its quota is exhausted. Free space through your approved recoverable cleanup process, then retry the same session." : "Skills context could not be loaded. Check authentication and the selected profile.";
80161
80186
  if (json2)
80162
80187
  console.log(JSON.stringify({ error: { code, message } }));
80163
80188
  else
@@ -108616,7 +108641,7 @@ async function connectCodexHookRpc(options) {
108616
108641
  }
108617
108642
  var SUPPORTED_CODEX_HOOK_VERSIONS;
108618
108643
  var init_codex_hook_rpc = __esm(() => {
108619
- SUPPORTED_CODEX_HOOK_VERSIONS = ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1"];
108644
+ SUPPORTED_CODEX_HOOK_VERSIONS = ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1", "codex-cli 0.156.1"];
108620
108645
  });
108621
108646
 
108622
108647
  // src/lib/codex-hook-trust-identity.ts
@@ -109054,6 +109079,9 @@ function hookFailureReason(error61, profileId) {
109054
109079
  const profile = typeof profileId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profileId) && !profileId.includes("..") ? profileId : undefined;
109055
109080
  const identity4 = [profile ? `profile=${profile}` : undefined, owned ? `stage=${error61.stage}` : undefined].filter(Boolean).join(", ");
109056
109081
  const prefix = `Skills context is unavailable [${code2}]${identity4 ? ` (${identity4})` : ""}.`;
109082
+ if (code2 === "SKILLS_STORAGE_FULL") {
109083
+ return `${prefix} The filesystem is full or its quota is exhausted. Free space through your approved recoverable cleanup process, then retry the same session. Do not delete session receipts or change credentials to bypass this failure.`;
109084
+ }
109057
109085
  if (code2 === "PROFILE_LOCK_MISMATCH") {
109058
109086
  return `${prefix} This session or project is pinned to a different profile. Inspect the session with skills sessions show <session-id> --json; review skills sessions reconcile for an intentional session change, or explicit project sync for a project change. Sync alone does not change that pin.`;
109059
109087
  }
@@ -109070,6 +109098,7 @@ var CONTEXT_ERROR_CODES, HookDiagnosticError;
109070
109098
  var init_hook_diagnostics = __esm(() => {
109071
109099
  CONTEXT_ERROR_CODES = new Set([
109072
109100
  "SKILLS_CONTEXT_FAILED",
109101
+ "SKILLS_STORAGE_FULL",
109073
109102
  "PROFILE_LOCK_MISMATCH",
109074
109103
  "PROFILE_IDENTITY_MISMATCH",
109075
109104
  "PROFILE_APPLIED_REPORT_FAILED",
@@ -109185,10 +109214,10 @@ function agents(value) {
109185
109214
  }
109186
109215
  function registerAgentIntegration(parent) {
109187
109216
  const hook = parent.command("hook").description("Load selected Skills context through agent lifecycle hooks");
109188
- hook.command("witness").requiredOption("--kind <kind>", "claude-settings-v2 or claude-marketplace-registry-v2").requiredOption("--path <path>", "Canonical absolute path to the reviewed settings or registry file").option("--json", "Output the discovery witness as JSON", false).description("Capture an explicit versioned review witness without installing it").action(async (options) => {
109189
- const capture2 = options.kind === "claude-settings-v2" ? captureClaudeSettingsV2 : options.kind === "claude-marketplace-registry-v2" ? captureClaudeMarketplaceRegistryV2 : null;
109217
+ hook.command("witness").requiredOption("--kind <kind>", "claude-settings-v3, claude-settings-v2 or claude-marketplace-registry-v2").requiredOption("--path <path>", "Canonical absolute path to the reviewed settings or registry file").option("--json", "Output the discovery witness as JSON", false).description("Capture an explicit versioned review witness without installing it").action(async (options) => {
109218
+ const capture2 = options.kind === "claude-settings-v3" ? captureClaudeSettingsV3 : options.kind === "claude-settings-v2" ? captureClaudeSettingsV2 : options.kind === "claude-marketplace-registry-v2" ? captureClaudeMarketplaceRegistryV2 : null;
109190
109219
  if (!capture2)
109191
- throw new Error("Unsupported witness kind; select claude-settings-v2 or claude-marketplace-registry-v2");
109220
+ throw new Error("Unsupported witness kind; select claude-settings-v3, claude-settings-v2 or claude-marketplace-registry-v2");
109192
109221
  await writeCliOutput(JSON.stringify(capture2(options.path), null, 2));
109193
109222
  });
109194
109223
  hook.command("agents").option("--json", "Output the adapter capability inventory", false).description("Show maintained native adapters and explicit coverage limits").action(async () => {
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.16",
20022
+ version: "0.9.18",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -21311,7 +21311,8 @@ async function resolveSkillsConnection(env = process.env, options = {}) {
21311
21311
  }
21312
21312
  let completed;
21313
21313
  try {
21314
- completed = await completePointerCredential(SKILLS_APP, pointer, env);
21314
+ const explicitVault = Boolean(snapshotEnv.HASNA_SECRETS_API_URL?.trim() && snapshotEnv.HASNA_SECRETS_API_KEY_OVERRIDE?.trim());
21315
+ completed = await completePointerCredential(SKILLS_APP, pointer, explicitVault ? snapshotEnv : env);
21315
21316
  } catch (error61) {
21316
21317
  const translated = asSkillsFleetCredentialError(error61, pointer.pointerVaultKey);
21317
21318
  if (translated)
@@ -38126,8 +38127,8 @@ function assertAgentPolicyCollections(policy) {
38126
38127
  text(source.path);
38127
38128
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
38128
38129
  if (source.hashMode !== undefined)
38129
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
38130
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
38130
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
38131
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
38131
38132
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve2(source.path) === source.path && basename3(source.path) === "settings.json");
38132
38133
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
38133
38134
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve2(source.path) === source.path && basename3(source.path) === "known_marketplaces.json");
@@ -47080,7 +47081,7 @@ function canonical2(value) {
47080
47081
  return { kind: "array", items: value.items.map(canonical2) };
47081
47082
  return value;
47082
47083
  }
47083
- function inferencePreference(key, child) {
47084
+ function inferencePreference(key, child, version2) {
47084
47085
  if (key === "skipDangerousModePermissionPrompt") {
47085
47086
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
47086
47087
  return false;
@@ -47093,7 +47094,7 @@ function inferencePreference(key, child) {
47093
47094
  return false;
47094
47095
  need2(child.kind === "object", "requires an object of model preferences");
47095
47096
  child.entries = child.entries.filter(([model, preferences]) => {
47096
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
47097
+ if (version2 === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
47097
47098
  return true;
47098
47099
  const effort = preferences.entries[0][1];
47099
47100
  need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -47104,7 +47105,7 @@ function inferencePreference(key, child) {
47104
47105
  function settingsDigest(text2, version2 = 1) {
47105
47106
  const value = parse8(text2);
47106
47107
  value.entries = value.entries.filter(([key, child]) => {
47107
- if (version2 === 2 && inferencePreference(key, child))
47108
+ if (version2 !== 1 && inferencePreference(key, child, version2))
47108
47109
  return false;
47109
47110
  if (key === "model") {
47110
47111
  need2(child.kind === "string", "contains an invalid model selection");
@@ -47142,6 +47143,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
47142
47143
  budget.remaining -= bytes;
47143
47144
  return settingsDigest(text2, 2);
47144
47145
  }
47146
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
47147
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
47148
+ }
47149
+ function hashClaudeSettingsReplacementV3(text2, budget) {
47150
+ need2(typeof text2 === "string", "requires settings text");
47151
+ const bytes = Buffer.byteLength(text2);
47152
+ need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
47153
+ budget.remaining -= bytes;
47154
+ return settingsDigest(text2, 3);
47155
+ }
47145
47156
  var CLAUDE_SETTINGS_WITNESS_LIMITS, sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, BOOLEAN_PREFERENCES, ENUM_PREFERENCES, BUILTIN_MODEL_SELECTIONS, PERSISTED_EFFORT_LEVELS;
47146
47157
  var init_claude_settings_witness = __esm(() => {
47147
47158
  CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
@@ -48867,13 +48878,13 @@ function read(path, changes) {
48867
48878
  return readFileSync14(path, "utf8");
48868
48879
  }
48869
48880
  function projected(source, changes, budget = discoveryByteBudget()) {
48870
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
48881
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
48871
48882
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
48872
48883
  throw new Error("Claude settings witnesses require exact typed metadata");
48873
- const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
48884
+ const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
48874
48885
  if (current !== source.sha256)
48875
48886
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
48876
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
48887
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
48877
48888
  }
48878
48889
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
48879
48890
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -48935,7 +48946,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
48935
48946
  safe4(root);
48936
48947
  }
48937
48948
  function assertMarketplaceBinding(binding, source) {
48938
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename7(source.path) !== "settings.json"))
48949
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename7(source.path) !== "settings.json"))
48939
48950
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
48940
48951
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
48941
48952
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -49016,7 +49027,7 @@ function resolveAgentDiscovery(options) {
49016
49027
  verifyAgentDiscovery(retained);
49017
49028
  if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
49018
49029
  unresolved("retained review is missing the full agent configuration source");
49019
- if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
49030
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
49020
49031
  unresolved("retained settings witness names another configuration source");
49021
49032
  if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual2(saved, source))) || [...roots].some((root) => !retained.roots.includes(root)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
49022
49033
  unresolved("retained review is missing current runtime discovery coverage");
@@ -49032,7 +49043,7 @@ function resolveAgentDiscovery(options) {
49032
49043
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
49033
49044
  throw new Error("Explicit discovery reviews require full source-file hashes");
49034
49045
  verifyAgentDiscovery(supplied);
49035
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
49046
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
49036
49047
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
49037
49048
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
49038
49049
  throw new Error("Discovery review must include the agent configuration source");
@@ -52017,7 +52028,20 @@ class RemoteSkillsClient {
52017
52028
  const url2 = new URL(upload.uploadUrl);
52018
52029
  if (url2.username || url2.password || url2.hash || url2.protocol !== "https:" && !(url2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)))
52019
52030
  throw new Error("Unsafe input upload URL");
52020
- const uploaded = await fetch(url2, { method: "PUT", body: file2.bytes, headers: { "Content-Type": file2.contentType ?? "application/octet-stream" }, redirect: "error", signal: AbortSignal.timeout(60000) });
52031
+ const headers = new Headers({ "Content-Type": file2.contentType ?? "application/octet-stream" });
52032
+ if (upload.uploadHeaders !== undefined) {
52033
+ if (upload.uploadHeaders === null || typeof upload.uploadHeaders !== "object" || Array.isArray(upload.uploadHeaders))
52034
+ throw new Error("Invalid input upload headers");
52035
+ const seen = new Set;
52036
+ for (const [name, value] of Object.entries(upload.uploadHeaders)) {
52037
+ const lowerName = name.toLowerCase();
52038
+ if (seen.has(lowerName) || !["if-none-match", "x-amz-tagging"].includes(lowerName) || typeof value !== "string" || value.length === 0 || value.length > 2048 || !/^[\x20-\x7e]+$/.test(value) || value.trim() !== value || lowerName === "if-none-match" && value !== "*")
52039
+ throw new Error("Invalid input upload headers");
52040
+ seen.add(lowerName);
52041
+ headers.set(name, value);
52042
+ }
52043
+ }
52044
+ const uploaded = await fetch(url2, { method: "PUT", body: file2.bytes, headers, redirect: "error", signal: AbortSignal.timeout(60000) });
52021
52045
  if (!uploaded.ok)
52022
52046
  throw new Error("Input upload failed");
52023
52047
  await uploaded.body?.cancel();
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.16",
10
+ version: "0.9.18",
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.16",
24125
+ version: "0.9.18",
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.16",
24128
+ version: "0.9.18",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
package/dist/index.d.ts CHANGED
@@ -63,7 +63,7 @@ export { inspectSkillSession, reconcileSkillSession, type SessionReconciliationI
63
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
- export { captureClaudeSettings, captureClaudeSettingsV2 } from "./lib/claude-settings-witness.js";
66
+ export { captureClaudeSettings, captureClaudeSettingsV2, captureClaudeSettingsV3, upgradeClaudeSettingsWitness } from "./lib/claude-settings-witness.js";
67
67
  export { buildPluginProjection, pluginTreeDigest, type PluginProjectionManifest, type PluginPayloadMapping, type PluginFileWitness } from "./lib/plugin-projection.js";
68
68
  export { planPluginAdmission, admitPlugin, resolveAdmittedPlugin, pluginResolverCommand, type PluginAdmissionTarget, type PluginAdmissionBinding, type PluginAdmissionPrincipal, type PluginAdmissionPlan, type PluginAdmissionIdentity, type PluginBundleIdentity, type PluginRoutingIdentity, type PluginAdmissionReceipt } from "./lib/plugin-admission.js";
69
69
  export { captureManagedPluginRegistry, type ManagedPluginRegistrationWitness } from "./lib/plugin-discovery.js";
package/dist/index.js CHANGED
@@ -9055,8 +9055,8 @@ function assertAgentPolicyCollections(policy) {
9055
9055
  text(source.path);
9056
9056
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
9057
9057
  if (source.hashMode !== undefined)
9058
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
9059
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
9058
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
9059
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
9060
9060
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute2(source.path) && resolve2(source.path) === source.path && basename2(source.path) === "settings.json");
9061
9061
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
9062
9062
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute2(source.path) && resolve2(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
@@ -9611,7 +9611,7 @@ import { isAbsolute as isAbsolute11, join as join23, resolve as resolve11 } from
9611
9611
  // package.json
9612
9612
  var package_default = {
9613
9613
  name: "@hasna/skills",
9614
- version: "0.9.16",
9614
+ version: "0.9.18",
9615
9615
  description: "Skills library for AI coding agents",
9616
9616
  type: "module",
9617
9617
  bin: {
@@ -10891,7 +10891,8 @@ async function resolveSkillsConnection(env = process.env, options = {}) {
10891
10891
  }
10892
10892
  let completed;
10893
10893
  try {
10894
- completed = await completePointerCredential(SKILLS_APP, pointer, env);
10894
+ const explicitVault = Boolean(snapshotEnv.HASNA_SECRETS_API_URL?.trim() && snapshotEnv.HASNA_SECRETS_API_KEY_OVERRIDE?.trim());
10895
+ completed = await completePointerCredential(SKILLS_APP, pointer, explicitVault ? snapshotEnv : env);
10895
10896
  } catch (error) {
10896
10897
  const translated = asSkillsFleetCredentialError(error, pointer.pointerVaultKey);
10897
10898
  if (translated)
@@ -12874,7 +12875,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
12874
12875
  need2(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
12875
12876
  }
12876
12877
  var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
12877
- function inferencePreference(key, child) {
12878
+ function inferencePreference(key, child, version) {
12878
12879
  if (key === "skipDangerousModePermissionPrompt") {
12879
12880
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
12880
12881
  return false;
@@ -12887,7 +12888,7 @@ function inferencePreference(key, child) {
12887
12888
  return false;
12888
12889
  need2(child.kind === "object", "requires an object of model preferences");
12889
12890
  child.entries = child.entries.filter(([model, preferences]) => {
12890
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
12891
+ if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
12891
12892
  return true;
12892
12893
  const effort = preferences.entries[0][1];
12893
12894
  need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -12898,7 +12899,7 @@ function inferencePreference(key, child) {
12898
12899
  function settingsDigest(text2, version = 1) {
12899
12900
  const value = parse3(text2);
12900
12901
  value.entries = value.entries.filter(([key, child]) => {
12901
- if (version === 2 && inferencePreference(key, child))
12902
+ if (version !== 1 && inferencePreference(key, child, version))
12902
12903
  return false;
12903
12904
  if (key === "model") {
12904
12905
  need2(child.kind === "string", "contains an invalid model selection");
@@ -12936,6 +12937,25 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
12936
12937
  budget.remaining -= bytes;
12937
12938
  return settingsDigest(text2, 2);
12938
12939
  }
12940
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
12941
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
12942
+ }
12943
+ function hashClaudeSettingsReplacementV3(text2, budget) {
12944
+ need2(typeof text2 === "string", "requires settings text");
12945
+ const bytes = Buffer.byteLength(text2);
12946
+ need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
12947
+ budget.remaining -= bytes;
12948
+ return settingsDigest(text2, 3);
12949
+ }
12950
+ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
12951
+ need2(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact legacy settings witness");
12952
+ const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
12953
+ const before = readSettings(reviewedSettingsPath, budget), current = readSettings(previous.path, budget);
12954
+ need2(settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : 2) === previous.sha256, "reviewed preimage does not match the legacy witness");
12955
+ const sha2562 = settingsDigest(current, 3);
12956
+ need2(settingsDigest(before, 3) === sha2562, "non-preference settings changed; explicit discovery review required");
12957
+ return { path: previous.path, hashMode: "claude-settings-v3", sha256: sha2562 };
12958
+ }
12939
12959
 
12940
12960
  // src/lib/agent-integration.ts
12941
12961
  var sha = (data) => createHash10("sha256").update(data).digest("hex");
@@ -16507,13 +16527,13 @@ function read2(path, changes) {
16507
16527
  return readFileSync16(path, "utf8");
16508
16528
  }
16509
16529
  function projected(source, changes, budget = discoveryByteBudget()) {
16510
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
16530
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
16511
16531
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
16512
16532
  throw new Error("Claude settings witnesses require exact typed metadata");
16513
- const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
16533
+ const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
16514
16534
  if (current !== source.sha256)
16515
16535
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
16516
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
16536
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
16517
16537
  }
16518
16538
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
16519
16539
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -16575,7 +16595,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
16575
16595
  safe4(root2);
16576
16596
  }
16577
16597
  function assertMarketplaceBinding(binding, source) {
16578
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
16598
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
16579
16599
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
16580
16600
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
16581
16601
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -16718,7 +16738,7 @@ function resolveAgentDiscovery(options) {
16718
16738
  verifyAgentDiscovery(retained);
16719
16739
  if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
16720
16740
  unresolved("retained review is missing the full agent configuration source");
16721
- if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
16741
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
16722
16742
  unresolved("retained settings witness names another configuration source");
16723
16743
  if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual3(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
16724
16744
  unresolved("retained review is missing current runtime discovery coverage");
@@ -16734,7 +16754,7 @@ function resolveAgentDiscovery(options) {
16734
16754
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
16735
16755
  throw new Error("Explicit discovery reviews require full source-file hashes");
16736
16756
  verifyAgentDiscovery(supplied);
16737
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
16757
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
16738
16758
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
16739
16759
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
16740
16760
  throw new Error("Discovery review must include the agent configuration source");
@@ -39076,7 +39096,20 @@ class RemoteSkillsClient {
39076
39096
  const url2 = new URL(upload.uploadUrl);
39077
39097
  if (url2.username || url2.password || url2.hash || url2.protocol !== "https:" && !(url2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)))
39078
39098
  throw new Error("Unsafe input upload URL");
39079
- const uploaded = await fetch(url2, { method: "PUT", body: file2.bytes, headers: { "Content-Type": file2.contentType ?? "application/octet-stream" }, redirect: "error", signal: AbortSignal.timeout(60000) });
39099
+ const headers = new Headers({ "Content-Type": file2.contentType ?? "application/octet-stream" });
39100
+ if (upload.uploadHeaders !== undefined) {
39101
+ if (upload.uploadHeaders === null || typeof upload.uploadHeaders !== "object" || Array.isArray(upload.uploadHeaders))
39102
+ throw new Error("Invalid input upload headers");
39103
+ const seen = new Set;
39104
+ for (const [name, value] of Object.entries(upload.uploadHeaders)) {
39105
+ const lowerName = name.toLowerCase();
39106
+ if (seen.has(lowerName) || !["if-none-match", "x-amz-tagging"].includes(lowerName) || typeof value !== "string" || value.length === 0 || value.length > 2048 || !/^[\x20-\x7e]+$/.test(value) || value.trim() !== value || lowerName === "if-none-match" && value !== "*")
39107
+ throw new Error("Invalid input upload headers");
39108
+ seen.add(lowerName);
39109
+ headers.set(name, value);
39110
+ }
39111
+ }
39112
+ const uploaded = await fetch(url2, { method: "PUT", body: file2.bytes, headers, redirect: "error", signal: AbortSignal.timeout(60000) });
39080
39113
  if (!uploaded.ok)
39081
39114
  throw new Error("Input upload failed");
39082
39115
  await uploaded.body?.cancel();
@@ -43682,6 +43715,7 @@ export {
43682
43715
  validateCron,
43683
43716
  validateBlogArticleRunOptions,
43684
43717
  uploadSkillsSnapshotFilesToS3,
43718
+ upgradeClaudeSettingsWitness,
43685
43719
  updateSkillRun,
43686
43720
  unsetConfig,
43687
43721
  unpinSkill,
@@ -43860,6 +43894,7 @@ export {
43860
43894
  captureDiscoveryPathSources,
43861
43895
  captureDiscoveryDirectories,
43862
43896
  captureDiscoveryByteSources,
43897
+ captureClaudeSettingsV3,
43863
43898
  captureClaudeSettingsV2,
43864
43899
  captureClaudeSettings,
43865
43900
  captureClaudeMarketplaceRegistryV2,
@@ -6,7 +6,7 @@ export { captureDiscoveryDirectories, type DiscoveryDirectory } from "./agent-di
6
6
  export interface DiscoverySource {
7
7
  path: string;
8
8
  sha256: string | null;
9
- hashMode?: "bytes" | "path-bytes" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-marketplace-registry-v2";
9
+ hashMode?: "bytes" | "path-bytes" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-settings-v3" | "claude-marketplace-registry-v2";
10
10
  managedPlugins?: ManagedPluginRegistrationWitness[];
11
11
  format?: "json" | "toml" | "yaml";
12
12
  fields?: string[];
@@ -34,3 +34,19 @@ export declare function captureClaudeSettingsV2(path: string, budget?: ClaudeSet
34
34
  sha256: string;
35
35
  };
36
36
  export declare function hashClaudeSettingsReplacementV2(text: string, budget: ClaudeSettingsWitnessBudget): string;
37
+ /** Explicit review of model-independent effort preferences; v1/v2 stay exact. */
38
+ export declare function captureClaudeSettingsV3(path: string, budget?: ClaudeSettingsWitnessBudget): {
39
+ path: string;
40
+ hashMode: "claude-settings-v3";
41
+ sha256: string;
42
+ };
43
+ export declare function hashClaudeSettingsReplacementV3(text: string, budget: ClaudeSettingsWitnessBudget): string;
44
+ /** Upgrade only with an exact semantic preimage of the prior review. This
45
+ * proves every non-preference setting is unchanged; it never refreshes drift.
46
+ * Both files are bounded regular settings.json files, read without symlinks.
47
+ */
48
+ export declare function upgradeClaudeSettingsWitness(previous: {
49
+ path: string;
50
+ hashMode: "claude-settings-v1" | "claude-settings-v2";
51
+ sha256: string;
52
+ }, reviewedSettingsPath: string): ReturnType<typeof captureClaudeSettingsV3>;
@@ -1,4 +1,4 @@
1
- export declare const SUPPORTED_CODEX_HOOK_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1"];
1
+ export declare const SUPPORTED_CODEX_HOOK_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1", "codex-cli 0.156.1"];
2
2
  export interface CodexHookRpc {
3
3
  version: string;
4
4
  processId?: number;
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.16",
32376
+ version: "0.9.18",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -33712,7 +33712,8 @@ async function resolveSkillsConnection(env = process.env, options = {}) {
33712
33712
  }
33713
33713
  let completed;
33714
33714
  try {
33715
- completed = await completePointerCredential(SKILLS_APP, pointer, env);
33715
+ const explicitVault = Boolean(snapshotEnv.HASNA_SECRETS_API_URL?.trim() && snapshotEnv.HASNA_SECRETS_API_KEY_OVERRIDE?.trim());
33716
+ completed = await completePointerCredential(SKILLS_APP, pointer, explicitVault ? snapshotEnv : env);
33716
33717
  } catch (error) {
33717
33718
  const translated = asSkillsFleetCredentialError(error, pointer.pointerVaultKey);
33718
33719
  if (translated)
@@ -37545,8 +37546,8 @@ function assertAgentPolicyCollections(policy) {
37545
37546
  text(source.path);
37546
37547
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
37547
37548
  if (source.hashMode !== undefined)
37548
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
37549
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
37549
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
37550
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
37550
37551
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "settings.json");
37551
37552
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
37552
37553
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
@@ -40154,7 +40155,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
40154
40155
  need3(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
40155
40156
  }
40156
40157
  var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
40157
- function inferencePreference(key, child) {
40158
+ function inferencePreference(key, child, version) {
40158
40159
  if (key === "skipDangerousModePermissionPrompt") {
40159
40160
  need3(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
40160
40161
  return false;
@@ -40167,7 +40168,7 @@ function inferencePreference(key, child) {
40167
40168
  return false;
40168
40169
  need3(child.kind === "object", "requires an object of model preferences");
40169
40170
  child.entries = child.entries.filter(([model, preferences]) => {
40170
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
40171
+ if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
40171
40172
  return true;
40172
40173
  const effort = preferences.entries[0][1];
40173
40174
  need3(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -40178,7 +40179,7 @@ function inferencePreference(key, child) {
40178
40179
  function settingsDigest(text2, version = 1) {
40179
40180
  const value = parse6(text2);
40180
40181
  value.entries = value.entries.filter(([key, child]) => {
40181
- if (version === 2 && inferencePreference(key, child))
40182
+ if (version !== 1 && inferencePreference(key, child, version))
40182
40183
  return false;
40183
40184
  if (key === "model") {
40184
40185
  need3(child.kind === "string", "contains an invalid model selection");
@@ -40216,6 +40217,25 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
40216
40217
  budget.remaining -= bytes;
40217
40218
  return settingsDigest(text2, 2);
40218
40219
  }
40220
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
40221
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
40222
+ }
40223
+ function hashClaudeSettingsReplacementV3(text2, budget) {
40224
+ need3(typeof text2 === "string", "requires settings text");
40225
+ const bytes = Buffer.byteLength(text2);
40226
+ need3(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
40227
+ budget.remaining -= bytes;
40228
+ return settingsDigest(text2, 3);
40229
+ }
40230
+ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
40231
+ need3(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact legacy settings witness");
40232
+ const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
40233
+ const before = readSettings(reviewedSettingsPath, budget), current = readSettings(previous.path, budget);
40234
+ need3(settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : 2) === previous.sha256, "reviewed preimage does not match the legacy witness");
40235
+ const sha2562 = settingsDigest(current, 3);
40236
+ need3(settingsDigest(before, 3) === sha2562, "non-preference settings changed; explicit discovery review required");
40237
+ return { path: previous.path, hashMode: "claude-settings-v3", sha256: sha2562 };
40238
+ }
40219
40239
 
40220
40240
  // src/lib/codex-hook-discovery-recovery.ts
40221
40241
  import { createHash as createHash14 } from "crypto";
@@ -40325,13 +40345,13 @@ function read2(path, changes) {
40325
40345
  return readFileSync12(path, "utf8");
40326
40346
  }
40327
40347
  function projected(source, changes, budget = discoveryByteBudget()) {
40328
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
40348
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
40329
40349
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
40330
40350
  throw new Error("Claude settings witnesses require exact typed metadata");
40331
- const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
40351
+ const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
40332
40352
  if (current !== source.sha256)
40333
40353
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
40334
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
40354
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
40335
40355
  }
40336
40356
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
40337
40357
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -40393,7 +40413,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
40393
40413
  safe4(root2);
40394
40414
  }
40395
40415
  function assertMarketplaceBinding(binding, source) {
40396
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
40416
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
40397
40417
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
40398
40418
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
40399
40419
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -40536,7 +40556,7 @@ function resolveAgentDiscovery(options) {
40536
40556
  verifyAgentDiscovery(retained);
40537
40557
  if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
40538
40558
  unresolved("retained review is missing the full agent configuration source");
40539
- if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
40559
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
40540
40560
  unresolved("retained settings witness names another configuration source");
40541
40561
  if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual3(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
40542
40562
  unresolved("retained review is missing current runtime discovery coverage");
@@ -40552,7 +40572,7 @@ function resolveAgentDiscovery(options) {
40552
40572
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
40553
40573
  throw new Error("Explicit discovery reviews require full source-file hashes");
40554
40574
  verifyAgentDiscovery(supplied);
40555
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
40575
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
40556
40576
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
40557
40577
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
40558
40578
  throw new Error("Discovery review must include the agent configuration source");
@@ -89646,7 +89666,20 @@ class RemoteSkillsClient {
89646
89666
  const url2 = new URL(upload.uploadUrl);
89647
89667
  if (url2.username || url2.password || url2.hash || url2.protocol !== "https:" && !(url2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)))
89648
89668
  throw new Error("Unsafe input upload URL");
89649
- const uploaded = await fetch(url2, { method: "PUT", body: file2.bytes, headers: { "Content-Type": file2.contentType ?? "application/octet-stream" }, redirect: "error", signal: AbortSignal.timeout(60000) });
89669
+ const headers = new Headers({ "Content-Type": file2.contentType ?? "application/octet-stream" });
89670
+ if (upload.uploadHeaders !== undefined) {
89671
+ if (upload.uploadHeaders === null || typeof upload.uploadHeaders !== "object" || Array.isArray(upload.uploadHeaders))
89672
+ throw new Error("Invalid input upload headers");
89673
+ const seen = new Set;
89674
+ for (const [name, value] of Object.entries(upload.uploadHeaders)) {
89675
+ const lowerName = name.toLowerCase();
89676
+ if (seen.has(lowerName) || !["if-none-match", "x-amz-tagging"].includes(lowerName) || typeof value !== "string" || value.length === 0 || value.length > 2048 || !/^[\x20-\x7e]+$/.test(value) || value.trim() !== value || lowerName === "if-none-match" && value !== "*")
89677
+ throw new Error("Invalid input upload headers");
89678
+ seen.add(lowerName);
89679
+ headers.set(name, value);
89680
+ }
89681
+ }
89682
+ const uploaded = await fetch(url2, { method: "PUT", body: file2.bytes, headers, redirect: "error", signal: AbortSignal.timeout(60000) });
89650
89683
  if (!uploaded.ok)
89651
89684
  throw new Error("Input upload failed");
89652
89685
  await uploaded.body?.cancel();
@@ -289,6 +289,24 @@ Primary runtime references: [command sources](https://code.claude.com/docs/en/pl
289
289
 
290
290
  ### Explicit v2 preference reviews
291
291
 
292
+ For new reviews, use `captureClaudeSettingsV3(path)` or
293
+ `skills hook witness --kind claude-settings-v3 --path <settings.json> --json`.
294
+ The v3 witness recognizes an entry containing only a valid `effortLevel`
295
+ independently of its model name. Claude can persist a preference for a newly
296
+ released model without blocking the next prompt. Entries with additional
297
+ properties, including `maxEffortLevel`, remain fully bound, as do provider
298
+ mappings, hooks, permissions, plugins, environment and unknown settings.
299
+ The top-level model selection rules remain unchanged.
300
+
301
+ Legacy v1 and v2 digests retain their meaning. `upgradeClaudeSettingsWitness`
302
+ accepts a legacy witness and a saved `settings.json` preimage. It verifies that
303
+ the preimage matches the old digest and that current settings differ only in
304
+ recognized preferences before returning a v3 witness. It refuses all other
305
+ drift. Use that witness in reviewed discovery inputs and apply the normal
306
+ guarded integration plan. Install compatible runtimes before activating v3;
307
+ when updating agents, verify their actual hook commands and native trust, not
308
+ only the executable found on `PATH`.
309
+
292
310
  `captureClaudeSettingsV2(path)` creates a `claude-settings-v2` witness. In
293
311
  addition to the v1 display/model selections, it tolerates the documented
294
312
  persisted effort values `low`, `medium`, `high`, and `xhigh`, both at the top
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.9.16",
3
+ "version": "0.9.18",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {