---
title: "Command"
description: "What a command is made of, how to name it, how its CLI face renders from JSON args, and the rules that keep it safe for an agent to call."
canonical: https://harnesscontextprotocol.io/docs/concepts/command
---

# Command

> What a command is made of, how to name it, how its CLI face renders from JSON args, and the rules that keep it safe for an agent to call.

A command is a named operation a pack declares and the unit of Execute. Its
CLI face and its JSON face are the same command under one Policy row, one
Binding and one Adapter, so the name you give it holds on every surface a host
shows. Give a pack 5 to 12 commands and write each CLI form to the
[agent-safe rules](#agent-safe-rules).

## Anatomy

The wire field `verb_key` and the manifest map `verbs` both mean command, since
HCP has no separate verb ontology.

| Part | Where | Example |
|------|-------|---------|
| Stable name | manifest `verbs` key; wire `verb_key` | `plan`, `mail.list`, `setup.apply` |
| Description | manifest | "Time-first options between two places." |
| CLI form | manifest `cli[]` | `["transit", "plan", "{{from}}", "{{to}}"]` |
| Argument placeholders | `{{name}}` in `cli[]` | map to JSON `args` |
| Title (optional) | manifest `title` | "Searched the Inbox for “Laura”" |
| Approval view (optional) | manifest `preview` | The card a person sees before a gated write |
| [Policy](/docs/concepts/policy) row | `policy.json` | `operation`, `privacy.land`, `confirm` |
| Adapter path | pack `kind` | spawn the wrap bin, or call in-process |

## The twin face

<Tabs items={['CLI face', 'JSON face']}>
<Tab value="CLI face">

```bash
transit plan "A" "B" --json
```

</Tab>
<Tab value="JSON face">

```json
{ "pack_id": "transit", "verb_key": "plan", "args": { "from": "A", "to": "B" } }
```

</Tab>
</Tabs>

Only the [Invoker](/docs/runtimes/execution#invoker) differs, which makes the
twin face the command's identity ([L2](/docs/introduction/design-principles)).
`hcpx exec` sends the JSON envelope from the command line for runtime operators
and tests, while Skills teach the domain bin. Its `k=v` pairs after `--` become
`args`, and `confirm=true` or `apply=true` satisfies the confirm gate.

```bash
hcpx exec transit plan --json -- from=A to=B
```

## Naming

Names are lower-case, dot-separated when a family is useful, and stable.

| Pattern | Use |
|---------|-----|
| `find` `plan` `show` `status` `follow` | Public read set |
| `setup.status` `setup.apply` `setup.update` | Installer family (HCP/1.1) |
| `connect` | Establish Binding; needs `pack.connect` |
| `doctor` `capabilities` `validate` | Health, discovery, and pre-flight checks; always `read` |
| `list` `get` `send` `create` | Domain ops on a linked channel |

Dots group names without creating a hierarchy, so `mail.list` is one command.
A pack that needs more than 12 commands is usually two packs.

Each name is identical in every projection, and lint fails on drift:

| Face | Name |
|------|------|
| CLI | `mail list` (space grammar from `cli[]`) |
| JSON | `"verb_key": "mail.list"` |
| Skill | `` `mail.list` `` in the hot-path table |
| Host tool | `hcpx exec mail mail.list` in a shell, `exec mail mail.list` through the one `hcp` tool, or `mail__mail_list` in an in-process loop (pack id, two underscores, `verb_key` with dots as underscores) |

The CLI face may carry aliases in other languages, declared as manifest
metadata and never as separate commands, while the JSON `verb_key` stays
English.

## Titles

A host shows a person each step of a run, and `title` gives the words: `live`
while the command runs and `done` once it ran. The pack declares them because a
command line shows only flags and ids, and model text uses words the pack never
said.

```json title="surface.json (fragment)"
"find": {
  "description": "Mail in the Inbox matching a query or a sender.",
  "cli": ["mail", "find", "--query", "{{q}}", "--from", "{{from}}"],
  "title": {
    "live": ["Searching the Inbox for “{{q}}”", "Searching the Inbox"],
    "done": ["Searched the Inbox for “{{q}}”", "Searched the Inbox for mail from {{from}}", "Searched the Inbox"]
  }
}
```

Each form is one template or a list tried in order. The first template whose
placeholders all have a value wins, so a title never shows an empty slot, and
the last one in each list should have no placeholders, so a title always shows.
`{{name}}` is a call argument, and `{{result.a.b}}` is a field of the JSON
result that works only in `done`, as in `"Read {{result.mail.subject}}"`.
Values are flattened to one line and cut to 60 characters, and a list of
strings is joined with commas.

| Call | `live` | `done` |
|------|--------|--------|
| `q=Laura` | Searching the Inbox for “Laura” | Searched the Inbox for “Laura” |
| `from=anna@acme.example` | Searching the Inbox | Searched the Inbox for mail from anna@acme.example |
| no arguments | Searching the Inbox | Searched the Inbox |

The SDK returns the words: `commandTitle(pack, verb_key, args)` while the
command runs and `commandTitle(pack, verb_key, args, result)` once it has a
result, or `null` when the pack declares no title and the host uses its own.
`hcpx check` reports a title that names an unknown argument, uses a result in
`live`, or has no plain fallback (M026).

[RFC 0007](https://github.com/harnesscontextprotocol/hcp-protocol/blob/master/rfcs/0007-host-projection.md)
proposes titles and the reference SDK implements them. It asks hosts to show a
declared title in place of the command line, keep the exact command one action
away, and never replace the title with text the model wrote.

## CLI projection

Every command projects to a CLI form an agent can run safely
([Projections](/docs/specification/projections)):

```text
<bin> <space…> [--flags] [--json]
```

<Tabs items={['CLI', 'JSON exec']}>
<Tab value="CLI">

```bash
mail list --folder=inbox --json
mail send --to user@example.com --subject Hello --confirm --json
```

</Tab>
<Tab value="JSON exec">

```json
{ "pack_id": "mail", "verb_key": "mail.list", "args": { "folder": "inbox" } }
{ "pack_id": "mail", "verb_key": "mail.send", "args": { "to": "user@example.com", "subject": "Hello", "confirm": true } }
```

</Tab>
</Tabs>

### How `cli[]` renders

The runtime substitutes placeholders from `args`, drops empty ones, and appends
the JSON flag.

| Declared | Args | Rendered |
|----------|------|----------|
| `["transit", "plan", "{{from}}", "{{to}}"]` | `{ from: "A", to: "B" }` | `transit plan A B --json` |
| `["mail", "list", "--folder={{folder}}"]` | `{ folder: "inbox" }` | `mail list --folder=inbox --json` |
| `["ledger", "transfer", "{{amount}}", "--to", "{{payee}}", "--memo", "{{memo}}", "--confirm"]` | `{ amount: "2.80", payee: "demo-transit", confirm: true }` | `ledger transfer 2.80 --to demo-transit --confirm --json` |
| `["ledger", "setup", "all", "-y"]` | `{}` | `ledger setup all -y --json` |

The first token is the bin, and the arguments go to `spawn` as an array with no
shell in between, so a value with spaces stays one argument. A missing optional
value drops its token and the flag right before it, so an empty `memo` leaves
no dangling `--memo`. Without `cli[]` the runtime falls back to
`wrap.argv_template`, where `{{verb}}` expands to the command tail (`mail.list`
becomes `list`).

### The JSON flag

The runtime appends `--json` unless the pack or the command selects JSON
another way.

| Declared | Appended |
|----------|----------|
| Nothing | `--json` |
| `wrap.json_flag: ["-o", "json"]` | `-o json` on every command |
| A command's own `json_flag` | That value instead, for that command only; `[]` appends nothing, for CLIs that print JSON by default |
| `cli[]` already selects JSON (`--json`, `--json=…`, `--json name,url`, or `-o`, `--output`, `--format`, `-f` followed by `json`) and the command has no `json_flag` | Nothing |

The JSON arguments always come last, and `hcpx check` probes render commands
the same way.
[RFC 0004](https://github.com/harnesscontextprotocol/hcp-protocol/blob/master/rfcs/0004-wrap-json-flag.md)
proposes the JSON flag and the reference runtime implements it.

### Exit codes

| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | Failure |
| `2` | Warning; used by `doctor` / `status` for degraded but usable |
| `127` | Bin not found or not allow-listed (runtime-side) |

A non-zero exit becomes `{ "ok": false, "error": <stderr or "exit N"> }` on the
JSON face.

### Agent-safe rules

| Rule | Why |
|------|-----|
| `--json` or JSON-by-default stdout | Agents parse; humans still read |
| Non-zero exit on failure | Orchestrators branch correctly |
| No interactive prompts on the agent path | Agents hang or invent answers |
| Idempotent reads | Retries are safe |
| Explicit writes: `operation: write` and clear names | No surprise side effects |
| `--confirm` / `--apply` on gated writes, mirroring Policy `confirm: true` | The gate is visible on both faces |
| Stable space grammar | Skills stay teachable across versions |
| `doctor` / `capabilities` for discovery | Beats hidden flags |

## What is not a command

A mega-command such as `run` or `exec -- <argv>` forwards arbitrary arguments
and behaves as a shell, which Execute does not cover. A PATH wrap over
`allowedSubcommands` is a [bootstrap](/docs/concepts/adapter#pass-through-is-a-bootstrap)
to promote to named commands. "New message arrived" is a
[Sense](/docs/concepts/sense) trigger such as `new_item`, while `mail.list` is a
command, and `follow` stays a read command however often an agent calls it.

## Related

- [Execution](/docs/runtimes/execution): the pipeline a command passes
- [Wrap a CLI](/docs/packs/wrap-a-cli)
- [Specification: Execute](/docs/specification/execute)

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