---
title: "Build a pack"
description: "Wrap a CLI you already ship in about ten minutes, then prove it installs, answers on both faces and refuses a gated write until confirmed."
canonical: https://harnesscontextprotocol.io/docs/packs
---

# Build a pack

> Wrap a CLI you already ship in about ten minutes, then prove it installs, answers on both faces and refuses a gated write until confirmed.

A pack is three files beside a binary you already have: `surface.json` names
the commands, `policy.json` gives each a rule and `HARNESS.md` teaches the
agent. This tutorial scaffolds them, lints, checks and runs the result, and adds
one gated write. Install `hcpx` first ([Quickstart](/docs/quickstart#install-the-cli))
and search the [catalog](/docs/packs/share#catalog), since someone may already
have wrapped your tool.

## Build it step by step

<Steps>
<Step>

### Scaffold the three files

Pick the starting point closest to what you have. The
[pack classes](/docs/packs/best-practices#pack-classes) explain each shape.

| You have | Run | You get |
|----------|-----|---------|
| A read-mostly tool with no login | `hcpx init weather --class public-read` | Public read template with a demo binary |
| A tool with a connect step or a risky write | `hcpx init payments --class gated-write` | Gated write template with installer commands and a confirm write |
| An existing binary on your `PATH` | `hcpx wrap weather` | The same files with `wrap.bin` pointing at that binary |
| A wish to read working code first | `hcpx init transit --template transit` | A copy of a gold pack (`transit` or `ledger`) |

Each writes `./<id>/` with the three files and a `package.json` that depends on
`@harnesscontextprotocol/cli`, so installing the pack also installs the CLI.
[Pack layout](/docs/concepts/pack#layout) lists the optional files and what
stays out.

</Step>
<Step>

### Name the commands

In `surface.json`, set the id, the binary, the Binding mode and the commands the
pack offers.

```json title="weather/surface.json"
{
  "id": "weather",
  "name": "Weather",
  "kind": "wrap",
  "cliBin": "weather",
  "plane": "runtime",
  "transports": ["cli", "json_exec"],
  "binding": { "mode": "public", "hub": "public" },
  "wrap": { "bin": "weather" },
  "verbs": {
    "find":     { "description": "Resolve a place name.", "cli": ["weather", "find", "{{query}}"] },
    "now":      { "description": "Current conditions.",   "cli": ["weather", "now", "{{place}}"] },
    "forecast": { "description": "Next 48 hours.",        "cli": ["weather", "forecast", "{{place}}"] },
    "alerts":   { "description": "Active warnings.",      "cli": ["weather", "alerts", "{{place}}"] },
    "status":   { "description": "Upstream health.",      "cli": ["weather", "status"] }
  }
}
```

Keep the set closed and small, five to twelve names. Each `cli` array is the
command's CLI form and every `{{name}}` placeholder maps to a JSON argument; the
runtime appends `--json` and never passes arguments through a shell.
[Manifest](/docs/packs/manifest) describes every field.

</Step>
<Step>

### Give every command a Policy row

Reads are `read` and anything that changes the world is `write`. A write that
must never fire unconfirmed also gets `confirm: true`.

```json title="weather/policy.json"
{
  "surface_id": "weather",
  "version": 1,
  "verbs": {
    "find":     { "operation": "read", "privacy": { "land": "personal" } },
    "now":      { "operation": "read", "privacy": { "land": "personal" } },
    "forecast": { "operation": "read", "privacy": { "land": "personal" } },
    "alerts":   { "operation": "read", "privacy": { "land": "personal" } },
    "status":   { "operation": "read", "privacy": { "land": "personal" } }
  },
  "stimuli": {}
}
```

`land` says where the data may end up (`personal`, `brain` or `local`). Roles,
user lists and keys stay out of the file because the runtime grants those, as
[Policy file](/docs/concepts/policy#policy-file) explains.

</Step>
<Step>

### Teach it on one screen

`HARNESS.md` teaches an agent, and a person, the hot path in a lead, a command
table, examples and the mistakes to avoid. It never holds essays, secrets or
host-absolute paths.

```md title="weather/HARNESS.md"
# HCP — Weather

Public read pack. No login. Resolve a place, then read conditions, forecast, or alerts.

## Hot path

| Command | CLI |
|---------|-----|
| `find` | `weather find "Berlin" --json` |
| `now` | `weather now berlin --json` |
| `forecast` | `weather forecast berlin --json` |
| `alerts` | `weather alerts berlin --json` |
| `status` | `weather status --json` |

## Anti-patterns

- Calling `now` in a tight loop. Read `forecast` once instead.
- Guessing a place id. Always `find` first.
```

[Skill file](/docs/concepts/skill#skill-file) is the writing guide.

</Step>
<Step>

### Lint until clean

```bash
hcpx lint ./weather --json
```

```json
{ "ok": true, "pack_id": "weather", "issues": [] }
```

Lint proves the files agree: every command has a Policy row, every row names a
real command, and the Skill exists and matches the manifest.
[Testing](/docs/packs/check#testing) lists each failure and its fix.

</Step>
<Step>

### Check it

`hcpx check` grades the pack as a reviewer would, and `--probe` runs the CLI as
an agent does.

```bash
hcpx check ./weather --init-evals    # write hcp.eval.json: one case per read, one refusal per confirm write
hcpx check ./weather --probe
```

Replace every `TODO` argument in `hcp.eval.json` with a real value and fix
findings until none is an error, using the rules in
[Check and evaluate](/docs/packs/check).

</Step>
<Step>

### Install, activate, run

```bash
hcpx install ./weather --json
hcpx activate weather --json
hcpx availability weather --json
hcpx exec weather now --json -- place=berlin
```

`hcpx exec` sent this envelope:

```json
{ "pack_id": "weather", "verb_key": "now", "args": { "place": "berlin" } }
```

and got back:

```json
{ "ok": true, "pack_id": "weather", "verb_key": "now", "result": { "temp_c": 17, "sky": "overcast" } }
```

Running `weather now berlin --json` directly returns the same result, because
the two forms are the command's [twin face](/docs/concepts/command#the-twin-face).

</Step>
<Step>

### Add a gated write

Declare any mutation that must never fire from a hallucinated turn as a confirm
write, in the shape the `ledger` gold pack uses:

```json title="surface.json (fragment)"
"transfer": {
  "description": "Move funds. Requires confirm.",
  "cli": ["ledger", "transfer", "{{amount}}", "--to", "{{payee}}", "--confirm"],
  "policy": "write"
}
```

```json title="policy.json (fragment)"
"transfer": { "operation": "write", "privacy": { "land": "local" }, "confirm": true }
```

Then prove the gate holds:

```bash
hcpx exec ledger transfer --json -- amount=2.80 payee=demo-transit
# { "ok": false, "error": "confirm required" }

hcpx exec ledger transfer --json -- amount=2.80 payee=demo-transit confirm=true
# { "ok": true, … }
```

The runtime checks `confirm` before the adapter runs, so no Invoker can skip it.

</Step>
<Step>

### Share it

```bash
hcpx share ./weather --json
```

`share` lints the pack and prints a catalog row to add with a pull request, and
[Share a pack](/docs/packs/share) has the checklist. Because `hcpx init` writes
`"hcp": { "pack": "." }` into `package.json`, the pack can also ship as an npm
package:

```bash
hcpx install npm:@scope/hcp-pack-weather --json
```

HCP's own packages are a private preview on GitHub Packages for now, not on the
public npm registry.

</Step>
</Steps>

## What stays out

The pack has no connector code, server, host tool registration or token, and
it does not wrap the whole `weather` binary. Hosts pick it up through
`hcpx adopt` ([Hosts](/docs/hosts)), and a binary that needs credentials or a
setup step continues in [Wrap a CLI](/docs/packs/wrap-a-cli).

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