The sentinel CLI: watch Stacks contracts and manage alerts from a terminal. Same client as the MCP server and the SDK.
npm i -g @secondlayer/sentinelInstalls 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. SENTINEL_API_URL overrides the API base.
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.
--dry-run and --yesA 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.
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.
| 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 |
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 |
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 |
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 |
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.
{
"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.
{
"$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"
}
}
}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-vaultA new webhook prints its signing secret once: store it then. See Watches & 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:
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 withdrawEach 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.
sentinel contracts
$ sentinel every daily 09:00 --digest
$ sentinel remove example-vault --yes