Concepts

Plugins

Versioned context packages you create, diff, export, publish, and apply.

A plugin is HarnessTap's versioned context package: the unit you create, diff, export, publish, and apply. Plugins compose resources, plugin dependencies (marketplace, local, git, or catalog), and an optional default environment.

What a plugin contains

PieceRole
ResourceSmallest unit: skill, rule, MCP server, hook, agent, command, ...
PluginVersioned package of resources plus dependencies, with an optional default environment
Plugin dependencyAnother plugin this one requires: plugin:ref@source with a version constraint

Create and edit plugins locally:

ht plugin create my-setup --description "Shared project assistant setup"
ht plugin edit my-setup --add research-helper --type skill
ht plugin edit my-setup --add plugin:formatter@my-marketplace --version "^2.1.0"
ht plugin edit team-stack --add plugin:shared-baseline --version "^1.2.0"
ht plugin show my-setup

plugin doctor checks for duplicate resources, empty definition, or invalid plugin metadata. plugin diff compares plugin metadata and contents against another plugin or a TOML bundle. plugin from-project scans a repository and turns imported resources into a new plugin.

Provenance (origin)

Every local plugin row has an origin. Capabilities vary by origin; ht plugin list shows an Origin column (JSON includes origin), and refusal errors name the fix instead of only failing.

OriginHow it gets thereedit / cut / publish / add needsOrigin update
authoredplugin create, plugin from-project, plugin fork, conflict scaffoldingYesNo
upstreamMaterialized from a marketplace, local path, or git plugin install treeNo: error names forkYes: plugin check / plugin update
catalogplugin pull from an org catalogNo: error names forkYes: plugin check / plugin update

plugin check and plugin update refresh library working heads from those origins using git hashes (or catalog version/digest). resource sync is a different path: it still reads harness install trees, not this origin fingerprint.

Upstream and catalog plugins are read-only graph nodes until you fork them:

ht plugin fork web-search
ht plugin fork web-search --as my-web-search
ht plugin edit my-web-search

plugin fork copies an upstream or catalog plugin into a new authored plugin (default name <name>-fork). The source row is left untouched; resource rows are shared until you attach or detach differently.

Plugin dependencies

Plugin pins and nested plugin refs are one type: a plugin attachment with provenance metadata. Add dependencies with plugin:ref@source (or a bare local name):

source_kindRef shapeResolves to
localbase, ./relative/pathA plugin row in the local library
marketplaceweb-search@anthropicsAn upstream plugin materialized from the install tree
githttps://..., git@...An upstream plugin materialized from the clone
catalogacme/default/baseA catalog plugin pulled from the cloud
ht plugin edit my-setup --add plugin:formatter@my-marketplace --version "^2.1.0"
ht plugin edit my-setup --add plugin:shared-baseline --version "^1.2.0"
ht plugin edit my-setup --add plugin:https://github.com/acme/plugin.git --version "^1.0.0"
ht plugin edit my-setup --add plugin:acme/default/base --version "^2.0.0"

Legacy selectors plugin_pin:... and plugin:... still resolve and print a notice naming the new plugin: spelling.

marketplace add reads every manifest in the repo: .claude-plugin/marketplace.json, .cursor-plugin/marketplace.json, .github/plugin/marketplace.json (Copilot), .agents/plugins/marketplace.json (Codex source.path, including ./ for the repository root), and a root marketplace.json. Each plugin is labeled with that manifest's flavor. When --platform is omitted, registry platforms are set from the manifests that were found. Codex stays a catalog flavor and is shown on the plugin row. A plugin root whose plugin.json uses the Agent Plugins 1.0.0 $schema is classified as an agent plugin. A source path that leaves the repository is omitted, with a warning. ht resource sync plugin:<name>@<marketplace> installs that pin. A second sync of the same tree is unchanged. A missing marketplace plugin names that command.

Attaching a marketplace plugin whose source path leaves the repository is rejected. No pin is recorded.

Composition and resolution

Plugins depend on other plugins. ht apply does not walk an ordered stack: it resolves the whole dependency graph, then materializes one coherent set of resources.

Pass 1: one version per plugin name. Every constraint on a name is collected and intersected. The selected version is the highest available version that satisfies every constraint. A version or override declared by the plugin you applied ends mediation for that name. An empty intersection is a hard error that names both requirers and their dependency paths.

Pass 2: one resource per type:name. Resources from the resolved set are flattened with each resource's depth (the root's own resources are depth 0):

CaseOutcome
Distinct type:nameBoth materialize
Same type:name, different depthNearest to root wins (silent; recorded in the explain trail)
Same type:name, same depth, identical contentNo-op
Same type:name, same depth, differing content, set-like types (skill, rule, agent, command, hook, mcp_server)Last-declared wins with a warning
Same type:name, same depth, differing content, singleton types (instruction, model_config, permission, env_var)Error: fix with an override

Merge semantics are replace-only: the winner replaces the resource whole.

ht apply my-setup --project .
ht apply team-base team-overrides   # ephemeral root with both as dependencies

Lockfile

