@hasna/skills 0.10.49 → 0.10.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -559,7 +559,8 @@ The plan proves the preserved original matches every witness being replaced,
559
559
  permits only the documented non-discovery differences, and labels the replacement
560
560
  witness (the JSON receipt lists them under `replacedWitnesses`). `--apply`
561
561
  preserves and reads back the original policy before writing, and never writes the
562
- native configuration. Claude upgrades to `claude-settings-v3`; Codex uses
562
+ native configuration. Claude upgrades to `claude-settings-v3`, or to
563
+ `claude-settings-v4` with `--claude-witness-version 4`; Codex uses
563
564
  `codex-settings-v2`, which also replaces the narrower automatic
564
565
  `plugins`/`marketplaces`/`skills` projection of the same file once the preserved
565
566
  original is proved to explain it. That projection is order-sensitive, so it keeps
@@ -1304,6 +1305,21 @@ the backup keeps the exact previous launcher (symlink text or pinned bytes), and
1304
1305
  projection and `skills self-update` read a managed pinned launcher as the exact
1305
1306
  entry it runs and bind the launcher's own bytes as well.
1306
1307
 
1308
+ An updater from before pinned launchers (0.10.48 and earlier) writes bare symlinks,
1309
+ including when it installs a version that has them, and an exact-version update to
1310
+ the version already installed is refused. `skills self-update --adopt-aliases` pins
1311
+ those launchers in place: in `~/.local/bin`, `~/.bun/bin`, `/opt/homebrew/bin` and
1312
+ `/usr/local/bin`, a bare symlink that already reaches the current runtime's entry is
1313
+ adopted like an alias still pointing at an older install. The same refusals apply
1314
+ (an unsafe or unwritable directory, a launcher owned by another account, a foreign
1315
+ file, an unsupported link chain), the exact symlink is kept as
1316
+ `<launcher>.skills-alias-prev-<receipt id>`, the receipt records it as the old
1317
+ `symlink` shape, and `--rollback-aliases <receipt id>` restores it exactly. A
1318
+ launcher already pinned to the current runtime is left unchanged. Undo in reverse
1319
+ order: roll back the adoption before rolling back the runtime update that wrote
1320
+ those symlinks, because that update's `--rollback` refuses with
1321
+ `LAUNCHER_DRIFT_ROLLBACK_REFUSED` while the adopted pinned launchers are in place.
1322
+
1307
1323
  To require a minimum dependency age, pass a positive integer number of days and
1308
1324
  repeat the exclusion option for package names or package globs:
1309
1325
 
