@hasna/skills 0.10.51 → 0.10.52

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.
@@ -173,6 +173,74 @@ new loaders still require their dedicated review. API failure refuses an update;
173
173
  stale projection as synchronized. Claude may retain its previous installation
174
174
  after a refused update, whose existing local drift checks continue to apply.
175
175
 
176
+ ## Reviewed plugin-hook attestation scope
177
+
178
+ A reviewed discovery input carries `pluginHooks: "reviewed-no-skill-injection"`.
179
+ That value is the reviewer's statement; Skills does not inspect hook behavior.
180
+ What the software currently enforces is narrower:
181
+
182
+ - It is point-in-time. It binds the listed sources as they were when the review
183
+ was applied, apart from the manifest projection described here and the
184
+ retired-root exemption described below. A manifest listed as a plain byte
185
+ witness with a non-null `sha256` is stored as a `claude-plugin-manifest-v1`
186
+ projection that ignores the descriptive fields `description`, `version`,
187
+ `author`, `homepage`, `repository`, `license` and `keywords`. The projection
188
+ also normalises whitespace and JSON string escaping; key order stays bound.
189
+ Every other manifest field, including `hooks`, `skills`, `commands` and
190
+ unknown fields, stays bound.
191
+ - It enforces one coupling only, and only for a Claude
192
+ `.claude-plugin/plugin.json` listed as a plain byte witness (`hashMode`
193
+ omitted or `"bytes"`) with a non-null `sha256`. The review must then also list
194
+ that plugin's `hooks/hooks.json` (when the file exists at review time) and any
195
+ string `hooks` target in the manifest, each as a plain byte witness with a
196
+ non-null hash. A non-string `hooks` value and a `hooks` target outside the
197
+ plugin root are refused. A manifest listed as `claude-plugin-manifest-v1`
198
+ (the form Skills writes into the stored policy) or as `path-bytes` triggers
199
+ no hook-file requirement. Nothing else is required.
200
+ - A `hooks/hooks.json` that is absent at review time and appears later is
201
+ detected if the review pins it with `sha256: null` or covers it with a
202
+ reviewed `directories` membership witness on the plugin root or its `hooks/`
203
+ directory. Such a witness, from `captureDiscoveryDirectories(paths)`, binds
204
+ the recursive path and type of every member, so every check detects added or
205
+ removed files and directories under it, but not content changes.
206
+ - For plugins not covered by a `claude-plugin-registry` admission witness (see
207
+ [Discovery transition contract](#discovery-transition-contract)):
208
+ - other plugin files, such as hook modules (`register.ts`, `runtime.ts` and
209
+ their imports), `bin/` files and `package.json`, are bound only if the
210
+ review lists them. A content change to an unlisted file is not detected,
211
+ and an added or removed file is detected only under a reviewed
212
+ `directories` witness that covers it;
213
+ - the review is not anchored to the roots registered in
214
+ `~/.claude/plugins/installed_plugins.json`. When a review is present, the
215
+ enabled-plugin walk does not run, so Skills does not check that the listed
216
+ plugin roots are the registered ones or that every enabled plugin is listed.
217
+ - A `claude-plugin-registry` witness covers its admitted, receipt-backed
218
+ plugins on every check. It re-reads `installed_plugins.json`, requires each
219
+ managed row's `installPath` to be `cache/<marketplace>/<plugin>/<version>`,
220
+ checks every file in every retained cache version against the admission
221
+ receipts without the review listing them, and binds unmanaged rows exactly.
222
+ - An entirely absent retired Claude version root exempts only its plain byte
223
+ sources (`hashMode` omitted or `"bytes"`, with no `format`, `fields` or
224
+ `managedPlugins`). Such a source with a non-null `sha256` is also the only
225
+ thing that identifies the root, and the root qualifies only while it stays
226
+ entirely absent and the reviewed settings and registry select a different,
227
+ verified version. Every other source under that root is re-hashed as usual,
228
+ and one whose file existed at review no longer matches its hash, so the
229
+ check refuses. That includes the plugin manifest in the
230
+ `claude-plugin-manifest-v1` form Skills stores, so a stored review that lists
231
+ the retired version's manifest refuses until a fresh review replaces it. A
232
+ `directories` witness that covers the retired root is not exempt either. If
233
+ the root reappears, its sources are checked again.
234
+ - It is detection-only. Later checks re-hash the stored sources and re-walk any
235
+ reviewed `directories` witnesses. Drift makes the
236
+ Skills hooks refuse with `NATIVE_SKILL_DRIFT`; it does not stop, unload or
237
+ sandbox plugin code, which the native client keeps running.
238
+ - It is a tripwire, not a security boundary. A process running as the same user
239
+ can rewrite the plugin files and the managed policy that holds the witnesses.
240
+
241
+ For plugins outside receipt-backed admission, a stronger witness anchored to the
242
+ registered plugin roots is planned and not yet implemented.
243
+
176
244
  ## Marketplace registry timestamps
177
245
 
178
246
  For an explicitly reviewed `known_marketplaces.json`,
@@ -196,6 +264,123 @@ not proof of a timestamp-only change: review the complete current registration
196
264
  and its complementary sources before explicitly replacing an old witness.
197
265
  Capturing this witness does not write a policy or approve native registration.
198
266
 
267
+ ## Marketplace plugin entry witness
268
+
269
+ `claude-marketplace-entry-v1` witnesses one plugin entry of a Claude
270
+ `marketplace.json`, for a plugin whose only declaration is its marketplace entry
271
+ (for example a `strict: false` entry with no `plugin.json`, such as
272
+ `swift-lsp@claude-plugins-official`). A bytes witness of the whole catalog
273
+ drifts every time Claude refreshes the marketplace by itself; this mode moves
274
+ only when something that decides how the selected entry resolves or what it
275
+ injects changes.
276
+
277
+ The source names an absolute, normalized `<root>/.claude-plugin/marketplace.json`
278
+ path, the exact marketplace `name` and the exact plugin entry `name`:
279
+
280
+ ```json
281
+ { "path": "/home/user/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json",
282
+ "hashMode": "claude-marketplace-entry-v1", "marketplace": "claude-plugins-official",
283
+ "plugin": "swift-lsp", "sha256": "<digest>" }
284
+ ```
285
+
286
+ It is accepted only in explicit reviewed Claude discovery. Paths with `..`,
287
+ `//`, a trailing slash, control characters, another file name or another parent
288
+ directory refuse, as do names outside `[A-Za-z0-9][A-Za-z0-9._-]{0,127}`.
289
+
290
+ The digest is SHA-256 over the domain prefix
291
+ `hasna.skills.claude-marketplace-entry.v1\0` and canonical JSON (sorted object
292
+ keys, array order and number spelling kept, no whitespace) of:
293
+
294
+ - the marketplace `name`, `metadata.pluginRoot` (`null` when absent) and
295
+ `allowCrossMarketplaceDependenciesOn` (`null` when absent), for every entry
296
+ whether or not it declares `dependencies`: the root allowlist also governs
297
+ dependencies declared in the plugin's own `plugin.json`, which Claude's
298
+ auto-update and `/reload-plugins` act on. A present allowlist must be an
299
+ array of strings, so `null` only ever means absent;
300
+ - the selected entry projected to its bound keys: `name`, `source`, `strict`,
301
+ `defaultEnabled`, `dependencies`, `relevance`, `headers`, `headersHelper`,
302
+ `settings`, `userConfig`, `types`, `channels`, `skills`, `commands`, `agents`,
303
+ `hooks`, `mcpServers`, `lspServers`, `outputStyles`, `workflows`,
304
+ `experimental`, `themes` and `monitors`, each with its full value.
305
+
306
+ Key order: this canonical JSON sorts object keys at every level, under the
307
+ domain prefix above. That deliberately differs from `claude-plugin-manifest-v1`,
308
+ which keeps the manifest's own key order and has no domain prefix. Reordering
309
+ keys in the catalog therefore never moves this digest, while array order,
310
+ including the order of `allowCrossMarketplaceDependenciesOn`, stays bound.
311
+
312
+ The entry's display and catalog metadata is validated and omitted:
313
+ `$schema`, `description`, `version`, `author`, `homepage`, `repository`,
314
+ `license`, `keywords`, `category`, `tags`, `displayName` and `metadata`. A real
315
+ version or install-path change is still caught by the separate
316
+ `installed_plugins.json` witness, which this mode never replaces. Other entries,
317
+ the catalog `description`, `version`, `owner` and `$schema`,
318
+ `forceRemoveDeletedPlugins` and renames of other plugins do not move the digest.
319
+ Adding, removing or changing `allowCrossMarketplaceDependenciesOn` always does.
320
+
321
+ The mode fails closed. Capture and verification refuse when:
322
+
323
+ - the file is missing, a link, a special file, larger than 1 MiB, not strict
324
+ UTF-8, not one JSON object, has trailing content or a duplicate key;
325
+ - the marketplace `name` differs from the bound name, or `plugins` is not an
326
+ array;
327
+ - the selected entry is missing, appears twice, or has a case-only variant;
328
+ - `renames` maps the selected name to another entry or to `null`;
329
+ - the entry has a key outside the two lists above (including the directory
330
+ listing fields), `experimental` has a key other than `themes`, `monitors` or
331
+ `evals`, or an object `source` has an unknown type or a field outside that
332
+ type's documented fields (a `url` source may also carry `path`, as the
333
+ official catalog does);
334
+ - the catalog has a top-level key outside `$schema`, `name`, `owner`,
335
+ `plugins`, `description`, `version`, `metadata`, `forceRemoveDeletedPlugins`,
336
+ `allowCrossMarketplaceDependenciesOn` and `renames`, or a `metadata` key other
337
+ than `description`, `version` and `pluginRoot`;
338
+ - an omitted metadata field has the wrong type.
339
+
340
+ New fields are refused, never ignored, until a reviewed version of this mode
341
+ covers them. A refusal for an unknown key names the mode, the bound marketplace
342
+ name and the exact key path, and never echoes a value. Key names are bounded to
343
+ 64 characters and JSON-quoted, with every character outside printable ASCII
344
+ escaped. A plain key extends the path with `.key`; any other key appears as
345
+ `["key"]`. For example:
346
+
347
+ ```text
348
+ claude-marketplace-entry-v1: unknown top-level key "pluginSearchPaths" in claude-plugins-official
349
+ claude-marketplace-entry-v1: unknown key "metadata.skillRoot" in claude-plugins-official
350
+ claude-marketplace-entry-v1: unknown key "plugins[swift-lsp].futureInjector" in claude-plugins-official
351
+ claude-marketplace-entry-v1: unknown key "plugins[swift-lsp].source.script" in claude-plugins-official
352
+ ```
353
+
354
+ Inside a hook check the same text follows `NATIVE_SKILL_DRIFT:`. The only way
355
+ forward after such a refusal is a fresh human review of the changed catalog
356
+ and a guarded exact re-pin through hook installation: write the reviewed
357
+ witnesses to a discovery inputs file, preview `skills hook install --agent
358
+ claude --discovery-inputs <file>`, then run the same command with `--apply`.
359
+ While the unknown key is present this mode refuses capture too, so that review
360
+ must bind the catalog another way, for example an exact `bytes` witness from
361
+ `captureDiscoveryByteSources`, until a reviewed version of this mode covers the
362
+ key. There is no bypass, ignore list or relaxed mode.
363
+
364
+ The field lists follow the
365
+ [marketplace reference](https://code.claude.com/docs/en/plugins/marketplace-reference)
366
+ and the [plugin manifest reference](https://code.claude.com/docs/en/plugins/manifest-reference),
367
+ read on 2026-10-07.
368
+
369
+ Capture a witness with
370
+ `skills hook witness --kind claude-marketplace-entry-v1 --path <marketplace.json>
371
+ --marketplace <name> --plugin <name> --json` (or
372
+ `captureClaudeMarketplaceEntry(path, marketplace, plugin)`). Capture writes
373
+ nothing and does not authorize a review. Put the witness in reviewed discovery
374
+ inputs, keep the `settings.json`, `installed_plugins.json` and
375
+ `known_marketplaces.json` witnesses and the plugin's absence witnesses, and
376
+ preview `skills hook install --discovery-inputs <file>` before applying. Hook
377
+ installation never rewrites the catalog, so an entry change between planning
378
+ and applying refuses.
379
+
380
+ Install a runtime that recognizes `claude-marketplace-entry-v1`, including any
381
+ bundled copy of the verifier, before a policy carries it; an older runtime
382
+ refuses the whole policy because the hash mode is unknown.
383
+
199
384
  ## Claude settings preferences
200
385
 
201
386
  `captureClaudeSettings(canonicalSettingsPath)` returns a versioned
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.51",
3
+ "version": "0.10.52",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {