---
title: "Wrap a CLI"
description: "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."
canonical: https://harnesscontextprotocol.io/docs/packs/wrap-a-cli
---

# 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.

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:

| Habit | Why HCP needs it |
|-------|------------------|
| `--json` (or JSON-default) output | Agents parse |
| Non-zero exit on failure | Orchestrators branch |
| A `doctor` or `status` command | Health without minting |
| Non-interactive flags (`-y`) on anything that would prompt | Agents hang |
| Named subcommands | Policy can only cover things with names |

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

## Start from a draft

```bash
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](/docs/reference/cli) 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)

```bash
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`

```bash
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`

```bash
pulse validate --file ./envelope.json --json
```

Validating a prospective write is a `read`.

### Connect or link: `write`, gated by `pack.connect`

```bash
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

| Shape | Policy |
|-------|--------|
| `list` `get` `recv` `brief` | `read` |
| `send` `create` `sync` `categorize` | `write` |
| `transfer` `delete` `publish` | `write` + `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

```json title="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`](/docs/concepts/command#the-json-flag).

## Declare Binding mode explicitly

| Mode | When |
|------|------|
| `user` | The CLI acts with a delegated user credential |
| `local` | The CLI keeps credentials in a machine-local store |
| `mothership` | The CLI is an operator or fleet tool; see [Control](#control-plane-versus-runtime) |
| `public` | No 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 concern | HCP home |
|-------------|----------|
| Which organisation | Runtime tenant, from the Principal's session, never a pack arg |
| Which shared project | Runtime project, when `privacy.land: brain` |
| Which local workspace directory | Land `local` (cwd jail), which is not a tenant |
| Which linked account or mailbox | Binding + command args (`--folder`, account id) |
| Which domain entity | Command 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).

| Approach | When |
|----------|------|
| Two packs | Different Binding modes, or very different RBAC |
| One pack, two namespaces | Same 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-pattern | Do instead |
|--------------|------------|
| Secrets in `HARNESS.md` or the manifest | Binding mint |
| Interactive `--login` on the agent path | Separate human `connect`; Sense `binding.degraded` |
| One mega-command `run` | Named commands with Policy |
| Different auth for UI vs agent | Shared Binding |
| Teaching an MCP tool list as the pack | Pack commands; MCP is an optional export |
| Host-absolute paths in Skill steps | Portable bins; runtime documents env |

## Proof

```bash
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](/docs/packs/check#testing) explains what each step proves.

## Related

- [CLI projection](/docs/concepts/command#cli-projection)
- [Pack classes](/docs/packs/best-practices#pack-classes)
- [Adapter](/docs/concepts/adapter)

## Sitemap

- [Documentation](https://harnesscontextprotocol.io/docs)
- [Packs](https://harnesscontextprotocol.io/packs)
- [Glossary](https://harnesscontextprotocol.io/docs/reference/glossary)
- [llms.txt](https://harnesscontextprotocol.io/llms.txt)
- [llms-full.txt](https://harnesscontextprotocol.io/llms-full.txt)
- [OpenAPI](https://harnesscontextprotocol.io/openapi.json)
- [MCP](https://harnesscontextprotocol.io/mcp)
