Manifest
Each surface.json field with its type and default: identity, adapter, where the CLI runs, Binding, commands, triggers and the hcp metadata object.
surface.json declares who the pack is, how it runs, which credentials it
needs and the closed set of commands it exposes. Start from the complete
example, then look up each field below; policy.json and HARNESS.md have
their own references in Policy file and
Skill file.
Complete example#
{
"id": "ledger",
"name": "Ledger",
"kind": "wrap",
"cliBin": "ledger",
"plane": "runtime",
"transports": ["cli", "json_exec"],
"binding": { "mode": "local", "hub": "local-credential", "family": "ledger" },
"wrap": {
"bin": "./bin/ledger.mjs",
"argv_template": ["{{verb}}"]
},
"verbs": {
"setup.status": { "description": "Report whether the local ledger store is set up. Idempotent.", "cli": ["ledger", "setup", "status"] },
"setup.apply": { "description": "Create the local ledger store non-interactively.", "cli": ["ledger", "setup", "all", "-y"], "policy": "write" },
"connect": { "description": "Link a provider account through the local credential hub.", "cli": ["ledger", "connect", "{{provider}}"], "policy": "write" },
"status": { "description": "Balance, currency, and provider link health.", "cli": ["ledger", "status"] },
"list": { "description": "Recent entries (amount, payee id, memo), newest first.", "cli": ["ledger", "list"] },
"transfer": {
"description": "Pay a saved payee. Refused until a person confirms the amount and payee.",
"cli": ["ledger", "transfer", "{{amount}}", "--to", "{{payee}}", "--memo", "{{memo}}", "--confirm"],
"policy": "write",
"args": {
"type": "object",
"properties": {
"amount": { "type": "string", "description": "Amount in the account currency, as a number", "examples": ["2.80"] },
"payee": { "type": "string", "description": "Payee id from payees", "examples": ["demo-transit"] },
"memo": { "type": "string", "description": "Optional note shown on the entry" }
},
"required": ["amount", "payee"]
},
"title": { "live": ["Paying {{amount}} to {{payee}}", "Paying a payee"], "done": ["Paid {{amount}} to {{payee}}", "Paid a payee"] },
"preview": { "kind": "payment", "title": "Pay {{payee}}", "fields": { "amount": "{{amount}}", "currency": "EUR", "to": "{{payee}}", "memo": "{{memo}}" } }
}
},
"stimulusTriggers": [
{ "ref": "binding.degraded" }
]
}Identity#
idrequiredstringPack id. Lower-case, stable. Equals policy.json surface_id and wire pack_id.
namestringDisplay name for hosts and catalogs.
cliBinstringThe CLI binary name the Skill teaches. The first token of every cli[] array.
plane'runtime' | 'control'default runtimeruntime (default) or control. Control packs default to Binding mothership and are a second pack.
transports('cli' | 'json_exec' | 'mcp')[]Faces the pack projects to. mcp is never required.
skill_urlstring (URL)Hosted copy of HARNESS.md.
Adapter#
kind'wrap' | 'composite' | 'openapi' | 'sense_only'default compositeHow commands run. Alias: adapter_kind.
wrapWrapTargetRequired when kind is wrap.
wrap.binrequiredstringA bare name is a CLI on PATH. A path with a slash is relative to the pack directory and must stay inside it, because install copies only the pack. Must be allow-listed by the sandbox.
wrap.json_flagstring[]default ['--json']Arguments that make the CLI print JSON, appended to every command. Skipped automatically when cli[] already selects JSON (--json fields, -o json, --format json). Use [] for CLIs that print JSON by default. A command can override it with its own json_flag.
wrap.argv_templatestring[]default ['{{verb}}']Fallback argv when a command has no cli[]. {{verb}} expands to the command tail; {{name}} to args.name.
wrap.agent_envRecord<string, string>Environment defaults the bin already understands. Pack-owned.
wrap.workspace.mode'none' | 'project' | 'workspace' | 'ephemeral'default 'none'Where the CLI runs. See Where the CLI runs below.
openapi{ url?: string; path?: string }Required when kind is openapi. One of url or path.
pass_through{ allowedSubcommands: string[]; maxArgv: number }HCP/1.1 bootstrap for wrap. Non-conforming as a destination.
Where the CLI runs#
wrap.workspace.mode sets the wrapped CLI's working directory.
| Mode | Working directory | Use it for |
|---|---|---|
project | The directory the caller is in (HCP_PROJECT_DIR overrides it) | CLIs that work on the project: git, gh, linters, notes and task tools that keep files in the repository. The usual choice for a CLI on PATH; hcpx wrap sets it |
none | The installed pack's own directory | Packs that ship their own script in ./bin and need nothing from the project |
workspace | A private directory for this pack, with its own HOME | CLIs that must not touch the project, or keep state that belongs to the user, outside the repository |
ephemeral | A fresh directory for each command | One-shot converters that read workspace: files and write an output |
RFC 0005
proposes project, and the reference runtime implements it. A runtime with no
project, such as a hosted one, refuses these commands with
code: "no_project", and hcpx check warns (M023) when a CLI on PATH would
run in the pack directory.
Binding#
binding.mode'public' | 'user' | 'local' | 'mothership' | 'mock'Credential mode. One per pack.
binding.hub'keyring' | 'local-credential' | 'public' | 'none'Which kind of hub mints. Runtime detail; the pack names the expectation.
binding.familystringPacks sharing a family share one consent and union their scopes.
binding.connect_provider_idstringProvider hint for the connect flow.
Commands#
verbs maps each command name, the verb_key on the wire, to its declaration.
descriptionstringOne sentence. Shown in host tool catalogs.
clistring[]Space-separated CLI form. First token is the bin. {{name}} placeholders map to args. --flag={{name}} is supported. A missing optional value drops its token and the flag right before it.
argsobjectJSON Schema for args: properties with a description and examples, and required. The runtime refuses unknown and missing arguments before anything runs, hcpx commands shows the first example of each in a ready-to-run line, and "x-position": n lets a bare word of a command line fill the argument.
json_flagstring[]Replaces wrap.json_flag for this command. [] appends nothing.
title{ live?; done? }What a host calls a step of this command: live while it runs, done after. Each is a template or a list of templates with {{argument}} placeholders ({{result.…}} in done), tried in order. See Command, Titles.
policy'read' | 'write'Legacy hint: read or write. policy.json is authoritative.
preview{ kind; title?; fields? }How a person sees this command when it asks for approval: a kind (message, payment, event, removal, publish, record, fields) and field templates filled from args. See Access, What the card shows.
Name 5 to 12 commands; fewer is fine for a public read pack, and more means
two packs. The set is closed, so every command is named and there is no run
and no exec --. Every command has a Policy row, and lint enforces coverage
both ways. The runtime substitutes {{name}} from args, appends the JSON flag
(--json unless json_flag says otherwise) and never builds a shell string, as
CLI projection describes.
Triggers#
stimulusTriggers{ ref: string }[]Sense declarations. Each ref must have a policy.json stimuli row. Only when the upstream system pushes.
Protocol metadata#
The manifest may carry an hcp metadata object, or the runtime derives one,
and either way it is strictly validated.
{
"hcp": 1,
"pack_id": "ledger",
"adapter_kind": "wrap",
"binding": { "mode": "local", "hub": "local-credential" },
"wrap": { "bin": "./bin/ledger.mjs" },
"plane": "runtime",
"transports": ["cli", "json_exec"]
}hcp is the protocol affinity (1 for HCP/1.x), pack_id must equal id,
and unknown fields fail validation.
Lint checks#
Lint rejects a manifest whose id differs from the policy.json surface_id,
a kind: wrap without wrap.bin or without named commands or pass_through,
a plane: control pack whose Binding is not mothership, a kind: openapi
without openapi, and a kind: sense_only without at least one trigger.
Testing lists the exact messages.