Successful project applies write apm.lock.yaml. Check it in. It records the resolved plugin name → version set (plus integrity metadata) so re-applies reuse the same resolution until you ask otherwise:

ht apply my-setup            # reuse lock when consistent with the manifest
ht apply my-setup --update   # ignore the lock and re-resolve

ht status --check reports lock drift (manifest and lock disagree) alongside ordinary project drift.

Inspecting decisions

ht apply my-setup --explain
ht plugin why base
ht plugin why skill:deploy

--explain prints the resolution trail: selected versions with the constraints that produced them, and every contested resource with winner, loser, and reason. plugin why answers the same questions against the lockfile (or --root when you want a fresh resolve).

Plugin dependencies and version policy

Marketplace and other upstream dependencies attach like any other composition item. Sync refreshes upstream or catalog plugins only: authored plugins have nothing to sync from.

ht plugin edit my-setup --add plugin:formatter@my-marketplace --version "^2.1.0"
ht plugin edit my-setup --add plugin:formatter@my-marketplace --sync   # eager sync after add
ht resource sync plugin:formatter@my-marketplace
ht apply my-setup --project . --strict-plugin-versions

On apply, HarnessTap compares dependency version constraints to library resolved_version values:

  • Default: warn on mismatch
  • --strict-plugin-versions: fail with exit code 2
  • --ignore-plugin-versions: skip validation
  • --sync-plugins: refresh upstream plugin resources before materialize

Plugin install and sync providers exist for Claude Code and Cursor. Plugin-source scan covers .claude-plugin/, .cursor-plugin/, .codex-plugin/, and .github/plugin/ layouts.

Refresh policy for marketplace metadata is configured in ~/.harnesstap/config.jsonc:

{
  "plugins": {
    "refreshMaxAgeHours": 24,
    "marketplaceRefreshMaxAgeMinutes": 60
  }
}

marketplaceRefreshMaxAgeMinutes (default 60) is how long Discover and managed marketplace catalogs reuse a last refresh before git-fetching again. refreshMaxAgeHours is for plugin origin check/update.

resource sync uses cached metadata unless it is stale; pass --force to refresh regardless.

Upstream updates are configured under updates in ~/.harnesstap/config.jsonc:

{
  "updates": {
    "mode": "notify",      // off, notify (default) or install
    "frequency": "daily",  // hourly, daily (default) or weekly
    "prompted": false      // true once the Keep plugins up to date? prompt was answered
  }
}

The Desktop sidecar checks upstreams when a check is due under frequency, counted from the newest stored check, so restarting the app does not check again. off stops remote checks only: synced profiles still follow their parent, and ht plugin check still works. install also installs, for pulled catalog plugins only, the newest minor or patch inside the caret range. It never installs a major version, a prerelease, or a version outside the range. It skips a plugin whose content no longer matches its published version (local edits), a separate copy, and an update that would rewrite a resource another plugin also uses. The installed version stays as a frozen cut next to the new one. When the active profile uses the plugin, it is re-applied through the normal apply path only if its harness files are clean, and the apply stops on any conflict. One plugin update runs at a time: a scheduled run is skipped while ht plugin update or a Desktop Update is running, and ht plugin update waits for a scheduled run to finish. HARNESSTAP_UPDATE_CHECK=off turns the background schedule off.

Version cuts and dirty heads

Each plugin name has a working head: the latest editable version. Edits after a cut (for example plugin edit --add) mark the head dirty without changing its semver. Dirty heads are shown with a trailing * in human output (plugin list, plugin show): for example 1.2.0*: while JSON output keeps the real version string and a separate dirty flag.

Cut a new semver to freeze the current composition and advance the head:

ht plugin cut my-setup --version 1.3.0

The previous head is frozen in place (copy-on-write); the new head starts clean at the requested version. Frozen versions cannot be edited or cut again.

List retained versions and restore a frozen snapshot onto the working head (same semver, marked dirty). This does not apply the plugin:

ht plugin versions my-setup
ht plugin rollback my-setup --to 1.0.0

HarnessTap keeps at most pluginVersionHistoryLimit versions per plugin name (head included). Oldest frozen versions are pruned on cut when over the limit. Configure in ~/.harnesstap/config.jsonc (default 10):

{
  "pluginVersionHistoryLimit": 10
}

Sharing rules: export, migrate export --plugin, and plugin publish refuse dirty heads so bundles and catalog uploads always reflect a cut version. edit, cut, publish, and adding needs also require an authored origin: fork upstream or catalog plugins first. Cut first, or pass --version <semver> on plugin publish to cut and publish in one step:

ht plugin publish my-setup --version 1.3.0 --account acme

Environment cascade

Environments carry how values (env vars, model config, permissions, secret refs). During apply, values resolve through a cascade: last wins:

home active environment ◂ plugin default environment

Switch the home active environment to change runtime values without rebuilding the plugin stack. Bind a default environment to a plugin with plugin edit --environment <name>.

Catalog baselines

Starter plugins such as engineering-foundation and frontend-engineer live in the HarnessTap Cloud public catalog, not inside the npm package.

