.md

Manifest

Each surface.json field with its type and default: identity, adapter, where the CLI runs, Binding, commands, triggers and the hcp metadata object.

On this page

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#

surface.json
{
  "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#

idrequired
string

Pack id. Lower-case, stable. Equals policy.json surface_id and wire pack_id.

name
string

Display name for hosts and catalogs.

cliBin
string

The CLI binary name the Skill teaches. The first token of every cli[] array.

plane
'runtime' | 'control'default runtime

runtime (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_url
string (URL)

Hosted copy of HARNESS.md.

Adapter#

kind
'wrap' | 'composite' | 'openapi' | 'sense_only'default composite

How commands run. Alias: adapter_kind.

wrap
WrapTarget

Required when kind is wrap.

wrap.binrequired
string

A 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_flag
string[]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_template
string[]default ['{{verb}}']

Fallback argv when a command has no cli[]. {{verb}} expands to the command tail; {{name}} to args.name.

wrap.agent_env
Record<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.

ModeWorking directoryUse it for
projectThe 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
noneThe installed pack's own directoryPacks that ship their own script in ./bin and need nothing from the project
workspaceA private directory for this pack, with its own HOMECLIs that must not touch the project, or keep state that belongs to the user, outside the repository
ephemeralA fresh directory for each commandOne-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.family
string

Packs sharing a family share one consent and union their scopes.

binding.connect_provider_id
string

Provider hint for the connect flow.

Commands#

verbs maps each command name, the verb_key on the wire, to its declaration.

description
string

One sentence. Shown in host tool catalogs.

cli
string[]

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.

args
object

JSON 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_flag
string[]

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.

JSON
{
  "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.