---
title: "Best practices"
description: "Pick one of five pack shapes, then hold the pack to the habits for scope, naming, Policy, Skill, Binding, wrapping and Sense that review checks."
canonical: https://harnesscontextprotocol.io/docs/packs/best-practices
---

# Best practices

> Pick one of five pack shapes, then hold the pack to the habits for scope, naming, Policy, Skill, Binding, wrapping and Sense that review checks.

Lint decides whether a pack is valid, and its shape and these habits decide
whether agents use it correctly. Pick a class first, copy its sample, and run
the review checklist at the end before you share.

## Pack classes

Every CLI fits one of five classes, even in a domain that feels different. The
three files keep their shape in each, so copy the sample and change the fields.

| If the capability… | Class |
|--------------------|-------|
| Needs no login | [Public read](#public-read). It may still write; Policy `confirm` gates the risky commands |
| Needs a login, a token, or a connect step | [Gated write](#gated-write) |
| Is a linked personal or project stream with real push events | [Linked channel](#linked-channel) |
| Is ingress or webhook only, with no user commands | [Emit-only](#emit-only) |
| Runs under a different Binding than the user runtime | [Control](#control), which is a second pack |
| Is MCP-only with no CLI and no `doctor` | Do not wrap. Ship a CLI first |

| Class | Binding | Land | Commands | Sense |
|-------|---------|------|----------|-------|
| Public read | `public` | `personal` | `find` · `plan` · `show` · `status` · `follow` (or equivalent) | Optional. Pull observation until a real push exists |
| Gated write | `local` or `user`, with a credential hub | `local` (workspace directory) or `personal` | `setup.status` · `setup.apply` · `connect` · `status` · one read · one confirm write | `binding.degraded` when connect health moves |
| Linked channel | `user`, shared `family` | `personal` or `brain` | `list` / `get` / `send` (`send` is `write`, usually `confirm`) | Channel event (`new_item`) + `binding.degraded` |
| Emit-only | none or `public` | | none | Stimulus refs + Policy rows |
| Control | `mothership` | | | Optional health. Never a user inbox |

### Public read

A public read pack has five to twelve closed reads, such as a discover, plan
and show sequence, and its sample is `examples/packs/transit`. `follow` is a read an agent may call
repeatedly, so when the source starts to push, add a trigger instead of
overloading `plan`.

### Gated write

At least one write must not fire from a hallucinated turn (transfer, send,
apply, delete). The `examples/packs/ledger` sample runs the whole family, and
without `--confirm` its transfer returns
`{ "ok": false, "error": "confirm required" }`.

```bash
ledger setup status --json           # read, idempotent
ledger setup all -y --json           # write, no wizard on the agent path
ledger connect demo --json           # write, gated by pack.connect
ledger list --json                   # read
ledger transfer 2.80 --to demo-transit --confirm --json  # write + confirm
```

### Linked channel

Mail, calendar, chat and tickets differ only in Binding family and payload.
This is the one class that uses both loops, and its adapter is `composite` once
the SDK is stable, with `wrap` as a fine bootstrap.

### Emit-only

The pack only announces, with adapter `sense_only` and no user CLI. Never write
bash to "install a webhook"; add `connect` or `setup.*` only if the upstream
source needs a registration call.

### Control

Admin, fleet, operator and origin tools set `plane: "control"` and ship as a
second pack that never sits silently on an end-user Principal. A domain CLI with
an admin face and a user face becomes two packs that share only the bin.

## Scope

Name 5 to 12 commands. More means two packs, because a large set dilutes the
Skill and agents pick wrong, and a CLI with forty subcommands probably has five
that matter to an agent. Keep one class per pack: an operator face mixed into a
user pack is the most common reason review rejects one.

## Naming

Name the outcome: `plan`, `send` and `transfer`, never `doPlanning`, `sendMail`
or `execTransfer`. Use families for lifecycle steps (`setup.status`,
`setup.apply`, `setup.update`), never for hierarchy (`mail.folder.item.get`).
Renaming a command breaks the Skills, host projections and grants that name it,
so add a new command and deprecate the old one.

## Policy

Mark a command `read` only if an agent could safely retry it. Confirm the
irreversible (transfers, deletes, external sends, publishes) and leave confirm
off idempotent writes such as `setup.apply` and `sync`, so agents retry them
freely. Choose the land by where the data lives, never by adapter or Binding
mode, and never encode who may run a command: roles and users are runtime
grants, and a pack that embeds them is wrong for every other runtime.

## Skill

Hosts keep roughly the first 480 characters of the lead in stable context, so
the lead must stand alone and name the first command. Show the confirm flag in
examples, because agents copy them and an example without `--confirm` gets the
first call denied, after which the agent improvises. Separate setup from daily
ops, or agents will `connect` on every turn. Anti-patterns are the most useful
section, so write the ones you have seen.

## Binding

Use one mode per pack, because a CLI that needs two is two packs. Declare the
family when a sibling pack shares the login, so users consent once. Never fall
back to env: when the runtime cannot mint, the command fails. Mock is explicit
(`mode: mock` in the envelope, never `PACK_MOCK=1`).

## Wrap

Template with `cli[]` and `{{placeholders}}` and never interpolate. Set
`agent_env` for output mode if the bin needs it (`FOO_OUTPUT=json`), so the
runtime never special-cases your bin. Pass-through is only a bootstrap, so
promote it to named commands. A pack keeps its class while its adapter moves on,
and every step keeps `pack_id`, `verb_key` and Policy unchanged:

```text
pass_through  ──►  named 5–12 commands + policy.json  ──►  composite for hot paths
```

## Sense

Declare only real pushes, since polling is a read command. Always declare
`binding.degraded` on a pack with `user` or `local` Binding, because that is how
a person learns to reconnect.

## Testing

Ship the [proof script](/docs/packs/check#the-proof-script) and run it in CI,
with the [twin-face check](/docs/packs/check#the-twin-face-check). Never publish
a catalog of many packs without lint, a twin-face check and a gated-write proof.

## Review checklist

- [ ] 5 to 12 named commands, one class
- [ ] Every command and trigger has a Policy row; lint is clean
- [ ] Irreversible writes have `confirm: true` and `--confirm` in `cli[]`
- [ ] Land chosen by data ownership
- [ ] One Binding mode; family declared if shared; no env fallback
- [ ] Skill lead names the first command; setup separated; anti-patterns present
- [ ] No secrets, absolute paths, UI, or MCP-as-install anywhere
- [ ] Control-plane commands are in a second pack
- [ ] Proof script passes in CI

## Related

- [Design principles](/docs/introduction/design-principles)
- [Skill file](/docs/concepts/skill#skill-file)
- [Examples](/docs/packs/examples)

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