# MISSIN CLI

> Run MISSIN from your terminal with @missin/cli: log in once, run any tool as a command, script with --json and exit codes, and start a local MCP server.

`@missin/cli` installs the `missin` command. Every tool an AI agent can use is also a command, so the terminal can do what the [MCP server](https://developer.missin.co.uk/mcp/connect-claude.md) can do, at the same access level.

## Install and log in

1. Install the package. It needs Node 22.13 or later: that is the version it is built and tested on. The Claude Desktop extension runs the same code with the Node that ships inside Desktop, and declares an older floor (18.20) because that is what Desktop supplies and where it was tested.

   ```bash
   npm install -g @missin/cli
   ```

2. Create a secret key in Settings > Developers > Create key (https://app.missin.co.uk/settings/developers), then log in. The key is read from a hidden prompt, or from a pipe.

   ```bash
   missin login
   printf '%s' "$MISSIN_KEY" | missin login --key-stdin
   ```

3. Check what the key can do.

   ```bash
   missin whoami
   missin tools --toolset partners
   ```

There is no `--key` flag, because a key on the command line lands in your shell history. `missin login` checks the key with the API before it stores anything, so a rejected key is never saved (exit code 4). Only secret `sk_live_` keys log in.

### Where the key lives

- The CLI stores the key in your system keychain. Where there is none, such as a headless Linux machine, it writes `~/.config/missin/credentials` (under `$XDG_CONFIG_HOME/missin` if that is set), readable only by you. The folder is mode 0700 and the file 0600.
- `MISSIN_API_KEY` in the environment always wins over a stored key. `missin login` and `missin logout` both warn you while it is set.
- `missin logout` removes the key from the keychain and the file, and says which it removed.

## What a key can reach

`missin tools` lists the commands your key can run, each with its access level. A command above the key's level is refused with exit code 7 and the server's sentence.

| Level | Reaches |
|---|---|
| Read | Looking things up, and getting a capture quote |
| Write | Also creating and changing partners, campaigns, posts, discounts and forms, and starting captures |
| Admin | Also changing connections, integration settings, the brand and workspace settings |

No level creates or revokes keys or members: do that in the app. A key acts as the member who created it, and its level is capped by that member's role.

## Commands

Commands are named `missin <domain> <action>`, from the same names the [SDK](https://developer.missin.co.uk/tools/sdk.md) uses. Workspace-wide tools sit at the top, as `missin whoami`, `missin find` and `missin toolsets`.

```bash
missin partners list --first 10
missin partners save --name "Test 11" --handle instagram:test11 --handle tiktok:test11
missin campaigns list
```

Run `missin <domain> --help` for a domain's commands and `missin <domain> <action> --help` for one command's flags.

- **Flags** are the tool's inputs, in kebab-case: `maxPerPartner` is `--max-per-partner`.
- **Repeat a flag** to pass a list, as `--handle` above. A pair is `network:handle`.
- **Names work where ids do.** `--partner @test11` and `--campaign spring` are resolved by the CLI, which looks the name up through the API before it makes the call. When a name matches several records, or none, the command prints the candidates and exits with code 3. Run it again with one id.
- **`--args`** passes all the arguments as one JSON object: `--args '{"first":10}'`, `--args ./args.json`, or `--args -` for stdin. Flags you give as well override it.

## Output

On a terminal you get a table. When stdout is piped, or with `--json`, you get JSON: the data on success, and on a failure the whole result with its `kind`. Failures go to stderr, so `missin partners list | jq` never mixes the two; a `needs_choice` answer is the exception, because it asks you a question, and goes to stdout. No output contains your key.

## Exit codes

Scripts can react to the code without reading the message.

| Code | Meaning |
|---|---|
| `0` | Done |
| `1` | Anything else: a usage mistake, not logged in, a server fault, a spend that was not confirmed |
| `2` | The API returned `userErrors`; nothing was written. They are printed to stderr |
| `3` | `needs_choice`: a name matched zero or several records. The candidates are printed |
| `4` | The key was rejected or cannot be used. Log in again |
| `5` | Rate limited. Wait, then run it again |
| `6` | The request timed out. A write may have happened, so check before you repeat it |
| `7` | `forbidden`: the key is valid but below the access level the command needs |
| `130` | You pressed Ctrl-C at a prompt |

## Spending credits

Some commands spend capture credits. They never do it silently: the CLI shows what it will spend, asks `y/N` with No as the default, and only then runs it. Off a terminal, with no `--yes`, such a command is refused before it calls anything.

| Command | What you are shown | `--yes` |
|---|---|---|
| `missin capture start` | The quote, in credits and pounds | Allowed: the quoted price is sent with the call, and a moved price is never accepted for you |
| `missin capture dispatch-hashtag` | The exact total the server checks | Allowed, same pinning |
| `missin capture dispatch-partner` | A price for display only. With no partner ids it says it will read every watched handle | Only with `--quoted-credits`, which the server checks |
| `missin capture rerun-run`, `missin capture resync-run`, `missin partners resync-profile`, `missin posts fetch-twitter-replies` (retired) | No price: the server has none to check | Refused: answer the prompt in a terminal |
| `missin capture save-schedule`, `missin capture save-integration-setting`, `missin posts set-partner-settings` with `--enabled`, and `missin partners set-handle-watched` with `"watched": true` in `--body` | That this commits recurring spend: every scheduled run that follows spends credits, with no price | Allowed |

If the price moves between the quote and the start, a terminal is shown the new price and asked again. With `--yes` the command stops with exit code 2 and the new price. A capture that started on some platforms and not others says which started and which did not.

## The API address

The CLI talks to the public API unless you set `--base-url <url>` or `MISSIN_BASE_URL`. Your key is sent to that address, so it must be `https`; plain `http` is accepted only for `localhost` and `127.0.0.1`. Any address other than the default is announced on stderr.

`--api-version <id>` (or `MISSIN_API_VERSION`) sends a different API version than the one this CLI was built for; `latest` follows the current version. [Versions and stability](https://developer.missin.co.uk/guides/stability.md) says how long an older version keeps working.

## Run a local MCP server

```bash
MISSIN_API_KEY=sk_live_... npx -y @missin/cli mcp --toolsets partners,campaigns
```

`missin mcp` runs the same tools as the hosted server at `https://api.missin.co.uk/mcp`, over stdio, for any client that starts a command. It reads the key like every other command, from `MISSIN_API_KEY` or the login. Choose which tools load with `--toolsets` or `MISSIN_TOOLSETS`: `core` is the default and `all` loads everything. Names are checked the way the hosted server checks `?toolsets=`. See [Connect Claude](https://developer.missin.co.uk/mcp/connect-claude.md) to add it to Claude.

Standard output carries only the protocol; everything else goes to stderr. With no key the command exits with code 1 before it speaks MCP.

Source: https://developer.missin.co.uk/tools/cli