ht plugin list --search foundation --remote-only
ht apply engineering-foundation

ht apply <name> resolves bare names against the public catalog (and any orgs or libraries you have connected). Missing dependencies of a catalog package: including those declared source: "local": are fetched from that same catalog at the pinned version. Use plugin pull to cache a bundle locally for offline work.

To opt out of anonymous public catalog lookups:

// ~/.harnesstap/config.jsonc
{ "catalog": { "publicCatalog": false } }

Or export HARNESSTAP_PUBLIC_CATALOG=0.

Connect additional org catalogs explicitly:

ht auth login
ht plugin catalog connect <org>/<library>
ht plugin pull org/plugin-name

Register publish destinations before uploading:

ht plugin catalog register acme/default
ht plugin publish my-setup

Portable format

Plugins travel as Agent Plugins 1.0 packages: the only portable format. There is no second transport.

my-plugin/
├── plugin.json               # AP manifest: core fields + extensions
├── skills/
│   └── deploy/
│       ├── SKILL.md
│       ├── scripts/run.sh
│       └── reference/notes.md
├── mcp.json                  # optional, AP standard
└── com.harnesstap/           # everything AP 1.0 does not model
    ├── instructions/<name>.md
    ├── rules/<name>.md
    ├── agents/<name>.md
    ├── commands/<name>.md
    ├── hooks.toml
    ├── permissions.toml
    ├── env.toml              # keys and references only, never secret values
    ├── model.toml
    ├── claude.toml
    └── embedded/<name>/      # nested AP package per embed-on-export dependency

plugin.json carries only Agent Plugins core fields at the top level ($schema, name, version, ...). Catalog download defaults a missing or empty $schema; publish and local authoring still require it. HarnessTap-specific data: dependencies, overrides, profile flag, needs, and pointers into com.harnesstap/: lives under extensions["com.harnesstap"]. Non-HarnessTap clients load skills/ and mcp.json and ignore the namespace.

Two shapes, same content:

ShapeWhen to use
Package directoryRepos, review, and any place you want a normal file tree
.ap.json envelopePasting into a message or shipping one file (--single-file)
ht migrate export ./my-setup --plugin my-setup
ht migrate export ./my-setup.ap.json --plugin my-setup --single-file
ht migrate import ./my-setup
ht migrate import ./my-setup.ap.json
ht migrate export ./team --plugin my-setup --embed-plugins

Default export is a directory named after the plugin. Pass --single-file for a plugin.ap.json envelope (urn:harnesstap:ap-package:v1) whose files map matches the directory. --embed-plugins inlines dependency trees under com.harnesstap/embedded/.

For a full workspace handoff (plugins, environments, harness preferences, config), use ht migrate export --workspace with a .tar.gz archive: see Scenario 28. Environments are machine-local and are not independently exportable.

For multiplayer distribution, plugin publish and plugin pull move that same Agent Plugins package over HarnessTap Cloud: the bytes on the wire equal what migrate export --single-file writes. There is no second cloud transport. Publish targets /api/plugins; pull downloads from the catalog .../versions/:version/package route. See Cloud connection.

Requests carry X-HarnessTap-CLI-Version and X-HarnessTap-API-Version. A CLI below the cloud's minimum floor fails with an upgrade instruction (426, naming npm install -g harnesstap@latest) rather than a parse error on an unfamiliar payload.

plugin.json $schema is https://agent-plugins.org/schemas/1.0.0/plugin.schema.json, the Agent Plugins 1.0.0 identifier. Package export, ht pack, and plugin publish always write it. Packages that carry the identifier earlier HarnessTap builds wrote (https://agentplugins.org/schema/v1/plugin.schema.json) still import: HarnessTap reads them as 1.0.0 and leaves the file as it is. Other agent-plugins.org schema versions are rejected as unsupported.

mcp.json declares $schema https://agent-plugins.org/schemas/1.0.0/mcp.schema.json and an explicit type for each server: stdio for command servers, streamable-http for HTTP servers (sse for SSE servers). Server settings that shape cannot hold, such as env on an HTTP server, go to com.harnesstap/mcp.toml so HarnessTap import restores them. ht pack converts a project .mcp.json into that shape in the packed copy and leaves the project file as it is.

Deprecated ht layer alias

ht layer ... remains a hidden alias for ht plugin ... for one release and prints a rename notice. Prefer ht plugin and top-level ht apply.

Plugin workflows at a glance

TaskCommand
Create from scratchplugin create
Add resources or depsplugin edit --add / --remove
Fork upstream / catalogplugin fork
Check origin freshnessplugin check
Update from originplugin update / plugin update --all --yes
Diagnose before applyplugin doctor
Explain a resolve decisionplugin why / apply --explain
Compare versionsplugin diff
Cut a new semverplugin cut --version
Restore a frozen cutplugin rollback --to
Infer from a repoplugin from-project
Apply to a projectapply / apply --update
Check lock driftstatus --check
Apply to home harnessprofile use (profile-tagged plugins)
Export / import AP packagemigrate export --plugin / migrate import
Publish / pull cloudplugin publish / plugin pull