@@ -2568,3 +2584,18 @@ Upgrade an existing review with `skills hook rebind-settings --agent codex
2568
2584
  Review the plan before adding `--apply`; the original review preimage must match
2569
2585
  its original mode, and unrelated changes refuse. This preserves the native
2570
2586
  configuration, root aliases, other witnesses and active session pins.
2587
+
2588
+ `claude-settings-v4` is an explicit successor to `claude-settings-v3`. It
2589
+ additionally ignores only a top-level `theme` whose value is exactly one of the
2590
+ documented built-in values (`auto`, `dark`, `light`, `dark-daltonized`,
2591
+ `light-daltonized`, `dark-ansi`, `light-ansi`). Custom and plugin themes, other
2592
+ values or types, nested `theme` keys, `skipDangerousModePermissionPrompt` and
2593
+ every permission, hook, environment, helper and unknown setting stay bound.
2594
+ Existing raw/V1/V2/V3 reviews retain their original meaning.
2595
+
2596
+ Capture with `skills hook witness --kind claude-settings-v4 --path <settings.json>`.
2597
+ Upgrade an existing review with `skills hook rebind-settings --agent claude
2598
+ --claude-witness-version 4 --reviewed-preimage <preserved-settings.json>
2599
+ --expected-policy-sha256 <policy-hash> --expected-settings-sha256 <settings-hash>`.
2600
+ Review the plan before adding `--apply`. See
2601
+ [built-in theme reviews](docs/plugin-admission.md#explicit-v4-built-in-theme-reviews).
package/bin/index.js CHANGED
@@ -36888,7 +36888,7 @@ var package_default;
36888
36888
  var init_package = __esm(() => {
36889
36889
  package_default = {
36890
36890
  name: "@hasna/skills",
36891
- version: "0.10.49",
36891
+ version: "0.10.50",
36892
36892
  description: "Skills library for AI coding agents",
36893
36893
  type: "module",
36894
36894
  bin: {
@@ -40066,7 +40066,9 @@ function inferencePreference(key, child, version) {
40066
40066
  function settingsDigest(text, version = 1) {
40067
40067
  const value = parse(text);
40068
40068
  value.entries = value.entries.filter(([key, child]) => {
40069
- if (version !== 1 && inferencePreference(key, child, version))
40069
+ if (version === 4 && key === "theme" && child.kind === "string" && BUILTIN_THEMES.has(child.value))
40070
+ return false;
40071
+ if (version !== 1 && inferencePreference(key, child, version === 4 ? 3 : version))
40070
40072
  return false;
40071
40073
  if (key === "model") {
40072
40074
  need(child.kind === "string", "contains an invalid model selection");
@@ -40124,6 +40126,26 @@ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
40124
40126
  need(settingsDigest(before2, 3) === sha256, "non-preference settings changed; explicit discovery review required");
40125
40127
  return { path: previous.path, hashMode: "claude-settings-v3", sha256 };
40126
40128
  }
40129
+ function captureClaudeSettingsV4(path, budget = { remaining: 256 * 1024 * 1024 }) {
40130
+ return { path, hashMode: "claude-settings-v4", sha256: settingsDigest(readNativeSettingsWitnessFile(path, budget), 4) };
40131
+ }
40132
+ function hashClaudeSettingsReplacementV4(text, budget) {
40133
+ need(typeof text === "string", "requires settings text");
40134
+ const bytes = Buffer.byteLength(text);
40135
+ need(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
40136
+ budget.remaining -= bytes;
40137
+ return settingsDigest(text, 4);
40138
+ }
40139
+ function upgradeClaudeSettingsWitnessV4(previous, reviewedSettingsPath) {
40140
+ need(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === undefined || previous.hashMode === "bytes" || previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2" || previous.hashMode === "claude-settings-v3") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact prior settings witness");
40141
+ const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
40142
+ const before2 = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
40143
+ const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash2("sha256").update(before2).digest("hex") : settingsDigest(before2, previous.hashMode === "claude-settings-v1" ? 1 : previous.hashMode === "claude-settings-v2" ? 2 : 3);
40144
+ need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
40145
+ const sha256 = settingsDigest(current, 4);
40146
+ need(settingsDigest(before2, 4) === sha256, "non-preference settings changed; explicit discovery review required");
40147
+ return { path: previous.path, hashMode: "claude-settings-v4", sha256 };
40148
+ }
40127
40149
  function hashNativeJsonControls(text, excludedMetadata) {
40128
40150
  need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
40129
40151
  const value = parse(text);
@@ -40156,7 +40178,7 @@ function hashSumiNativeJsonControls(text) {
40156
40178
  });
40157
40179
  return createHash2("sha256").update("hasna.skills.sumi-settings.v1\x00").update(JSON.stringify(value)).digest("hex");
40158
40180
  }
40159
- 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;
40181
+ 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, CLAUDE_BUILTIN_THEMES, BUILTIN_THEMES, PERSISTED_EFFORT_LEVELS;
40160
40182
  var init_claude_settings_witness = __esm(() => {
40161
40183
  CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
40162
40184
  BOOLEAN_PREFERENCES = new Set([
@@ -40210,6 +40232,8 @@ var init_claude_settings_witness = __esm(() => {
40210
40232
  "claude-opus-4-6[1m]",
40211
40233
  "claude-sonnet-4-6[1m]"
40212
40234
  ]);
40235
+ CLAUDE_BUILTIN_THEMES = Object.freeze(["auto", "dark", "light", "dark-daltonized", "light-daltonized", "dark-ansi", "light-ansi"]);
40236
+ BUILTIN_THEMES = new Set(CLAUDE_BUILTIN_THEMES);
40213
40237
  PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
40214
40238
  });
40215
40239
 
@@ -40387,14 +40411,14 @@ function assertAgentPolicyCollections(policy) {
40387
40411
  text(source.path);
40388
40412
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
40389
40413
  if (source.hashMode !== undefined)
40390
- requireBound(["bytes", "path-bytes", "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "sumi-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
40414
+ requireBound(["bytes", "path-bytes", "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "sumi-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
40391
40415
  if (source.hashMode === "claude-plugin-manifest-v1")
40392
40416
  requireBound(agent === "claude" && value.agent === "claude" && ["automatic", "reviewed"].includes(value.method) && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve4(source.path) === source.path && source.path.endsWith(join11(sep2, ".claude-plugin", "plugin.json")) && source.managedPlugins === undefined);
40393
40417
  if (source.hashMode === "sumi-settings-v1")
40394
40418
  requireBound(agent === "sumi" && value.agent === "sumi" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve4(source.path) === source.path && basename3(source.path) === "sumi.json");
40395
40419
  if (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4"))
40396
40420
  requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve4(source.path) === source.path && basename3(source.path) === "config.toml");
40397
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
40421
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4"))
40398
40422
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve4(source.path) === source.path && basename3(source.path) === "settings.json");
40399
40423
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
40400
40424
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve4(source.path) === source.path && basename3(source.path) === "known_marketplaces.json");
@@ -55262,6 +55286,8 @@ function atomicWrite2(path, content) {
55262
55286
  function planAgentSettingsWitnessUpgrade(options) {
55263
55287
  if (options.targetCodexVersion !== undefined && (options.agent !== "codex" || ![2, 3, 4].includes(options.targetCodexVersion)))
55264
55288
  throw new Error("Invalid Codex witness target version");
55289
+ if (options.targetClaudeVersion !== undefined && (options.agent !== "claude" || ![3, 4].includes(options.targetClaudeVersion)))
55290
+ throw new Error("Invalid Claude witness target version");
55265
55291
  const home = resolve14(options.home ?? homedir3()), dataDir3 = options.dataDir ?? getDataDirReadOnly();
55266
55292
  const snapshot2 = readManagedSkillPolicySnapshot(dataDir3);
55267
55293
  if (!snapshot2 || sha3(snapshot2.text) !== options.expectedPolicySha256)
@@ -55309,7 +55335,7 @@ function planAgentSettingsWitnessUpgrade(options) {
55309
55335
  replaced.push(source);
55310
55336
  }
55311
55337
  }
55312
- const next = options.agent === "sumi" ? upgradeSumiSettingsWitness(previous, options.reviewedPreimage) : options.agent === "claude" ? upgradeClaudeSettingsWitness(previous, options.reviewedPreimage) : options.targetCodexVersion === 4 ? upgradeCodexSettingsWitnessV4(previous, options.reviewedPreimage) : options.targetCodexVersion === 3 ? upgradeCodexSettingsWitnessV3(previous, options.reviewedPreimage) : upgradeCodexSettingsWitness(previous, options.reviewedPreimage);
55338
+ const next = options.agent === "sumi" ? upgradeSumiSettingsWitness(previous, options.reviewedPreimage) : options.agent === "claude" ? options.targetClaudeVersion === 4 ? upgradeClaudeSettingsWitnessV4(previous, options.reviewedPreimage) : upgradeClaudeSettingsWitness(previous, options.reviewedPreimage) : options.targetCodexVersion === 4 ? upgradeCodexSettingsWitnessV4(previous, options.reviewedPreimage) : options.targetCodexVersion === 3 ? upgradeCodexSettingsWitnessV3(previous, options.reviewedPreimage) : upgradeCodexSettingsWitness(previous, options.reviewedPreimage);
55313
55339
  const rebound = [];
55314
55340
  for (const source of binding.sources) {
55315
55341
  if (!replaced.includes(source)) {
@@ -55355,7 +55381,7 @@ function applyAgentIntegrationUnlocked(plan, options = {}, assertCurrent) {
55355
55381
  const upgrade = plan.settingsWitnessUpgrade;
55356
55382
  if (!plan.managedAgentChecks || !plan.observedPolicy?.before)
55357
55383
  throw new Error("Invalid settings witness upgrade plan");
55358
- const verified = planAgentSettingsWitnessUpgrade({ agent: upgrade.agent, ...upgrade.agent === "codex" ? { targetCodexVersion: upgrade.toHashMode === "codex-settings-v4" ? 4 : upgrade.toHashMode === "codex-settings-v3" ? 3 : 2 } : {}, reviewedPreimage: upgrade.reviewedPreimage, home: plan.managedAgentChecks.home, dataDir: plan.dataDir, expectedPolicySha256: sha3(plan.observedPolicy.before), expectedSettingsSha256: upgrade.currentSettingsSha256 });
55384
+ const verified = planAgentSettingsWitnessUpgrade({ agent: upgrade.agent, ...upgrade.agent === "codex" ? { targetCodexVersion: upgrade.toHashMode === "codex-settings-v4" ? 4 : upgrade.toHashMode === "codex-settings-v3" ? 3 : 2 } : {}, ...upgrade.agent === "claude" && upgrade.toHashMode === "claude-settings-v4" ? { targetClaudeVersion: 4 } : {}, reviewedPreimage: upgrade.reviewedPreimage, home: plan.managedAgentChecks.home, dataDir: plan.dataDir, expectedPolicySha256: sha3(plan.observedPolicy.before), expectedSettingsSha256: upgrade.currentSettingsSha256 });
55359
55385
  if (JSON.stringify(verified.changes) !== JSON.stringify(plan.changes) || JSON.stringify(verified.settingsWitnessUpgrade) !== JSON.stringify(upgrade))
55360
55386
  throw new Error("Settings witness upgrade plan changed");
55361
55387
  provenDiscovery = verified.discoveryBefore[0];
@@ -59082,13 +59108,13 @@ function projected(source, changes, budget2 = discoveryByteBudget()) {
59082
59108
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
59083
59109
  return changes?.has(source.path) ? (source.hashMode === "codex-settings-v4" ? hashCodexSettingsReplacementV4 : source.hashMode === "codex-settings-v3" ? hashCodexSettingsReplacementV3 : source.hashMode === "codex-settings-v2" ? hashCodexSettingsReplacementV2 : hashCodexSettingsReplacement)(changes.get(source.path), budget2) : current;
59084
59110
  }
59085
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
59111
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4")) {
59086
59112
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
59087
59113
  throw new Error("Claude settings witnesses require exact typed metadata");
59088
- const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget2).sha256;
59114
+ const current = (source.hashMode === "claude-settings-v4" ? captureClaudeSettingsV4 : source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget2).sha256;
59089
59115
  if (current !== source.sha256)
59090
59116
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
59091
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget2) : current;
59117
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v4" ? hashClaudeSettingsReplacementV4 : source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget2) : current;
59092
59118
  }
59093
59119
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
59094
59120
  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))
@@ -59323,7 +59349,7 @@ function assertMarketplaceBinding(binding, source) {
59323
59349
  throw new Error("Sumi settings witnesses require explicit reviewed Sumi configuration");
59324
59350
  if ((source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")) && (binding.agent !== "codex" || binding.method !== "reviewed" || basename10(source.path) !== "config.toml"))
59325
59351
  throw new Error("Codex settings witnesses require explicit reviewed Codex configuration");
59326
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename10(source.path) !== "settings.json"))
59352
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4")) && (binding.agent !== "claude" || binding.method !== "reviewed" || basename10(source.path) !== "settings.json"))
59327
59353
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
59328
59354
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
59329
59355
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -59513,7 +59539,7 @@ function resolveAgentDiscovery(options) {
59513
59539
  verifyAgentDiscovery(retained);
59514
59540
  if (!retained.sources.some((source) => source.path === canonical7(configPath) && source.format === undefined && source.fields === undefined))
59515
59541
  unresolved("retained review is missing the full agent configuration source");
59516
- if (retained.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
59542
+ if (retained.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
59517
59543
  unresolved("retained settings witness names another configuration source");
59518
59544
  const covered = supersedesSettingsProjection(agent, retained.sources, canonical7(configPath)) ? sources.filter((source) => !isSupersededSettingsProjection(agent, canonical7(configPath), source)) : sources;
59519
59545
  if (covered.some((source) => !retained.sources.some((saved) => isDeepStrictEqual11(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
@@ -59527,7 +59553,7 @@ function resolveAgentDiscovery(options) {
59527
59553
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
59528
59554
  throw new Error("Explicit discovery reviews require full source-file hashes");
59529
59555
  verifyAgentDiscovery(supplied);
59530
- if (review.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
59556
+ if (review.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
59531
59557
  throw new Error("Semantic settings witnesses must name the configured agent configuration source");
59532
59558
  if (!review.sources.some((source) => source.path === canonical7(configPath)))
59533
59559
  throw new Error("Discovery review must include the agent configuration source");
@@ -112470,7 +112496,7 @@ function adoptCopyfileAliases(options = {}) {
112470
112496
  const actual = launcher.target;
112471
112497
  const oldLinkTarget = launcher.kind === "symlink" ? launcher.linkTarget : actual;
112472
112498
  const newTarget = join60(layout.currentPackageRoot, target);
112473
- if (actual === newTarget)
112499
+ if (actual === newTarget && launcher.kind === "pinned")
112474
112500
  continue;
112475
112501
  const chain = inspectAliasPackageChain(home, path, oldLinkTarget, actual, target);
112476
112502
  const source = validateAliasSource(home, name, actual);
@@ -112627,7 +112653,7 @@ function rollbackCopyfileAliases(receiptId, options = {}) {
112627
112653
  if (entryExists(item.backupPath)) {
112628
112654
  if (!launcherIs(item.backupPath, oldLauncherState(item)))
112629
112655
  throw new Error("ALIAS_PREIMAGE_DRIFT");
112630
- } else if (current !== item.oldTarget) {
112656
+ } else if (current !== item.oldTarget || !launcherIs(item.path, oldLauncherState(item))) {
112631
112657
  throw new Error("ALIAS_BACKUP_MISSING_FOR_SWITCH");
112632
112658
  }
112633
112659
  }
@@ -116159,19 +116185,21 @@ function registerAgentIntegration(parent) {
116159
116185
  process.exitCode = 1;
116160
116186
  }
116161
116187
  });
116162
- hook.command("witness").requiredOption("--kind <kind>", "sumi-settings-v1, claude-settings-v3, claude-settings-v2, codex-settings-v4, codex-settings-v3, codex-settings-v2, codex-settings-v1 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) => {
116163
- const capture2 = options.kind === "sumi-settings-v1" ? captureSumiSettings : options.kind === "codex-settings-v4" ? captureCodexSettingsV4 : options.kind === "codex-settings-v3" ? captureCodexSettingsV3 : options.kind === "codex-settings-v2" ? captureCodexSettingsV2 : options.kind === "codex-settings-v1" ? captureCodexSettings : options.kind === "claude-settings-v3" ? captureClaudeSettingsV3 : options.kind === "claude-settings-v2" ? captureClaudeSettingsV2 : options.kind === "claude-marketplace-registry-v2" ? captureClaudeMarketplaceRegistryV2 : null;
116188
+ hook.command("witness").requiredOption("--kind <kind>", "sumi-settings-v1, claude-settings-v4, claude-settings-v3, claude-settings-v2, codex-settings-v4, codex-settings-v3, codex-settings-v2, codex-settings-v1 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) => {
116189
+ const capture2 = options.kind === "sumi-settings-v1" ? captureSumiSettings : options.kind === "codex-settings-v4" ? captureCodexSettingsV4 : options.kind === "codex-settings-v3" ? captureCodexSettingsV3 : options.kind === "codex-settings-v2" ? captureCodexSettingsV2 : options.kind === "codex-settings-v1" ? captureCodexSettings : options.kind === "claude-settings-v4" ? captureClaudeSettingsV4 : options.kind === "claude-settings-v3" ? captureClaudeSettingsV3 : options.kind === "claude-settings-v2" ? captureClaudeSettingsV2 : options.kind === "claude-marketplace-registry-v2" ? captureClaudeMarketplaceRegistryV2 : null;
116164
116190
  if (!capture2)
116165
- throw new Error("Unsupported witness kind; select sumi-settings-v1, claude-settings-v3, claude-settings-v2, codex-settings-v4, codex-settings-v3, codex-settings-v2, codex-settings-v1 or claude-marketplace-registry-v2");
116191
+ throw new Error("Unsupported witness kind; select sumi-settings-v1, claude-settings-v4, claude-settings-v3, claude-settings-v2, codex-settings-v4, codex-settings-v3, codex-settings-v2, codex-settings-v1 or claude-marketplace-registry-v2");
116166
116192
  await writeCliOutput(JSON.stringify(capture2(options.path), null, 2));
116167
116193
  });
116168
- hook.command("rebind-settings").requiredOption("--agent <agent>", "claude, codex or sumi").requiredOption("--reviewed-preimage <path>", "Exact preserved legacy settings.json, config.toml or sumi.json").requiredOption("--expected-policy-sha256 <sha256>", "Exact current managed policy bytes").requiredOption("--expected-settings-sha256 <sha256>", "Exact current native settings bytes").option("--codex-witness-version <version>", "Explicit target: 2 (legacy default) 3 (service tier and model-advertised effort), or 4 (native availability UI counts)").option("--apply", "Apply the explicit semantic witness upgrade with preservation and readback", false).option("--json", "Return the metadata-only plan or receipt", false).description("Explicitly migrate a legacy native settings witness after proving only known preferences changed").action(async (options) => {
116194
+ hook.command("rebind-settings").requiredOption("--agent <agent>", "claude, codex or sumi").requiredOption("--reviewed-preimage <path>", "Exact preserved legacy settings.json, config.toml or sumi.json").requiredOption("--expected-policy-sha256 <sha256>", "Exact current managed policy bytes").requiredOption("--expected-settings-sha256 <sha256>", "Exact current native settings bytes").option("--codex-witness-version <version>", "Explicit target: 2 (legacy default) 3 (service tier and model-advertised effort), or 4 (native availability UI counts)").option("--claude-witness-version <version>", "Explicit target: 3 (default) or 4 (also permits a built-in top-level theme)").option("--apply", "Apply the explicit semantic witness upgrade with preservation and readback", false).option("--json", "Return the metadata-only plan or receipt", false).description("Explicitly migrate a legacy native settings witness after proving only known preferences changed").action(async (options) => {
116169
116195
  try {
116170
116196
  if (!["claude", "codex", "sumi"].includes(options.agent))
116171
116197
  throw new Error("Settings witness rebind accepts claude, codex or sumi");
116172
116198
  if (options.codexWitnessVersion !== undefined && !["2", "3", "4"].includes(options.codexWitnessVersion))
116173
116199
  throw new Error("Codex witness version accepts 2, 3 or 4");
116174
- const plan = planAgentSettingsWitnessUpgrade({ ...options.codexWitnessVersion !== undefined ? { targetCodexVersion: Number(options.codexWitnessVersion) } : {}, agent: options.agent, reviewedPreimage: options.reviewedPreimage, expectedPolicySha256: options.expectedPolicySha256, expectedSettingsSha256: options.expectedSettingsSha256 });
116200
+ if (options.claudeWitnessVersion !== undefined && !["3", "4"].includes(options.claudeWitnessVersion))
116201
+ throw new Error("Claude witness version accepts 3 or 4");
116202
+ const plan = planAgentSettingsWitnessUpgrade({ ...options.codexWitnessVersion !== undefined ? { targetCodexVersion: Number(options.codexWitnessVersion) } : {}, ...options.claudeWitnessVersion !== undefined ? { targetClaudeVersion: Number(options.claudeWitnessVersion) } : {}, agent: options.agent, reviewedPreimage: options.reviewedPreimage, expectedPolicySha256: options.expectedPolicySha256, expectedSettingsSha256: options.expectedSettingsSha256 });
116175
116203
  const result2 = options.apply ? applyAgentIntegration(plan) : { changed: [], backups: [] };
116176
116204
  const receipt = { applied: options.apply, settingsWitnessUpgrade: plan.settingsWitnessUpgrade, ...result2 };
116177
116205
  await writeCliOutput(options.json ? JSON.stringify(receipt) : `Settings witness ${options.apply ? "upgraded" : "planned"} for ${options.agent}. Native configuration was not changed.`);
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.10.49",
20022
+ version: "0.10.50",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -38370,7 +38370,9 @@ function inferencePreference(key, child, version2) {
38370
38370
  function settingsDigest(text, version2 = 1) {
38371
38371
  const value = parse6(text);
38372
38372
  value.entries = value.entries.filter(([key, child]) => {
38373
- if (version2 !== 1 && inferencePreference(key, child, version2))
38373
+ if (version2 === 4 && key === "theme" && child.kind === "string" && BUILTIN_THEMES.has(child.value))
38374
+ return false;
38375
+ if (version2 !== 1 && inferencePreference(key, child, version2 === 4 ? 3 : version2))
38374
38376
  return false;
38375
38377
  if (key === "model") {
38376
38378
  need(child.kind === "string", "contains an invalid model selection");
@@ -38418,6 +38420,16 @@ function hashClaudeSettingsReplacementV3(text, budget) {
38418
38420
  budget.remaining -= bytes;
38419
38421
  return settingsDigest(text, 3);
38420
38422
  }
38423
+ function captureClaudeSettingsV4(path, budget = { remaining: 256 * 1024 * 1024 }) {
38424
+ return { path, hashMode: "claude-settings-v4", sha256: settingsDigest(readNativeSettingsWitnessFile(path, budget), 4) };
38425
+ }
38426
+ function hashClaudeSettingsReplacementV4(text, budget) {
38427
+ need(typeof text === "string", "requires settings text");
38428
+ const bytes = Buffer.byteLength(text);
38429
+ need(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
38430
+ budget.remaining -= bytes;
38431
+ return settingsDigest(text, 4);
38432
+ }
38421
38433
  function hashNativeJsonControls(text, excludedMetadata) {
38422
38434
  need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
38423
38435
  const value = parse6(text);
@@ -38450,7 +38462,7 @@ function hashSumiNativeJsonControls(text) {
38450
38462
  });
38451
38463
  return createHash2("sha256").update("hasna.skills.sumi-settings.v1\x00").update(JSON.stringify(value)).digest("hex");
38452
38464
  }
38453
- 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;
38465
+ 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, CLAUDE_BUILTIN_THEMES, BUILTIN_THEMES, PERSISTED_EFFORT_LEVELS;
38454
38466
  var init_claude_settings_witness = __esm(() => {
38455
38467
  CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
38456
38468
  BOOLEAN_PREFERENCES = new Set([
@@ -38504,6 +38516,8 @@ var init_claude_settings_witness = __esm(() => {
38504
38516
  "claude-opus-4-6[1m]",
38505
38517
  "claude-sonnet-4-6[1m]"
38506
38518
  ]);
38519
+ CLAUDE_BUILTIN_THEMES = Object.freeze(["auto", "dark", "light", "dark-daltonized", "light-daltonized", "dark-ansi", "light-ansi"]);
38520
+ BUILTIN_THEMES = new Set(CLAUDE_BUILTIN_THEMES);
38507
38521
  PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
38508
38522
  });
38509
38523
 
@@ -38666,14 +38680,14 @@ function assertAgentPolicyCollections(policy) {
38666
38680
  text(source.path);
38667
38681
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
38668
38682
  if (source.hashMode !== undefined)
38669
- requireBound(["bytes", "path-bytes", "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "sumi-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
38683
+ requireBound(["bytes", "path-bytes", "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "sumi-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
38670
38684
  if (source.hashMode === "claude-plugin-manifest-v1")
38671
38685
  requireBound(agent === "claude" && value.agent === "claude" && ["automatic", "reviewed"].includes(value.method) && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute4(source.path) && resolve3(source.path) === source.path && source.path.endsWith(join10(sep3, ".claude-plugin", "plugin.json")) && source.managedPlugins === undefined);
38672
38686
  if (source.hashMode === "sumi-settings-v1")
38673
38687
  requireBound(agent === "sumi" && value.agent === "sumi" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute4(source.path) && resolve3(source.path) === source.path && basename4(source.path) === "sumi.json");
38674
38688
  if (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4"))
38675
38689
  requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute4(source.path) && resolve3(source.path) === source.path && basename4(source.path) === "config.toml");
38676
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
38690
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4"))
38677
38691
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute4(source.path) && resolve3(source.path) === source.path && basename4(source.path) === "settings.json");
38678
38692
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
38679
38693
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute4(source.path) && resolve3(source.path) === source.path && basename4(source.path) === "known_marketplaces.json");
@@ -50580,13 +50594,13 @@ function projected(source, changes, budget2 = discoveryByteBudget()) {
50580
50594
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
50581
50595
  return changes?.has(source.path) ? (source.hashMode === "codex-settings-v4" ? hashCodexSettingsReplacementV4 : source.hashMode === "codex-settings-v3" ? hashCodexSettingsReplacementV3 : source.hashMode === "codex-settings-v2" ? hashCodexSettingsReplacementV2 : hashCodexSettingsReplacement)(changes.get(source.path), budget2) : current;
50582
50596
  }
50583
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
50597
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4")) {
50584
50598
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
50585
50599
  throw new Error("Claude settings witnesses require exact typed metadata");
50586
- const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget2).sha256;
50600
+ const current = (source.hashMode === "claude-settings-v4" ? captureClaudeSettingsV4 : source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget2).sha256;
50587
50601
  if (current !== source.sha256)
50588
50602
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
50589
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget2) : current;
50603
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v4" ? hashClaudeSettingsReplacementV4 : source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget2) : current;
50590
50604
  }
50591
50605
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
50592
50606
  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))
@@ -50805,7 +50819,7 @@ function assertMarketplaceBinding(binding, source) {
50805
50819
  throw new Error("Sumi settings witnesses require explicit reviewed Sumi configuration");
50806
50820
  if ((source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")) && (binding.agent !== "codex" || binding.method !== "reviewed" || basename8(source.path) !== "config.toml"))
50807
50821
  throw new Error("Codex settings witnesses require explicit reviewed Codex configuration");
50808
- 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"))
50822
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4")) && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
50809
50823
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
50810
50824
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
50811
50825
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -50927,7 +50941,7 @@ function resolveAgentDiscovery(options) {
50927
50941
  verifyAgentDiscovery(retained);
50928
50942
  if (!retained.sources.some((source) => source.path === canonical7(configPath) && source.format === undefined && source.fields === undefined))
50929
50943
  unresolved("retained review is missing the full agent configuration source");
50930
- if (retained.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
50944
+ if (retained.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
50931
50945
  unresolved("retained settings witness names another configuration source");
50932
50946
  const covered = supersedesSettingsProjection(agent, retained.sources, canonical7(configPath)) ? sources.filter((source) => !isSupersededSettingsProjection(agent, canonical7(configPath), source)) : sources;
50933
50947
  if (covered.some((source) => !retained.sources.some((saved) => isDeepStrictEqual4(saved, source))) || [...roots].some((root) => !retained.roots.includes(root)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
@@ -50941,7 +50955,7 @@ function resolveAgentDiscovery(options) {
50941
50955
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
50942
50956
  throw new Error("Explicit discovery reviews require full source-file hashes");
50943
50957
  verifyAgentDiscovery(supplied);
50944
- if (review.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
50958
+ if (review.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
50945
50959
  throw new Error("Semantic settings witnesses must name the configured agent configuration source");
50946
50960
  if (!review.sources.some((source) => source.path === canonical7(configPath)))
50947
50961
  throw new Error("Discovery review must include the agent configuration source");
package/bin/migrate.js CHANGED
@@ -25,7 +25,7 @@ import { join as join5 } from "path";
25
25
  // package.json
26
26
  var package_default = {
27
27
  name: "@hasna/skills",
28
- version: "0.10.49",
28
+ version: "0.10.50",
29
29
  description: "Skills library for AI coding agents",
30
30
  type: "module",
31
31
  bin: {
package/bin/server.js CHANGED
@@ -24140,7 +24140,7 @@ restoreLaunchCwd();
24140
24140
  // package.json
24141
24141
  var package_default = {
24142
24142
  name: "@hasna/skills",
24143
- version: "0.10.49",
24143
+ version: "0.10.50",
24144
24144
  description: "Skills library for AI coding agents",
24145
24145
  type: "module",
24146
24146
  bin: {
package/bin/worker.js CHANGED
@@ -24143,7 +24143,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24143
24143
  // package.json
24144
24144
  var package_default = {
24145
24145
  name: "@hasna/skills",
24146
- version: "0.10.49",
24146
+ version: "0.10.50",
24147
24147
  description: "Skills library for AI coding agents",
24148
24148
  type: "module",
24149
24149
  bin: {
package/dist/index.d.ts CHANGED
@@ -64,7 +64,7 @@ export { planAgentIntegration, planClaudeStopHookUpdate, planClaudePreToolUseHoo
64
64
  export type { ClaudeManagedHookProjection, ClaudeManagedHookProjectionOptions } from "./lib/agent-integration.js";
65
65
  export { captureDiscoveryDirectories, captureDiscoveryByteSources, captureDiscoveryPathSources, type DiscoveryDirectory, type DiscoverySource } from "./lib/agent-discovery.js";
66
66
  export { captureClaudeMarketplaceRegistry, captureClaudeMarketplaceRegistryV2 } from "./lib/claude-marketplace-registry.js";
67
- export { captureClaudeSettings, captureClaudeSettingsV2, captureClaudeSettingsV3, upgradeClaudeSettingsWitness } from "./lib/claude-settings-witness.js";
67
+ export { captureClaudeSettings, captureClaudeSettingsV2, captureClaudeSettingsV3, captureClaudeSettingsV4, upgradeClaudeSettingsWitness, upgradeClaudeSettingsWitnessV4 } from "./lib/claude-settings-witness.js";
68
68
  export { buildPluginProjection, pluginTreeDigest, type PluginProjectionManifest, type PluginPayloadMapping, type PluginFileWitness } from "./lib/plugin-projection.js";
69
69
  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";
70
70
  export { captureManagedPluginRegistry, type ManagedPluginRegistrationWitness } from "./lib/plugin-discovery.js";
package/dist/index.js CHANGED
@@ -9177,6 +9177,8 @@ var BUILTIN_MODEL_SELECTIONS = new Set([
9177
9177
  "claude-opus-4-6[1m]",
9178
9178
  "claude-sonnet-4-6[1m]"
9179
9179
  ]);
9180
+ var CLAUDE_BUILTIN_THEMES = Object.freeze(["auto", "dark", "light", "dark-daltonized", "light-daltonized", "dark-ansi", "light-ansi"]);
9181
+ var BUILTIN_THEMES = new Set(CLAUDE_BUILTIN_THEMES);
9180
9182
  function canonical(value) {
9181
9183
  if (value.kind === "object")
9182
9184
  return { kind: "object", entries: value.entries.map(([key, child]) => [key, canonical(child)]).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0) };
@@ -9235,7 +9237,9 @@ function inferencePreference(key, child, version) {
9235
9237
  function settingsDigest(text, version = 1) {
9236
9238
  const value = parse(text);
9237
9239
  value.entries = value.entries.filter(([key, child]) => {
9238
- if (version !== 1 && inferencePreference(key, child, version))
9240
+ if (version === 4 && key === "theme" && child.kind === "string" && BUILTIN_THEMES.has(child.value))
9241
+ return false;
9242
+ if (version !== 1 && inferencePreference(key, child, version === 4 ? 3 : version))
9239
9243
  return false;
9240
9244
  if (key === "model") {
9241
9245
  need(child.kind === "string", "contains an invalid model selection");
@@ -9293,6 +9297,26 @@ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
9293
9297
  need(settingsDigest(before, 3) === sha256, "non-preference settings changed; explicit discovery review required");
9294
9298
  return { path: previous.path, hashMode: "claude-settings-v3", sha256 };
9295
9299
  }
9300
+ function captureClaudeSettingsV4(path, budget = { remaining: 256 * 1024 * 1024 }) {
9301
+ return { path, hashMode: "claude-settings-v4", sha256: settingsDigest(readNativeSettingsWitnessFile(path, budget), 4) };
9302
+ }
9303
+ function hashClaudeSettingsReplacementV4(text, budget) {
9304
+ need(typeof text === "string", "requires settings text");
9305
+ const bytes = Buffer.byteLength(text);
9306
+ need(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
9307
+ budget.remaining -= bytes;
9308
+ return settingsDigest(text, 4);
9309
+ }
9310
+ function upgradeClaudeSettingsWitnessV4(previous, reviewedSettingsPath) {
9311
+ need(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === undefined || previous.hashMode === "bytes" || previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2" || previous.hashMode === "claude-settings-v3") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact prior settings witness");
9312
+ const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
9313
+ const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
9314
+ const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash2("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : previous.hashMode === "claude-settings-v2" ? 2 : 3);
9315
+ need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
9316
+ const sha256 = settingsDigest(current, 4);
9317
+ need(settingsDigest(before, 4) === sha256, "non-preference settings changed; explicit discovery review required");
9318
+ return { path: previous.path, hashMode: "claude-settings-v4", sha256 };
9319
+ }
9296
9320
  function hashNativeJsonControls(text, excludedMetadata) {
9297
9321
  need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
9298
9322
  const value = parse(text);
@@ -9497,14 +9521,14 @@ function assertAgentPolicyCollections(policy) {
9497
9521
  text(source.path);
9498
9522
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
9499
9523
  if (source.hashMode !== undefined)
9500
- requireBound(["bytes", "path-bytes", "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "sumi-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
9524
+ requireBound(["bytes", "path-bytes", "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "sumi-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
9501
9525
  if (source.hashMode === "claude-plugin-manifest-v1")
9502
9526
  requireBound(agent === "claude" && value.agent === "claude" && ["automatic", "reviewed"].includes(value.method) && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && source.path.endsWith(join7(sep2, ".claude-plugin", "plugin.json")) && source.managedPlugins === undefined);
9503
9527
  if (source.hashMode === "sumi-settings-v1")
9504
9528
  requireBound(agent === "sumi" && value.agent === "sumi" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename3(source.path) === "sumi.json");
9505
9529
  if (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4"))
9506
9530
  requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename3(source.path) === "config.toml");
9507
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
9531
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4"))
9508
9532
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename3(source.path) === "settings.json");
9509
9533
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
9510
9534
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename3(source.path) === "known_marketplaces.json");
@@ -11572,7 +11596,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
11572
11596
  // package.json
11573
11597
  var package_default = {
11574
11598
  name: "@hasna/skills",
11575
- version: "0.10.49",
11599
+ version: "0.10.50",
11576
11600
  description: "Skills library for AI coding agents",
11577
11601
  type: "module",
11578
11602
  bin: {
@@ -17254,6 +17278,8 @@ function atomicWrite2(path, content) {
17254
17278
  function planAgentSettingsWitnessUpgrade(options) {
17255
17279
  if (options.targetCodexVersion !== undefined && (options.agent !== "codex" || ![2, 3, 4].includes(options.targetCodexVersion)))
17256
17280
  throw new Error("Invalid Codex witness target version");
17281
+ if (options.targetClaudeVersion !== undefined && (options.agent !== "claude" || ![3, 4].includes(options.targetClaudeVersion)))
17282
+ throw new Error("Invalid Claude witness target version");
17257
17283
  const home = resolve13(options.home ?? homedir2()), dataDir2 = options.dataDir ?? getDataDirReadOnly();
17258
17284
  const snapshot2 = readManagedSkillPolicySnapshot(dataDir2);
17259
17285
  if (!snapshot2 || sha3(snapshot2.text) !== options.expectedPolicySha256)
@@ -17301,7 +17327,7 @@ function planAgentSettingsWitnessUpgrade(options) {
17301
17327
  replaced.push(source);
17302
17328
  }
17303
17329
  }
17304
- const next = options.agent === "sumi" ? upgradeSumiSettingsWitness(previous, options.reviewedPreimage) : options.agent === "claude" ? upgradeClaudeSettingsWitness(previous, options.reviewedPreimage) : options.targetCodexVersion === 4 ? upgradeCodexSettingsWitnessV4(previous, options.reviewedPreimage) : options.targetCodexVersion === 3 ? upgradeCodexSettingsWitnessV3(previous, options.reviewedPreimage) : upgradeCodexSettingsWitness(previous, options.reviewedPreimage);
17330
+ const next = options.agent === "sumi" ? upgradeSumiSettingsWitness(previous, options.reviewedPreimage) : options.agent === "claude" ? options.targetClaudeVersion === 4 ? upgradeClaudeSettingsWitnessV4(previous, options.reviewedPreimage) : upgradeClaudeSettingsWitness(previous, options.reviewedPreimage) : options.targetCodexVersion === 4 ? upgradeCodexSettingsWitnessV4(previous, options.reviewedPreimage) : options.targetCodexVersion === 3 ? upgradeCodexSettingsWitnessV3(previous, options.reviewedPreimage) : upgradeCodexSettingsWitness(previous, options.reviewedPreimage);
17305
17331
  const rebound = [];
17306
17332
  for (const source of binding.sources) {
17307
17333
  if (!replaced.includes(source)) {
@@ -17347,7 +17373,7 @@ function applyAgentIntegrationUnlocked(plan, options = {}, assertCurrent) {
17347
17373
  const upgrade = plan.settingsWitnessUpgrade;
17348
17374
  if (!plan.managedAgentChecks || !plan.observedPolicy?.before)
17349
17375
  throw new Error("Invalid settings witness upgrade plan");
17350
- const verified = planAgentSettingsWitnessUpgrade({ agent: upgrade.agent, ...upgrade.agent === "codex" ? { targetCodexVersion: upgrade.toHashMode === "codex-settings-v4" ? 4 : upgrade.toHashMode === "codex-settings-v3" ? 3 : 2 } : {}, reviewedPreimage: upgrade.reviewedPreimage, home: plan.managedAgentChecks.home, dataDir: plan.dataDir, expectedPolicySha256: sha3(plan.observedPolicy.before), expectedSettingsSha256: upgrade.currentSettingsSha256 });
17376
+ const verified = planAgentSettingsWitnessUpgrade({ agent: upgrade.agent, ...upgrade.agent === "codex" ? { targetCodexVersion: upgrade.toHashMode === "codex-settings-v4" ? 4 : upgrade.toHashMode === "codex-settings-v3" ? 3 : 2 } : {}, ...upgrade.agent === "claude" && upgrade.toHashMode === "claude-settings-v4" ? { targetClaudeVersion: 4 } : {}, reviewedPreimage: upgrade.reviewedPreimage, home: plan.managedAgentChecks.home, dataDir: plan.dataDir, expectedPolicySha256: sha3(plan.observedPolicy.before), expectedSettingsSha256: upgrade.currentSettingsSha256 });
17351
17377
  if (JSON.stringify(verified.changes) !== JSON.stringify(plan.changes) || JSON.stringify(verified.settingsWitnessUpgrade) !== JSON.stringify(upgrade))
17352
17378
  throw new Error("Settings witness upgrade plan changed");
17353
17379
  provenDiscovery = verified.discoveryBefore[0];
@@ -20394,13 +20420,13 @@ function projected(source, changes, budget2 = discoveryByteBudget()) {
20394
20420
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
20395
20421
  return changes?.has(source.path) ? (source.hashMode === "codex-settings-v4" ? hashCodexSettingsReplacementV4 : source.hashMode === "codex-settings-v3" ? hashCodexSettingsReplacementV3 : source.hashMode === "codex-settings-v2" ? hashCodexSettingsReplacementV2 : hashCodexSettingsReplacement)(changes.get(source.path), budget2) : current;
20396
20422
  }
20397
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
20423
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4")) {
20398
20424
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
20399
20425
  throw new Error("Claude settings witnesses require exact typed metadata");
20400
- const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget2).sha256;
20426
+ const current = (source.hashMode === "claude-settings-v4" ? captureClaudeSettingsV4 : source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget2).sha256;
20401
20427
  if (current !== source.sha256)
20402
20428
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
20403
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget2) : current;
20429
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v4" ? hashClaudeSettingsReplacementV4 : source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget2) : current;
20404
20430
  }
20405
20431
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
20406
20432
  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))
@@ -20635,7 +20661,7 @@ function assertMarketplaceBinding(binding, source) {
20635
20661
  throw new Error("Sumi settings witnesses require explicit reviewed Sumi configuration");
20636
20662
  if ((source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")) && (binding.agent !== "codex" || binding.method !== "reviewed" || basename10(source.path) !== "config.toml"))
20637
20663
  throw new Error("Codex settings witnesses require explicit reviewed Codex configuration");
20638
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename10(source.path) !== "settings.json"))
20664
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4")) && (binding.agent !== "claude" || binding.method !== "reviewed" || basename10(source.path) !== "settings.json"))
20639
20665
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
20640
20666
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
20641
20667
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -20837,7 +20863,7 @@ function resolveAgentDiscovery(options) {
20837
20863
  verifyAgentDiscovery(retained);
20838
20864
  if (!retained.sources.some((source) => source.path === canonical7(configPath) && source.format === undefined && source.fields === undefined))
20839
20865
  unresolved("retained review is missing the full agent configuration source");
20840
- if (retained.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
20866
+ if (retained.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
20841
20867
  unresolved("retained settings witness names another configuration source");
20842
20868
  const covered = supersedesSettingsProjection(agent, retained.sources, canonical7(configPath)) ? sources.filter((source) => !isSupersededSettingsProjection(agent, canonical7(configPath), source)) : sources;
20843
20869
  if (covered.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))
@@ -20851,7 +20877,7 @@ function resolveAgentDiscovery(options) {
20851
20877
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
20852
20878
  throw new Error("Explicit discovery reviews require full source-file hashes");
20853
20879
  verifyAgentDiscovery(supplied);
20854
- if (review.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
20880
+ if (review.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
20855
20881
  throw new Error("Semantic settings witnesses must name the configured agent configuration source");
20856
20882
  if (!review.sources.some((source) => source.path === canonical7(configPath)))
20857
20883
  throw new Error("Discovery review must include the agent configuration source");
@@ -47854,6 +47880,7 @@ export {
47854
47880
  upgradeCodexSettingsWitnessV4,
47855
47881
  upgradeCodexSettingsWitnessV3,
47856
47882
  upgradeCodexSettingsWitness,
47883
+ upgradeClaudeSettingsWitnessV4,
47857
47884
  upgradeClaudeSettingsWitness,
47858
47885
  updateSkillRun,
47859
47886
  unsetConfig,
@@ -48045,6 +48072,7 @@ export {
48045
48072
  captureCodexSettingsV2,
48046
48073
  captureCodexSettings,
48047
48074
  captureCodexNativeSkillCatalog,
48075
+ captureClaudeSettingsV4,
48048
48076
  captureClaudeSettingsV3,
48049
48077
  captureClaudeSettingsV2,
48050
48078
  captureClaudeSettings,
@@ -7,7 +7,7 @@ export { captureDiscoveryDirectories, type DiscoveryDirectory } from "./agent-di
7
7
  export interface DiscoverySource {
8
8
  path: string;
9
9
  sha256: string | null;
10
- hashMode?: "bytes" | "path-bytes" | "claude-plugin-manifest-v1" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-settings-v3" | "claude-marketplace-registry-v2" | "codex-settings-v1" | "codex-settings-v2" | "codex-settings-v3" | "codex-settings-v4" | "sumi-settings-v1";
10
+ hashMode?: "bytes" | "path-bytes" | "claude-plugin-manifest-v1" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-settings-v3" | "claude-settings-v4" | "claude-marketplace-registry-v2" | "codex-settings-v1" | "codex-settings-v2" | "codex-settings-v3" | "codex-settings-v4" | "sumi-settings-v1";
11
11
  managedPlugins?: ManagedPluginRegistrationWitness[];
12
12
  format?: "json" | "toml" | "yaml";
13
13
  fields?: string[];
@@ -190,6 +190,7 @@ export declare function planClaudeHookEventsUpdate(options: ClaudeHookUpdateOpti
190
190
  export declare function planAgentSettingsWitnessUpgrade(options: {
191
191
  agent: "claude" | "codex" | "sumi";
192
192
  targetCodexVersion?: 2 | 3 | 4;
193
+ targetClaudeVersion?: 3 | 4;
193
194
  reviewedPreimage: string;
194
195
  home?: string;
195
196
  dataDir?: string;
@@ -10,6 +10,7 @@ export interface ClaudeSettingsWitnessBudget {
10
10
  remaining: number;
11
11
  }
12
12
  export declare function readNativeSettingsWitnessFile(path: string, budget: ClaudeSettingsWitnessBudget, filename?: "settings.json" | "config.toml" | "sumi.json"): string;
13
+ export declare const CLAUDE_BUILTIN_THEMES: readonly ["auto", "dark", "light", "dark-daltonized", "light-daltonized", "dark-ansi", "light-ansi"];
13
14
  /** Cooperating installers may replace only their explicitly selected event. Compare the strict syntax
14
15
  * trees, not JSON.parse values or the preference-tolerant witness digest: all
15
16
  * other hooks, discovery settings, unknown values and number spellings remain
@@ -51,6 +52,24 @@ export declare function upgradeClaudeSettingsWitness(previous: {
51
52
  hashMode?: "bytes" | "claude-settings-v1" | "claude-settings-v2";
52
53
  sha256: string;
53
54
  }, reviewedSettingsPath: string): ReturnType<typeof captureClaudeSettingsV3>;
55
+ /** Explicit review that additionally permits a built-in top-level theme.
56
+ * v1/v2/v3 witnesses keep their meaning; nothing selects v4 implicitly. */
57
+ export declare function captureClaudeSettingsV4(path: string, budget?: ClaudeSettingsWitnessBudget): {
58
+ path: string;
59
+ hashMode: "claude-settings-v4";
60
+ sha256: string;
61
+ };
62
+ export declare function hashClaudeSettingsReplacementV4(text: string, budget: ClaudeSettingsWitnessBudget): string;
63
+ /** Explicit opt-in upgrade to v4 with an exact preserved preimage of the prior
64
+ * review (raw bytes, v1, v2 or v3). It proves that every setting v4 binds is
65
+ * unchanged; any other drift, including skipDangerousModePermissionPrompt,
66
+ * refuses. A stored witness is never rewritten without this call.
67
+ */
68
+ export declare function upgradeClaudeSettingsWitnessV4(previous: {
69
+ path: string;
70
+ hashMode?: "bytes" | "claude-settings-v1" | "claude-settings-v2" | "claude-settings-v3";
71
+ sha256: string;
72
+ }, reviewedSettingsPath: string): ReturnType<typeof captureClaudeSettingsV4>;
54
73
  /** Strict bounded JSON control witness; metadata exclusion must be explicit. */
55
74
  export declare function hashNativeJsonControls(text: string, excludedMetadata?: "version"): string;
56
75
  /** Sumi's versioned review uses the same strict JSON parser and bounded syntax
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.49",
32376
+ version: "0.10.50",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -38140,6 +38140,8 @@ var BUILTIN_MODEL_SELECTIONS = new Set([
38140
38140
  "claude-opus-4-6[1m]",
38141
38141
  "claude-sonnet-4-6[1m]"
38142
38142
  ]);
38143
+ var CLAUDE_BUILTIN_THEMES = Object.freeze(["auto", "dark", "light", "dark-daltonized", "light-daltonized", "dark-ansi", "light-ansi"]);
38144
+ var BUILTIN_THEMES = new Set(CLAUDE_BUILTIN_THEMES);
38143
38145
  function canonical(value) {
38144
38146
  if (value.kind === "object")
38145
38147
  return { kind: "object", entries: value.entries.map(([key, child]) => [key, canonical(child)]).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0) };
@@ -38198,7 +38200,9 @@ function inferencePreference(key, child, version) {
38198
38200
  function settingsDigest(text, version = 1) {
38199
38201
  const value = parse2(text);
38200
38202
  value.entries = value.entries.filter(([key, child]) => {
38201
- if (version !== 1 && inferencePreference(key, child, version))
38203
+ if (version === 4 && key === "theme" && child.kind === "string" && BUILTIN_THEMES.has(child.value))
38204
+ return false;
38205
+ if (version !== 1 && inferencePreference(key, child, version === 4 ? 3 : version))
38202
38206
  return false;
38203
38207
  if (key === "model") {
38204
38208
  need(child.kind === "string", "contains an invalid model selection");
@@ -38256,6 +38260,26 @@ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
38256
38260
  need(settingsDigest(before, 3) === sha2562, "non-preference settings changed; explicit discovery review required");
38257
38261
  return { path: previous.path, hashMode: "claude-settings-v3", sha256: sha2562 };
38258
38262
  }
38263
+ function captureClaudeSettingsV4(path, budget = { remaining: 256 * 1024 * 1024 }) {
38264
+ return { path, hashMode: "claude-settings-v4", sha256: settingsDigest(readNativeSettingsWitnessFile(path, budget), 4) };
38265
+ }
38266
+ function hashClaudeSettingsReplacementV4(text, budget) {
38267
+ need(typeof text === "string", "requires settings text");
38268
+ const bytes = Buffer.byteLength(text);
38269
+ need(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
38270
+ budget.remaining -= bytes;
38271
+ return settingsDigest(text, 4);
38272
+ }
38273
+ function upgradeClaudeSettingsWitnessV4(previous, reviewedSettingsPath) {
38274
+ need(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === undefined || previous.hashMode === "bytes" || previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2" || previous.hashMode === "claude-settings-v3") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact prior settings witness");
38275
+ const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
38276
+ const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
38277
+ const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash6("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : previous.hashMode === "claude-settings-v2" ? 2 : 3);
38278
+ need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
38279
+ const sha2562 = settingsDigest(current, 4);
38280
+ need(settingsDigest(before, 4) === sha2562, "non-preference settings changed; explicit discovery review required");
38281
+ return { path: previous.path, hashMode: "claude-settings-v4", sha256: sha2562 };
38282
+ }
38259
38283
  function hashNativeJsonControls(text, excludedMetadata) {
38260
38284
  need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
38261
38285
  const value = parse2(text);
@@ -40405,14 +40429,14 @@ function assertAgentPolicyCollections(policy) {
40405
40429
  text(source.path);
40406
40430
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
40407
40431
  if (source.hashMode !== undefined)
40408
- requireBound(["bytes", "path-bytes", "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "sumi-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
40432
+ requireBound(["bytes", "path-bytes", "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "sumi-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
40409
40433
  if (source.hashMode === "claude-plugin-manifest-v1")
40410
40434
  requireBound(agent === "claude" && value.agent === "claude" && ["automatic", "reviewed"].includes(value.method) && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute8(source.path) && resolve7(source.path) === source.path && source.path.endsWith(join14(sep4, ".claude-plugin", "plugin.json")) && source.managedPlugins === undefined);
40411
40435
  if (source.hashMode === "sumi-settings-v1")
40412
40436
  requireBound(agent === "sumi" && value.agent === "sumi" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute8(source.path) && resolve7(source.path) === source.path && basename5(source.path) === "sumi.json");
40413
40437
  if (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4"))
40414
40438
  requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute8(source.path) && resolve7(source.path) === source.path && basename5(source.path) === "config.toml");
40415
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
40439
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4"))
40416
40440
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute8(source.path) && resolve7(source.path) === source.path && basename5(source.path) === "settings.json");
40417
40441
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
40418
40442
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute8(source.path) && resolve7(source.path) === source.path && basename5(source.path) === "known_marketplaces.json");
@@ -43246,13 +43270,13 @@ function projected(source, changes, budget2 = discoveryByteBudget()) {
43246
43270
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
43247
43271
  return changes?.has(source.path) ? (source.hashMode === "codex-settings-v4" ? hashCodexSettingsReplacementV4 : source.hashMode === "codex-settings-v3" ? hashCodexSettingsReplacementV3 : source.hashMode === "codex-settings-v2" ? hashCodexSettingsReplacementV2 : hashCodexSettingsReplacement)(changes.get(source.path), budget2) : current;
43248
43272
  }
43249
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
43273
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4")) {
43250
43274
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
43251
43275
  throw new Error("Claude settings witnesses require exact typed metadata");
43252
- const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget2).sha256;
43276
+ const current = (source.hashMode === "claude-settings-v4" ? captureClaudeSettingsV4 : source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget2).sha256;
43253
43277
  if (current !== source.sha256)
43254
43278
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
43255
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget2) : current;
43279
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v4" ? hashClaudeSettingsReplacementV4 : source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget2) : current;
43256
43280
  }
43257
43281
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
43258
43282
  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))
@@ -43487,7 +43511,7 @@ function assertMarketplaceBinding(binding, source) {
43487
43511
  throw new Error("Sumi settings witnesses require explicit reviewed Sumi configuration");
43488
43512
  if ((source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")) && (binding.agent !== "codex" || binding.method !== "reviewed" || basename8(source.path) !== "config.toml"))
43489
43513
  throw new Error("Codex settings witnesses require explicit reviewed Codex configuration");
43490
- 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"))
43514
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4")) && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
43491
43515
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
43492
43516
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
43493
43517
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -43689,7 +43713,7 @@ function resolveAgentDiscovery(options) {
43689
43713
  verifyAgentDiscovery(retained);
43690
43714
  if (!retained.sources.some((source) => source.path === canonical7(configPath) && source.format === undefined && source.fields === undefined))
43691
43715
  unresolved("retained review is missing the full agent configuration source");
43692
- if (retained.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
43716
+ if (retained.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
43693
43717
  unresolved("retained settings witness names another configuration source");
43694
43718
  const covered = supersedesSettingsProjection(agent, retained.sources, canonical7(configPath)) ? sources.filter((source) => !isSupersededSettingsProjection(agent, canonical7(configPath), source)) : sources;
43695
43719
  if (covered.some((source) => !retained.sources.some((saved) => isDeepStrictEqual6(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
@@ -43703,7 +43727,7 @@ function resolveAgentDiscovery(options) {
43703
43727
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
43704
43728
  throw new Error("Explicit discovery reviews require full source-file hashes");
43705
43729
  verifyAgentDiscovery(supplied);
43706
- if (review.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
43730
+ if (review.sources.some((source) => (source.hashMode === "sumi-settings-v1" || source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4")))) && source.path !== canonical7(configPath)))
43707
43731
  throw new Error("Semantic settings witnesses must name the configured agent configuration source");
43708
43732
  if (!review.sources.some((source) => source.path === canonical7(configPath)))
43709
43733
  throw new Error("Discovery review must include the agent configuration source");
@@ -45374,6 +45398,8 @@ function atomicWrite2(path, content) {
45374
45398
  function planAgentSettingsWitnessUpgrade(options) {
45375
45399
  if (options.targetCodexVersion !== undefined && (options.agent !== "codex" || ![2, 3, 4].includes(options.targetCodexVersion)))
45376
45400
  throw new Error("Invalid Codex witness target version");
45401
+ if (options.targetClaudeVersion !== undefined && (options.agent !== "claude" || ![3, 4].includes(options.targetClaudeVersion)))
45402
+ throw new Error("Invalid Claude witness target version");
45377
45403
  const home = resolve18(options.home ?? homedir3()), dataDir2 = options.dataDir ?? getDataDirReadOnly();
45378
45404
  const snapshot2 = readManagedSkillPolicySnapshot(dataDir2);
45379
45405
  if (!snapshot2 || sha4(snapshot2.text) !== options.expectedPolicySha256)
@@ -45421,7 +45447,7 @@ function planAgentSettingsWitnessUpgrade(options) {
45421
45447
  replaced.push(source);
45422
45448
  }
45423
45449
  }
45424
- const next = options.agent === "sumi" ? upgradeSumiSettingsWitness(previous, options.reviewedPreimage) : options.agent === "claude" ? upgradeClaudeSettingsWitness(previous, options.reviewedPreimage) : options.targetCodexVersion === 4 ? upgradeCodexSettingsWitnessV4(previous, options.reviewedPreimage) : options.targetCodexVersion === 3 ? upgradeCodexSettingsWitnessV3(previous, options.reviewedPreimage) : upgradeCodexSettingsWitness(previous, options.reviewedPreimage);
45450
+ const next = options.agent === "sumi" ? upgradeSumiSettingsWitness(previous, options.reviewedPreimage) : options.agent === "claude" ? options.targetClaudeVersion === 4 ? upgradeClaudeSettingsWitnessV4(previous, options.reviewedPreimage) : upgradeClaudeSettingsWitness(previous, options.reviewedPreimage) : options.targetCodexVersion === 4 ? upgradeCodexSettingsWitnessV4(previous, options.reviewedPreimage) : options.targetCodexVersion === 3 ? upgradeCodexSettingsWitnessV3(previous, options.reviewedPreimage) : upgradeCodexSettingsWitness(previous, options.reviewedPreimage);
45425
45451
  const rebound = [];
45426
45452
  for (const source of binding.sources) {
45427
45453
  if (!replaced.includes(source)) {
@@ -45467,7 +45493,7 @@ function applyAgentIntegrationUnlocked(plan, options = {}, assertCurrent) {
45467
45493
  const upgrade = plan.settingsWitnessUpgrade;
45468
45494
  if (!plan.managedAgentChecks || !plan.observedPolicy?.before)
45469
45495
  throw new Error("Invalid settings witness upgrade plan");
45470
- const verified = planAgentSettingsWitnessUpgrade({ agent: upgrade.agent, ...upgrade.agent === "codex" ? { targetCodexVersion: upgrade.toHashMode === "codex-settings-v4" ? 4 : upgrade.toHashMode === "codex-settings-v3" ? 3 : 2 } : {}, reviewedPreimage: upgrade.reviewedPreimage, home: plan.managedAgentChecks.home, dataDir: plan.dataDir, expectedPolicySha256: sha4(plan.observedPolicy.before), expectedSettingsSha256: upgrade.currentSettingsSha256 });
45496
+ const verified = planAgentSettingsWitnessUpgrade({ agent: upgrade.agent, ...upgrade.agent === "codex" ? { targetCodexVersion: upgrade.toHashMode === "codex-settings-v4" ? 4 : upgrade.toHashMode === "codex-settings-v3" ? 3 : 2 } : {}, ...upgrade.agent === "claude" && upgrade.toHashMode === "claude-settings-v4" ? { targetClaudeVersion: 4 } : {}, reviewedPreimage: upgrade.reviewedPreimage, home: plan.managedAgentChecks.home, dataDir: plan.dataDir, expectedPolicySha256: sha4(plan.observedPolicy.before), expectedSettingsSha256: upgrade.currentSettingsSha256 });
45471
45497
  if (JSON.stringify(verified.changes) !== JSON.stringify(plan.changes) || JSON.stringify(verified.settingsWitnessUpgrade) !== JSON.stringify(upgrade))
45472
45498
  throw new Error("Settings witness upgrade plan changed");
45473
45499
  provenDiscovery = verified.discoveryBefore[0];
@@ -239,8 +239,8 @@ Invalid values for the display preferences refuse capture and verification. Ever
239
239
  other field remains bound, including unknown future fields, hooks and their
240
240
  exact commands, permissions, native skill suppression and synchronization,
241
241
  plugin roots, marketplaces, environment and configuration precedence. Language,
242
- output style, theme, and command-bearing status or file suggestion settings are
243
- intentionally retained. These are narrow preference and inference-selection
242
+ output style, theme (v4 alone permits the built-in theme values), and
243
+ command-bearing status or file suggestion settings are intentionally retained. These are narrow preference and inference-selection
244
244
  exceptions, not general permission to change Claude configuration. The native
245
245
  bridge guard continues to check its exact commands and native Skill policy.
246
246
 
@@ -263,6 +263,43 @@ or guarantee when a running native client adopts a settings edit.
263
263
  Those complementary checks retain their existing exact registration and hook
264
264
  entry comparisons; this mode does not relax them for property-order changes.
265
265
 
266
+ ### Explicit v4 built-in theme reviews
267
+
268
+ `claude-settings-v4` is an explicit successor to v3. It binds everything v3
269
+ binds, with one more omission: the top-level `theme` key, and only when its
270
+ value is exactly one of the built-in values `auto`, `dark`, `light`,
271
+ `dark-daltonized`, `light-daltonized`, `dark-ansi` or `light-ansi`. The list is
272
+ pinned in code (`CLAUDE_BUILTIN_THEMES`) from the
273
+ [`theme` setting reference](https://code.claude.com/docs/en/settings-reference#theme)
274
+ and the [built-in presets](https://code.claude.com/docs/en/terminal-config#match-the-color-theme),
275
+ read on 2026-10-07. Custom and plugin themes (`custom:<slug>`,
276
+ `custom:<plugin-name>:<slug>`) load theme files, so they stay bound, as does any
277
+ other value, type, spelling or letter case, and any `theme` key nested inside
278
+ another setting. `skipDangerousModePermissionPrompt`, permissions, hooks,
279
+ environment, `apiKeyHelper` and other helpers, plugins, marketplaces, custom
280
+ model values and unknown settings stay bound exactly as in v3. The digest is
281
+ domain-separated from v3.
282
+
283
+ Existing v1, v2 and v3 witnesses keep their meaning; nothing selects v4
284
+ implicitly, and a stored witness is never rewritten. A policy opts in through
285
+ either explicit path:
286
+
287
+ - a fresh review: `skills hook witness --kind claude-settings-v4 --path
288
+ <settings.json> --json` (or `captureClaudeSettingsV4(path)`), placed in the
289
+ reviewed discovery inputs and applied with the normal `skills hook install
290
+ --discovery-inputs <file>` plan/apply flow;
291
+ - a preserved-preimage upgrade: `skills hook rebind-settings --agent claude
292
+ --claude-witness-version 4 --reviewed-preimage <preserved-settings.json>
293
+ --expected-policy-sha256 <policy-hash> --expected-settings-sha256
294
+ <settings-hash>` (or `upgradeClaudeSettingsWitnessV4`). It accepts a raw, v1,
295
+ v2 or v3 witness, proves the preimage matches it in its original mode, and
296
+ refuses unless the current settings equal the preimage under v4. Without
297
+ `--claude-witness-version` the rebind target remains v3.
298
+
299
+ Install a runtime that recognizes `claude-settings-v4`, including any bundled
300
+ copy of the verifier, before a policy carries it; an older runtime refuses the
301
+ unknown hash mode.
302
+
266
303
  ## Verification and limits
267
304
 
268
305
  Unit tests cover provenance and mapping failures, offline/revoked authorities,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.49",
3
+ "version": "0.10.50",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {