> Every Sentinel doc, as Markdown: https://runsentinel.app/llms.txt

# CLI

The sentinel CLI: watch Stacks contracts and manage alerts from a terminal. Same client as the MCP server and the SDK.

```sh
$ npm i -g @secondlayer/sentinel
```

Installs `sentinel` and `sentinel-mcp`. Node 22 or newer. Without installing: `npx -y @secondlayer/sentinel <command>`.

Credentials, first match wins: `--token <key>`, then `SENTINEL_API_KEY`, then the key saved by `sentinel login --with-token`, then the login from `sentinel login`. See [Authentication](/docs/authentication). `SENTINEL_API_URL` overrides the API base.

## First run

No login yet? Run the command you want. It prints a link and a code; open the link and confirm the code. In a terminal it waits and then runs your command. From an agent's shell it exits `3`; `sentinel login --wait` (or the same command again) waits for the approval and runs it once. It runs exactly what you typed. The same happens once when a saved login has expired.

## Writes, `--dry-run` and `--yes`

A free write happens at once: `when`, `every`, `at`, `on`, `destinations`, `import`, `group` and the small changes. Add `--dry-run` to any of them to see what it would do and write nothing.

`--yes` is for money and stopping coverage. A check, an ask or a Builder contract over your count shows its cost and waits for `--yes`, and so does an email address that is not yours (it is mailed a confirm link). `off`, `remove`, `destinations remove`, `keys revoke` and `backtest` preview until `--yes`, and so does `test-alert`, which sends the sample alert.

Add `--json` to `whoami`, `contracts`, `list`, `show`, `logs` and the other reads for the raw response.

Exit codes: `0` ok, `1` API error (message on stderr), `2` usage error, `3` waiting for you to approve a sign-in.

## Targets

A `<target>` is a contract: its full id (`SP….name`), its name on your account (`example-vault`), or a unique part of one. Leave the target out of `every` and `at` for a digest of every watch you have. A name that matches two contracts is refused with both ids.

{/* cli-commands:start */}
## Account

| Command | Does |
|---|---|
| `sentinel login [--force]` | Sign in with your secondlayer account: open the link it prints, confirm the code. A terminal waits for you; without one it exits and `login --wait` finishes |
| `sentinel login --wait` | Finish a sign-in started without a terminal. Waits up to 100 s: exit `0` signed in, `3` not yet (run it again), `1` cancelled or expired |
| `sentinel login --email` | Your email and a 6-digit code instead, for a machine with no browser. Needs a terminal |
| `sentinel login --with-token` | Read an API key from stdin (CI). Checked before it is saved |
| `sentinel logout` | Sign this machine out (revokes the session). Also signs the secondlayer CLI out |
| `sentinel whoami [--json]` | Who you are, where the credential came from, your tier, how many live contracts it covers and this period's check usage |
| `sentinel upgrade <builder\|team>` | The Stripe link to subscribe to Builder or Team, or to the portal once you are subscribed. Sentinel only gives the link: you pay or confirm on Stripe. A terminal opens it and waits; without one it prints the link and exits, and `upgrade --wait` finishes. Contracts waiting for your tier start by themselves after payment |
| `sentinel upgrade --wait` | Finish an upgrade started without a terminal. Waits up to 100 s: exit `0` subscribed (with the contracts now live), `3` not yet (run it again), `1` the link expired |
| `sentinel status [--json]` | Is monitoring up, measured: the store, every subscription, the outbox and the lanes. Signed in, your own subscriptions too. Anyone can read it without a token |
| `sentinel status watch on\|off` | Your digests carry a line saying whether monitoring is up, or stop. Degraded notices come either way. At once. Needs Monitoring: write |
| `sentinel keys` | Your API keys and connected apps. Needs your own login, not a key |
| `sentinel keys create <name> [--plans none\|read\|write] [--monitoring none\|read\|write] [--alerts none\|read\|write]` | Make a key. Areas left out are read (alerts: write). Prints it once |
| `sentinel keys revoke <id> [--yes]` | Revoke a key or a connected app |
| `sentinel alerts [--contract <id>] [--after <alert id\|time>] [--before <alert id\|time>] [--watch <watch id>]` | Your alerts, newest first; `--after`: the ones raised since, oldest first (catch up after downtime); `--before`: the older ones; `--watch`: one watch's |
| `sentinel read <alert>\|--all` | Mark one alert, or every alert, read. Needs Alerts: write |
| `sentinel ask "<question>"` | An answer from Sentinel's docs |

