Files
msd-core/docs/reference/gsd-capability-command.md
Tom Boucher 34bc096ec2 feat(#1451): wire gsd capability install/update/remove/list/disable/enable management CLI (#1457)
* feat(#1451): wire gsd capability install/update/remove/list/disable/enable CLI

ADR-1244 D5/D6: the management command was built as a library (capability-lifecycle.cjs
install/upgrade/remove + capability-ledger.cjs) across Phases 3-5 but never wired to a
user-facing command — gsd-tools.cjs 'capability' only handled state/set. This adds the
six subcommands, dispatching to the existing lifecycle/ledger:

- install <spec> [--integrity] [--scope global|project] [--yes] [--shared-file <rel>]…
- update [<id>|--all] [--scope] [--yes] [--shared-file]  (re-resolves recorded source)
- remove <id> [--purge-data] [--scope]  (first-party rejected)
- list [--json]  (first-party + overlay, both scopes, JSON array)
- disable|enable <id>  (activation-state alias of capability set --off/--on)

Scope→runtimeDir mapping matches capability-loader exactly (global=$GSD_HOME||home,
project=project root; caps at <root>/.gsd/capabilities/<id>, ledger at <root>/.gsd-capabilities.json).
Consent is non-interactive: --yes grants; without it an executable install aborts after
printing the disclosure and writes nothing. Best-effort reconcile before each mutation.

Tests: tests/capability-cli.test.cjs (20 behavioral, real resolver via local specs,
GSD_HOME-sandboxed) — install consent/block/usage matrix, list, update round-trip,
remove round-trip + first-party guard, disable/enable, unknown subcommand.
Docs: docs/reference/gsd-capability-command.md reconciled to the real surface
(ledger paths, --shared-file, consent model, disable mechanism, outdated marked planned);
docs/COMMANDS.md gains the gsd capability entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1451): resolve adversarial-review findings + root-cause the --raw silent-output bug

Adversarial-review (Codex) fixes:
- capReadStrict passes a malformed strict_known_registries value THROUGH so the trust gate
  fail-closes on it (was silently downgrading to permissive)
- installCapability/upgradeCapability gain an expectedId guard + first-party-id rejection
  (capability-lifecycle.cts): an overlay can't shadow a first-party id, and 'update <id>' can't
  act on a different id if the recorded source was retargeted
- capability update: prints the consent disclosure, exits non-zero on --all partial failure,
  no longer masks the resolved id
- capability remove: ledger-first ordering so an overlay is removable even if it shadows a
  first-party name; first-party guard only fires for ids not in the ledger
- gsd-capability-command.md: disable/enable doc corrected (registry-known ids; overlay toggle
  not yet wired through this path)

Silent-output bug (root cause, not waved off as pre-existing):
- captureStdoutSyncWrites buffered fd-1 output and DISCARDED it on the throw path — any --raw
  command that emitted a result/error envelope then threw (to set a non-zero exit) lost ALL of
  stdout. Now it flushes the captured buffer before re-throwing (exit code preserved).
- cmdCapabilitySet threw via process.exit() (bypassing the capture wrapper entirely); now throws
  ExitError so the wrapper flushes — matches the repo's no-process-exit architecture.
- Regression test: capability disable <unknown> --raw must emit the JSON error envelope on stdout.

Verified: capability suite 165/165, @file/json-errors/phase 183/183, lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1451): address adversarial-review R2 — shared-file confinement, MCP no-clobber, config fail-closed

- confinedSharedFile(): realpath-confine every shared-config write/strip to the scope root (mirrors
  safeRmUnder), so a --shared-file whose parent is a symlink escaping the scope can't write outside it.
- mcpServers shared edits: never overwrite an UNOWNED entry — a name collision with the user's (or
  another capability's) server is skipped, so install/remove can't silently clobber user MCP config
  (hooks already append; the map-keyed mcpServers path was the gap).
- capReadStrict: a PRESENT-but-unparseable .planning/config.json now fails CLOSED (lockdown) instead
  of silently downgrading the strict_known_registries policy to permissive.
- Tests: symlink-escape shared-file writes nothing outside scope; colliding user mcpServers entry
  preserved; unparseable config blocks an external install. capability suite 83/83, lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1451): address code-review — aborted-status robustness + coverage + project-scoped strict doc

- install/update: handle an 'aborted' result independently of the requiresConsent flag so it can never
  fall through to the generic 'blocked: unknown reason' arm (aborted always means consent-needed per
  the lifecycle contract; latent today, hardened for future status additions).
- Clarify capResolveScope comment (project scope === already-resolved cwd) and document that
  strict_known_registries is a PROJECT-scoped policy (read regardless of --scope; no machine-wide
  allowlist) in gsd-capability-command.md.
