.md

Wrap a CLI

Bring a CLI with a login, setup, several accounts or an admin face under HCP: the setup family, connect, doctor, gated writes, entities, the control split.

On this page

Most packs enter HCP as adapter: wrap, which names the commands a tool already has and gives each a rule without changing the tool. This page covers the CLI that has grown a login, a setup step, several accounts or an admin face, starting from the draft hcpx wrap writes.

Before you start#

The CLI needs these habits first:

HabitWhy HCP needs it
--json (or JSON-default) outputAgents parse
Non-zero exit on failureOrchestrators branch
A doctor or status commandHealth without minting
Non-interactive flags (-y) on anything that would promptAgents hang
Named subcommandsPolicy can only cover things with names

A capability that exists only as an MCP server needs a CLI first.

Start from a draft#

Terminal
hcpx wrap mail --class gated-write --json

hcpx wrap reads mail --help with no TTY and drafts the three files in packs/mail: one command per usage line it can parse, a Policy row for each, confirm and an approval preview on the ones that sound risky, and wrap.workspace.mode: project so the CLI runs in the caller's project. Review what its next list names, then run hcpx check ./packs/mail --init-evals and hcpx check ./packs/mail --probe --agent until no error is left. The CLI reference lists the options.

Map the lifecycle to command families#

HCP turns the habits production CLIs share into families with a default Policy.

Setup: read and write (HCP/1.1)#

Terminal
mail setup status --json     # read  — idempotent; detected hosts, Skill merge state
mail setup all -y --json     # write — apply; -y default on the agent path
mail setup update --json     # write — refresh Skill after a pack upgrade

The pack keys are setup.status, setup.apply and setup.update. All three are idempotent, and the agent path never opens a wizard.

Health and discovery: read#

Terminal
mail doctor --json           # auth edge, binaries, config, capabilities
mail status --json           # narrower than doctor
mail capabilities --json     # feature discovery before send

Doctor reports Binding health, must not mint a long-lived secret as a side effect, and exits 0 when healthy, 2 on a warning and 1 on failure.

Validate before mutate: read#

Terminal
pulse validate --file ./envelope.json --json

Validating a prospective write is a read.

Terminal
mail connect --json
finance connect bank --entity acme-eu --json

Connect establishes Binding. It is operation: write but needs the pack.connect capability instead of verb.exec.write, and it belongs under Setup in the Skill so agents run it once.

Domain ops#

ShapePolicy
list get recv briefread
send create sync categorizewrite
transfer delete publishwrite + confirm

Dry-run#

Offer --dry-run on expensive or destructive writes only when the system can preview without persisting anything. Policy stays write.

Declare the wrap#

surface.json (fragment)
"kind": "wrap",
"cliBin": "mail",
"binding": { "mode": "user", "hub": "keyring", "family": "office" },
"wrap": { "bin": "mail", "workspace": { "mode": "project" }, "agent_env": { "MAIL_OUTPUT": "json" } },
"verbs": {
  "setup.status": { "cli": ["mail", "setup", "status"] },
  "setup.apply":  { "cli": ["mail", "setup", "all", "-y"], "policy": "write" },
  "connect":      { "cli": ["mail", "connect"], "policy": "write" },
  "doctor":       { "cli": ["mail", "doctor"] },
  "list":         { "cli": ["mail", "list", "--folder={{folder}}"] },
  "get":          { "cli": ["mail", "get", "{{id}}"] },
  "send":         { "cli": ["mail", "send", "--to", "{{to}}", "--subject", "{{subject}}", "--confirm"], "policy": "write" }
}

The runtime renders cli[] with args, appends the JSON flag and spawns the allow-listed bin in the caller's project without a shell. A CLI that selects JSON its own way (-o json, --json name,url, or JSON by default) sets json_flag.

Declare Binding mode explicitly#

ModeWhen
userThe CLI acts with a delegated user credential
localThe CLI keeps credentials in a machine-local store
mothershipThe CLI is an operator or fleet tool; see Control
publicNo credential

Never infer the mode from whatever token sits in the environment. Mock is explicit in the envelope, and an env-flag mock fails closed in sandbox.

Multi-entity and multi-tenant arguments#

CLIs often take --entity, --workspace or --account. Each maps to its own home in HCP, and pack arguments alone never select the tenant.

CLI concernHCP home
Which organisationRuntime tenant, from the Principal's session, never a pack arg
Which shared projectRuntime project, when privacy.land: brain
Which local workspace directoryLand local (cwd jail), which is not a tenant
Which linked account or mailboxBinding + command args (--folder, account id)
Which domain entityCommand args, still scoped by tenant. --entity only when Binding mints per entity

Control plane versus runtime#

Keep the split many ecosystems make between an admin CLI (link, provision) and a user CLI (send, receive).

ApproachWhen
Two packsDifferent Binding modes, or very different RBAC
One pack, two namespacesSame Binding family; mark connect.* clearly

Different Binding modes always mean two packs, and an operator bin must never be reachable from an end-user Principal just because it is on PATH.

Bootstrap with pass-through, then promote#

Pass-through gets a binary under Policy in an hour:

JSON
"pass_through": { "allowedSubcommands": ["status", "list"], "maxArgv": 6 }

Then promote the hot path to named commands with Policy rows and delete the allow-list, because a wrap that stays pass-through does not conform.

Anti-patterns#

Anti-patternDo instead
Secrets in HARNESS.md or the manifestBinding mint
Interactive --login on the agent pathSeparate human connect; Sense binding.degraded
One mega-command runNamed commands with Policy
Different auth for UI vs agentShared Binding
Teaching an MCP tool list as the packPack commands; MCP is an optional export
Host-absolute paths in Skill stepsPortable bins; runtime documents env

Proof#

Terminal
hcpx check ./packs/mail --probe --strict   # quality rules, live probes, eval cases
hcpx lint ./packs/mail --json
hcpx install ./packs/mail --json && hcpx activate mail --json
hcpx exec mail list --json -- folder=inbox                 # one read
hcpx exec mail send --json -- to=a@b.c subject=Hi           # → confirm required
hcpx exec mail send --json -- to=a@b.c subject=Hi confirm=true   # mock/paper

Testing explains what each step proves.