## Contracts

The contracts on your account: the ones you added and the ones you only watch. Reads need Plans: read; importing, renaming and removing need write.

| Command | Does |
|---|---|
| `sentinel import <repo-url\|path> [--only a,b] [--all] [--deployer SP… [--hold]] [--dry-run]` | A local folder (`.`: its `Clarinet.toml` and the `.clar` files under `contracts/`, read as text, at most 40 files and 1 MB; nothing runs) or a public Clarinet repo's contracts in deploy order, with deps, state and whether each is suggested, and the cost of the selection (the suggested ones, unless `--only` or `--all`). They are planned and added to your account at the commit shown; `--dry-run` only shows them. `--deployer` arms each to go live when it deploys from that address (code that differs is flagged, not blocked); `--hold` keeps its watches off after the deploy until `sentinel on <id>` |
| `sentinel contracts [--json]` | The contracts on your account: the ones you added and the ones you only watch, and each one's monitoring |
| `sentinel rename <target> "<name>"` | Your own name for a contract (`""` clears it) |
| `sentinel remove <target> [--yes]` | A contract leaves your account: its monitoring, alerts and claim |

## Pre-launch upkeep

On a pre-launch contract you own. A write happens at once; `--dry-run` previews it. A key needs Plans: read to look, write to change.

| Command | Does |
|---|---|
| `sentinel update <target> [--commit <sha>] [--dry-run]` | Whether a pre-launch plan's branch moved on and what an update would review; Plans the latest commit (or `--commit`); `--dry-run` only shows where the branch is. Free while monitoring is on |
| `sentinel auto-plan <target> on\|off [--dry-run]` | Plan a waiting plan on its own once the contracts it waits on deploy |
| `sentinel link <target> <contract> <url>\|--clear [--dry-run]` | Link code for a contract the plan waits on |
| `sentinel plan-linked <target> [--dry-run]` | Plan now with the linked code. A free plan |

## Monitoring

A watch is a `w_…` id (from `sentinel list`): `show`, `logs`, `on`, `off` and `mute` take it. `on` and `off` also take a contract, to resume or stop all of its monitoring.

| Command | Does |
|---|---|
| `sentinel describe "<sentence>"\|<line> [--json]` | A sentence (`"alert if more than 5 sBTC leaves example-vault"`), or a line with names, as one watch: its line, what it would have done in the last 90 days, and its cost. Writes nothing. |
| `sentinel when call\|transfer\|mint\|print\|state\|deploy <target> … \| when sbtc\|pox5 <check> … [--to <ref>]… [--note "<text>"] [--check "<question>" [--deep] [--cap N]] [--hold] [--dry-run] [--yes] [--wait]` | A watch, as one line: `when call <target> <fn\|@admin\|@value\|@control> [--caller <addr>]… [--not-caller <addr>]… [--arg k=v]… [--new-caller] [--first-call] [--role-change]`, `when transfer <target> <stx\|token\|*> --in\|--out [--over <amount\|20%>] [--within 1h] [--new-recipient]`, `when mint <token-contract> <token-name> [--new-minter \| --over <amount>]`, `when print <target> <topic\|*>`, `when state <target> <fn\|var\|map> [--arg k=v]… [--key <v>] [--drops 20% --within 1h \| --crosses <n>]`, `when deploy <address>`, `when sbtc <stall\|payout\|mint\|rotation\|protocol-update\|reserve\|peg> [--gap <blocks>] [--open <blocks>] [--after <hours>h]`, `when pox5 <bond-setup\|admin\|early-exit>` (sBTC, PoX-5 checked as a whole: no target). It is added at once, with what it would have done in the last 90 days: a contract not on your account joins it (its monitoring on), and a destination that never had an alert from you gets a test one. `--dry-run` only shows its line, that backtest and its cost. A line that costs money (`--check`, or a Builder contract over its count) shows the cost and waits for `--yes`. `--wait` waits until it is live (exit 1 if it can't be). The same line again edits it. `--check`: a model answers your question on each event first, and it alerts only on a yes whose reasons cite the code ($0.25 a run, `--deep` $0.75, at most `--cap` a month, default 100). `--hold`, on a pre-launch contract armed for its deployer, keeps the watch off after the deploy until `sentinel on <id>`. |
| `sentinel every <spec> [<target>] --digest [--ask "<question>"] \| --check "<question>" [--deep] [--cap N] [--tz <zone>] [--to <ref>]… [--note "<text>"] [--dry-run] [--yes]` | A digest of a contract you monitor, or of everything you monitor when the line names no target, on a clock. Free; `--ask` adds one cited answer to each digest (one check run). `--check` runs your question each fire instead: a yes alerts, a no sends nothing. Checks are billed per run ($0.25, `--deep` $0.75), at most `--cap` a month. `<spec>`: `6h`, `1d` (at least `1h`), `monday 09:00`, `daily 09:00` or a 5-field cron line; `--tz` is an IANA zone (default: this machine's); `--to` is a comma-separated list of `email`, `slack`, `webhook`, `slack:<label>`, `email:<address>` (default: your default destinations). `--dry-run`: the next fires and a sample of the last 7 days. With `--ask` or `--check` it shows the cost and waits for `--yes` |
| `sentinel at <iso> [<target>] --digest [--ask "<question>"] \| --check "<question>" [--deep] [--cap N] [--to <ref>]… [--note "<text>"] [--dry-run] [--yes]` | One digest (or check) at one time (ISO 8601 with `Z` or an offset, within a year) |
| `sentinel backtest <w_id> [--run N] [--yes]` | Run the watch's check on its last `N` (1 to 5) matching events now: paid per run, never delivered, never against the cap. Previews until `--yes`: which events and the cost |
| `sentinel list [<target>] [--all] [--json]` | Your watches and schedules by id (`w_…`, `sch_…`): each one's line and whether it alerts now. `--all`: the suggestions too |
| `sentinel show <w_id\|sch_id\|chk_id\|alr_id> [--json] show <contract> [--json] show sbtc\|pox5 <series> [--limit N] [--json]` | One watch (its line, where it goes, its check, a group's members and where each came from, its last alert), schedule, check run, or alert (what happened, what to check first, your note, links and deliveries). `show <contract>`: what its code has (functions, tokens, the `@admin`, `@value` and `@control` groups and the events it prints, with what the plan suggests). Starts its free plan (a GitHub `.clar` link works too) and turns nothing on. `show sbtc reserve`, `show pox5 custody` and `show pox5 stbtc`: a reconciliation series, newest last (`--limit`). |
| `sentinel logs [<w_id\|sch_id\|target>] [--only events\|alerts\|checks\|schedules\|changes] [--since <iso>] [--tx <id>] [--after <cursor>] [--limit N] [-f] [--json]` | What happened, oldest first: a watch's matched events, alerts, check runs, schedule runs, and changes to watches, destinations, monitoring, schedules, checks and groups. `-f` follows (every 5 s) |
| `sentinel group <target> [@admin\|@value\|@control] [+fn …] [-fn …] [--dry-run]` | A contract's groups, each member and where it came from (`facts`, `jev`, `you`). `+fn` / `-fn` correct one group for your account only; a watch on it follows. `--dry-run`: the members it would have |
| `sentinel on <w_id\|target> [--deployer SP…] [--dry-run]` | Turn a watch on by its id, or monitoring: the watches switched on. A live mainnet contract counts toward your tier; events are never billed. A contract turns on only if one of its watches is on; add one with `when`. `--deployer` arms a pre-launch plan |
| `sentinel off <w_id\|sch_id\|target> [--yes]` | Turn a watch or a contract's monitoring off. Nothing is deleted: `on` turns it back on (`remove` takes a contract off your account). A schedule id stops the schedule; its history stays |
| `sentinel destinations [--json]` | Where your alerts can go: your own address and what you added. A watch's `--to` picks one (`slack:#oncall`, `email:a@b`, `webhook:ops`); a default gets every watch that names none |
| `sentinel destinations add slack\|email\|webhook <target> [--label <name>] [--default] [--yes]` | Add a destination: a Slack `#channel` (after `connect slack`), a Slack incoming webhook under `--label`, a signed webhook under `--label` (its signing secret is shown once), or an address. `--default` sends every watch that names none there too. An address that is not yours is mailed a confirm link and receives nothing until then; it previews until `--yes` |
| `sentinel destinations default <ref\|d_id> on\|off [--dry-run]` | Make a destination a default, or not: a default gets every watch that names none |
| `sentinel destinations remove <ref\|d_id> [--yes]` | Remove a destination. Your own address stays: turn it off as a default instead |
| `sentinel connect slack [--status \| --remove <team>] [--yes]` | The Sentinel Slack app: with no option, a link to add it to your workspace (it works once, for 10 minutes); `--status` lists your workspaces; `--remove` disconnects one (previews until `--yes`). Needs Monitoring |
| `sentinel test-alert <target> [--example money_out] [--yes]` | The newest real match of the contract's watches (else a sample), as email and Slack get it, marked a test. Previews until `--yes`, which sends it to every saved destination |
| `sentinel mute <w_id\|alert> [1h\|24h\|7d\|off]` | Mute a watch (default 24h; `off` unmutes), or the watch behind an alert (needs Alerts: write). At once |
| `sentinel redeliver <alert>` | Send an alert again to the destinations that never took it, after you fix one. At once. Needs Alerts: write |
| `sentinel retry-history <target>` | Read a contract's watch history again after a failed read. At once |
{/* cli-commands:end */}

## Let an agent run it

{/* permissions:start */}
Let an agent run the reads without asking. Everything else (`when`, `every`, `at`, `off`, `destinations add`, `remove` …) stays a prompt, so a person sees the full line before anything is added, rerouted or stopped. The read verbs refuse `--yes` and every write flag, so appending flags to an allowed prefix can't widen it. Don't add a blanket `sentinel *` rule, and don't list `keys`.

Claude Code, in `.claude/settings.json`. An `ask` rule beats an `allow` rule, so there is none: Claude Code asks for any command that isn't allowed. The `mcp__sentinel__` rules assume the server is named `sentinel`.

```json
{
  "permissions": {
    "allow": [
      "Bash(sentinel whoami *)",
      "Bash(sentinel status *)",
      "Bash(sentinel alerts *)",
      "Bash(sentinel ask *)",
      "Bash(sentinel contracts *)",
      "Bash(sentinel describe *)",
      "Bash(sentinel list *)",
      "Bash(sentinel show *)",
      "Bash(sentinel logs *)",
      "Bash(sentinel destinations *)",
      "mcp__sentinel__whoami",
      "mcp__sentinel__status",
      "mcp__sentinel__alerts",
      "mcp__sentinel__ask",
      "mcp__sentinel__contracts",
      "mcp__sentinel__describe",
      "mcp__sentinel__list",
      "mcp__sentinel__show",
      "mcp__sentinel__logs",
      "mcp__sentinel__destinations"
    ]
  }
}
```

OpenCode, in `opencode.json`. The last matching rule wins, so the catch-all comes first.

```json
{
  "$schema": "https://opencode.ai/config.json",
  "permission": {
    "bash": {
      "sentinel *": "ask",
      "sentinel whoami": "allow",
      "sentinel whoami *": "allow",
      "sentinel status": "allow",
      "sentinel status *": "allow",
      "sentinel alerts": "allow",
      "sentinel alerts *": "allow",
      "sentinel ask": "allow",
      "sentinel ask *": "allow",
      "sentinel contracts": "allow",
      "sentinel contracts *": "allow",
      "sentinel describe": "allow",
      "sentinel describe *": "allow",
      "sentinel list": "allow",
      "sentinel list *": "allow",
      "sentinel show": "allow",
      "sentinel show *": "allow",
      "sentinel logs": "allow",
      "sentinel logs *": "allow",
      "sentinel destinations": "allow",
      "sentinel destinations *": "allow"
    }
  }
}
```
{/* permissions:end */}

## Examples

```sh
$ echo "$KEY" | sentinel login --with-token
$ sentinel show SP000000000000000000002Q6VF78.example-vault
$ sentinel when transfer example-vault sBTC --out --over 2 --within 1d
$ sentinel destinations add webhook https://example.com/hook --label ops --default
$ sentinel on example-vault
$ sentinel logs example-vault -f
$ sentinel alerts --contract SP000000000000000000002Q6VF78.example-vault
```

A new webhook prints its signing secret once: store it then. See [Watches & alerts](/docs/watches-and-alerts).

Import a whole repo instead of one file at a time. For a monorepo, link one project's folder (`/tree/<branch>/<folder>`). Before it deploys, `--deployer` arms each contract, and `when` on them saves armed:

```sh
$ sentinel import https://github.com/acme/protocol --dry-run
# --dry-run lists each contract: + marks what would be imported
$ sentinel import . --deployer SP000000000000000000002Q6VF78
$ sentinel when call vault withdraw
```

Each new contract is one plan, and joins your account. Plans are free. `--hold` keeps each contract's watches off after the deploy until `sentinel on <id>`. See [Import a repo](/docs/api#import-a-repo).

```sh
$ sentinel contracts
$ sentinel every daily 09:00 --digest
$ sentinel remove example-vault --yes
```