- Tests: update --all over an empty ledger returns an empty result set (exit 0); a flag value that
  looks like another flag (--integrity --scope) is rejected, not swallowed. CLI suite 33/33, lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1451): FEATURES.md entry #147 + Added/Fixed changesets for the capability CLI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1451): backfill changeset PR number → #1457

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 12:02:36 -04:00

12 KiB

gsd capability Command Reference

Slash form: gsd:capability (surfaced as a slash command on slash-command runtimes) CLI form: gsd capability Canonical ADR: ADR-1244 See also: Capability Manifest Reference · How to develop a capability · The capability trust model

The capability family manages the installation, upgrade, removal, and inspection of GSD capabilities — both first-party (shipped) and third-party overlays. A row for this command also appears in docs/COMMANDS.md (that file is not edited here).

Implemented in 1.6.0: install, update, remove, list, disable, enable (plus the pre-existing state and set introspection/activation subcommands). Planned (not yet implemented): outdated — see Planned subcommands.


Subcommands

install

Synopsis

gsd capability install <spec> [--integrity sha512-<hash>] [--scope global|project] [--yes] [--shared-file <rel>]…

Arguments

Argument Description
<spec> Source specification (see Source specifications below).

Flags

Flag Type Default Description
--integrity sha512-<base64> — SHA-512 bundle hash to verify before extraction. When supplied, a mismatch aborts the install. When the source registry or capability.json already carries an integrity field, both must agree.
--scope global | project global Installation root (see Install layout).
--yes flag off Grant consent for the capability's executable surfaces non-interactively. The disclosure is still printed. Without it, an install that declares executable surfaces is aborted after printing the disclosure (the CLI is non-interactive — there is no prompt to answer).
--shared-file path (repeatable) — A file, relative to the scope root, into which the capability's disclosed hooks / MCP servers should be spliced (e.g. a runtime's settings.json). Each fragment is marker-isolated so remove can strip exactly it. When omitted, the bundle still installs (declaratively); no shared-file edits are made.

Behaviour

Resolves <spec> to a versioned, staged capability bundle. The pipeline is: fetch → verify integrity or SHA pin → check engines.gsd against the installed GSD version → disclose executable surfaces (hooks, command modules, MCP servers) → obtain consent (a declarative capability needs none; an executable one requires --yes) → validate the incoming manifest against the trust invariants → extract to the scope root → write the ledger entry atomically.

An overlay whose id uses a reserved first-party prefix (gsd-, gsd-core-, anthropic-) is rejected before extraction. Install never executes capability code; staging is copy-only. A declined install (executable surface, no --yes) writes nothing — no bundle, no ledger entry, no shared-file edits.

A best-effort reconciliation sweep runs before the mutation to recover any crash orphans from a prior interrupted operation.

The ledger file (<scope-root>/.gsd-capabilities.json, see Install layout) records the installed version, source, integrity hash, owned files, and any fragments written into shared files.


update

Synopsis

gsd capability update [<id> | --all] [--scope global|project] [--yes] [--shared-file <rel>]…

Arguments

Argument Description
<id> Capability identifier to update. Omitting both <id> and --all is an error; passing both is an error.

Flags

Flag Description
--all Re-resolve and update every installed overlay capability in the chosen scope.
--scope Scope root to operate in (global default; see Install layout).
--yes Grant consent when the new version's executable set differs from the previously consented one.
--shared-file As for install — where to splice the (re-derived) hook / MCP fragments.

Behaviour

Re-resolves the capability's recorded source (the source stored in its ledger entry at install time) and, if the resolved version differs, performs an atomic stage-then-swap: the new bundle is fully staged, verified, and validated before the ledger write commits the swap. A crash during staging leaves the previous version intact; a crash after the ledger write leaves the new version intact, and a reconciliation sweep on the next run resolves any orphaned files.

For third-party capabilities, a version whose executable set (hooks, command modules, MCP servers) differs from the previously consented version requires --yes to re-consent before the swap completes; without it the update is aborted and the old version is left fully intact.

--all iterates every ledger entry in the scope and reports a per-capability outcome (upgraded / not_installed / aborted / blocked). Update availability is source-dependent:

