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
| Piece | Role |
|---|---|
| Resource | Smallest unit: skill, rule, MCP server, hook, agent, command, ... |
| Plugin | Versioned package of resources plus dependencies, with an optional default environment |
| Plugin dependency | Another 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-setupplugin 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.
| Origin | How it gets there | edit / cut / publish / add needs | Origin update |
|---|---|---|---|
authored | plugin create, plugin from-project, plugin fork, conflict scaffolding | Yes | No |
upstream | Materialized from a marketplace, local path, or git plugin install tree | No: error names fork | Yes: plugin check / plugin update |
catalog | plugin pull from an org catalog | No: error names fork | Yes: 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-searchplugin 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_kind | Ref shape | Resolves to |
|---|---|---|
local | base, ./relative/path | A plugin row in the local library |
marketplace | web-search@anthropics | An upstream plugin materialized from the install tree |
git | https://..., git@... | An upstream plugin materialized from the clone |
catalog | acme/default/base | A 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):
| Case | Outcome |
|---|---|
Distinct type:name | Both materialize |
Same type:name, different depth | Nearest to root wins (silent; recorded in the explain trail) |
Same type:name, same depth, identical content | No-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 dependenciesLockfile
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-resolveht 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-versionsOn 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.0The 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.0HarnessTap 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 acmeEnvironment 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 environmentSwitch 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-foundationht 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-nameRegister publish destinations before uploading:
ht plugin catalog register acme/default
ht plugin publish my-setupPortable 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 dependencyplugin.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:
| Shape | When to use |
|---|---|
| Package directory | Repos, review, and any place you want a normal file tree |
.ap.json envelope | Pasting 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-pluginsDefault 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
| Task | Command |
|---|---|
| Create from scratch | plugin create |
| Add resources or deps | plugin edit --add / --remove |
| Fork upstream / catalog | plugin fork |
| Check origin freshness | plugin check |
| Update from origin | plugin update / plugin update --all --yes |
| Diagnose before apply | plugin doctor |
| Explain a resolve decision | plugin why / apply --explain |
| Compare versions | plugin diff |
| Cut a new semver | plugin cut --version |
| Restore a frozen cut | plugin rollback --to |
| Infer from a repo | plugin from-project |
| Apply to a project | apply / apply --update |
| Check lock drift | status --check |
| Apply to home harness | profile use (profile-tagged plugins) |
| Export / import AP package | migrate export --plugin / migrate import |
| Publish / pull cloud | plugin publish / plugin pull |
Related
- Resources: what plugins are made of
- Profiles: machine-wide apply
- Projects: repo-scoped apply
- Command reference:
plugincommand group - Scenario 7: preview and apply
- Scenario 25: stack multiple plugins