Source kind Re-resolution behaviour
<name>@<registry> Registry catalogue query
git (https://…/repo.git#<tag>) Remote tag fetch
npm (npm:@org/pkg@<range>) npm dist-tags / range resolution
tarball (https://…/cap-x.y.z.tgz) Re-fetch of the recorded URL
local (./local/path) Re-read of the recorded filesystem path

remove

Synopsis

gsd capability remove <id> [--purge-data] [--scope global|project]

Arguments

Argument Description
<id> Identifier of the installed overlay capability to remove.

Flags

Flag Description
--purge-data Also remove data files created by the capability at runtime (artefacts under the capability's declared paths that are not part of the install bundle).
--scope Scope root to remove from (global default).

Behaviour

Reads the ledger entry for <id> and removes exactly: the owned files listed in files, and the fragments written into shared files listed in sharedEdits (e.g. hook registrations spliced into a settings.json). Shared files themselves are not deleted; only the capability's marker-isolated fragments are stripped. The ledger entry is removed atomically after all file operations complete.

First-party capabilities (shipped with GSD) cannot be removed via this subcommand — remove rejects a first-party id and points at the product uninstaller (gsd --uninstall).


disable

Synopsis

gsd capability disable <id> [--config-dir <path>] [--runtime <r>] [--scope <s>]

Behaviour

Marks the capability inactive in the runtime activation state — identical to gsd capability set <id> --off. A disabled capability stays on disk; it is excluded from the active surface and contributes no hooks, config keys, or loop extension registrations until re-enabled. This toggles the capability-state layer (the runtime config), not the install ledger. The id must be a capability known to the registry; activation toggling of an installed third-party overlay by id is not yet wired through this path — remove an overlay with gsd capability remove. enable reverses a disable without re-fetching.


enable

Synopsis

gsd capability enable <id> [--config-dir <path>] [--runtime <r>] [--scope <s>]

Behaviour

Clears the inactive flag for <id> in the runtime activation state — identical to gsd capability set <id> --on. On the next GSD invocation the capability is included in the active surface again, subject to its engines.gsd range (an incompatible capability is still skipped with a warning at load time).


list

Synopsis

gsd capability list [--json]

Flags

Flag Description
--json Emit the JSON array explicitly. (In 1.6.0 list always emits JSON; a formatted table is planned.)

Behaviour

Lists capabilities visible to the current session: first-party capabilities (from the registry) plus installed overlay capabilities in both the global and project scopes. Emits a JSON array of descriptors.

Output shape

[
  {
    "id": "string",
    "role": "feature | runtime | null",
    "version": "semver | null",
    "tier": "core | standard | full | null",
    "source": "first-party | <recorded source string>",
    "scope": "first-party | global | project",
    "status": "active | incompatible",
    "title": "string | null"
  }
]

status values:

Value Meaning
active Present and (for overlays) compatible with the running GSD version.
incompatible An overlay whose engines.gsd range does not satisfy the current GSD version; skipped with a warning at load time.

Whether a capability has been turned off via disable is reported by gsd capability state (the activation-state view), not by list.


Planned subcommands

These appear in ADR-1244's command surface but are not implemented in 1.6.0. They are documented here so the surface is explicit; invoking them returns the unknown-subcommand error listing the available set.

Subcommand Intended behaviour
outdated Query each installed overlay's source and report those with a newer version available (--json for machine output). Until it ships, update --all re-resolves every recorded source and reports what changed.

Source specifications

The install subcommand accepts the following source specification forms.

Form Example Adapter
Registry name my-cap@gsd-registry Registry — fetches the capability bundle from the named registry; integrity is populated from the registry catalogue.
Git URL with tag https://github.com/org/repo.git#v1.2.0 Git — clones/fetches at the specified tag; #sha:<40-hex> pins a specific commit.
npm package npm:@org/gsd-capability-foo@^1.0.0 npm — resolves via npm dist-tags / semver range; installs with --ignore-scripts.
Tarball URL https://host/path/cap-x.y.z.tgz Tarball — fetches over HTTPS, verifies SHA-512 when --integrity is supplied.
Local path ./local/path (or an absolute path) Local — copies from the filesystem path. Auto-update detection is not available for this form.

Which source forms are permitted is governed by the capabilities.strict_known_registries policy (see Configuration and the capability trust model): null/absent is permissive, [] is lockdown (no third-party sources), and a host allowlist permits only matching registries. This policy is project-scoped — it is read from the current project's .planning/config.json and applied to installs run in that project regardless of --scope; there is no machine-wide source allowlist. (A present-but-unparseable config fails closed — external installs are blocked until it is fixed.)

All permitted forms pass through the same pipeline: fetch → verify integrity or SHA pin → check engines.gsd → obtain consent → validate → extract → record ledger.


Install layout

Installed overlay capabilities are written under a scope root selected by --scope:

Scope Scope root Bundle path Ledger file
global $GSD_HOME, defaulting to your home directory <root>/.gsd/capabilities/<id>/ <root>/.gsd-capabilities.json
project the current project root <root>/.gsd/capabilities/<id>/ <root>/.gsd-capabilities.json

The ledger is the commit point for installs and upgrades. Its entries record the installed version, original source, integrity hash, owned files, and shared-file edits. A reconciliation sweep on the next GSD run resolves crash orphans (files on disk without a ledger entry, or ledger entries with missing files). These paths are exactly the ones the runtime registry overlay reads when composing installed capabilities, so an install is visible to the loop without any further step.