# Maritime CLI: instructions for AI agents

Copy this file into your agent's context (CLAUDE.md, AGENTS.md, or a system prompt), or let the agent fetch it from https://maritime.sh/docs/cli/llms.txt. It teaches an LLM to operate Maritime (https://maritime.sh): every computer an agent needs (sandboxes, Linux and Windows desktops, Android phones). Agents deployed here run in persistent micro-VMs with storage at /data; they sleep when idle and wake in about a second on the next message or trigger. Billing: plans from $20 a month, and the plan fee becomes usage credit; machines are billed per second at hourly rates, with a 7-day free trial (card needed, usage capped at $20). Details: https://maritime.sh/pricing

The CLI describes itself offline: `maritime guide` prints its usage guide, and `maritime guide --json` emits the full command and flag manifest, introspected live from the installed CLI. Prefer that manifest, and `maritime <command> --help`, over guessing flags. `--dry-run` on any command prints the HTTP request without sending it.

## Setup

1. Install: curl -fsSL https://maritime.sh/install.sh | sh (no Node needed; it checks the download's sha512), or npm install -g maritime-cli (the npm package is maritime-cli; the command is maritime; needs Node 18+. If a flag below is unknown, run npm install -g maritime-cli@latest)
2. Authenticate, first path that works wins:
   - New user, or no plan yet: maritime onboard --json. It prints one JSON object whose step names what the user must do: sign_in (give them sign_in.url and sign_in.code), checkout (give them checkout_url: the 7-day free trial in Stripe Checkout, card required, nothing charged for 7 days) or billing_action (give them billing_url). After each step run maritime onboard --wait --json, until step is done. Do this before you build anything: no machine starts without a plan or the trial.
   - MARITIME_TOKEN in the environment (an mk_ key or a login token); the CLI reads it before anything else.
   - Any account, browser or not: maritime login --json prints one JSON line on stderr with user_code and verification_uri_complete; show both to the user and wait for their approval in any signed-in browser.
   - Email/password account, no browser: maritime login --email <email> --password <password>
   - No account, user present: maritime signup -e <email> -p <password> -n <name> --json (ask the user for the email and password; never invent them)
   - No account, do not stop: add --claim-later to maritime create. No login needed; the agent runs under a temporary account for 72 hours. The JSON response carries data.claimUrl (the user opens it to keep the agent) and data.token (a temporary key for that agent; export it as MARITIME_TOKEN to operate it). One agent only; template agents must bring their own model key (-e ANTHROPIC_API_KEY=...).
3. Verify: maritime whoami --json prints {authenticated, source, method, expires_at} and exits 4 when no credential is set or a login token expired. It does not call the API, so a wrong mk_ key still reports authenticated; maritime doctor checks it.
4. The email verification wall: until the user clicks the link Maritime emailed them, create, deploy, start, restart, keys create, and computer creation are refused with HTTP 403 and email_not_verified in the error (exit 4). Ask the user to click the link. To get it again they sign in at https://maritime.sh/login, which offers to resend it. Then retry the same command. List, status, logs, and templates work before verification.
5. Long-running automation: once verified, mint a key that never expires: maritime keys create --name my-agent --json (data.raw_key is shown once), and export MARITIME_TOKEN=mk_...

Full setup procedure, step by step: https://maritime.sh/agents.md

MARITIME_API_URL overrides the API base (default https://api.maritime.sh).

## The contract

- Pass --json on EVERY command. Results go to stdout: reads print {"meta": {...}, "results": ...}; writes print {"action", "resource", "path", "status", "success", "data"}; chat, status and whoami print their object directly.
  Errors print "Error: ..." on stderr; API errors include the HTTP status and the API's response body. A 409 also prints {"error": "...", "code": 5} on stdout. Branch on the exit code, never scrape human text.
- The global --agent flag sets --json --compact --no-input --no-color --yes.
  --compact keeps only key fields; leave it off when you need the full body.
- Exit codes: 0 success, 1 other error, 2 usage (unknown flag, missing argument, missing --yes), 3 not found (HTTP 404), 4 auth (no credential, expired, HTTP 401/403), 5 API error (other HTTP errors such as 402, 409, 422, 5xx), 6 partial failure, 7 rate limited (HTTP 429), 10 config.
- The top-level agent commands (chat, logs, status, env, delete and the rest) take the exact agent name or an ID prefix. Inside a directory linked with maritime init/link, the agent argument can be omitted. Commands whose help shows <agent_id> (exec, files, agents ...) need the full ID.
- In a linked directory, chat treats every argument as the message. To reach another agent from there, use maritime chat -a <agent> "...".

## Core tasks

# Discover templates, create an agent, wait for it, talk to it:
maritime templates --json
maritime create my-agent --template openclaw --json
maritime status my-agent --json          # wait for "active"
maritime chat my-agent "introduce yourself" --json | jq -r '.response'

# No account yet and nobody to ask: deploy now, claim later (72 h, 1 agent):
maritime create my-bot --repo https://github.com/you/agent --claim-later --json

# Deploy your own code (any framework; Dockerfile at the repo root):
maritime create my-bot --json
maritime deploy my-bot --source github --repo https://github.com/you/agent --branch main --wait --json

# Serve a repo on a public no-login URL (sleeps idle, wakes on the next visit):
maritime create my-app --repo https://github.com/you/app --public --port 3000 --json

# Encrypted env vars (secret by default; --reload pushes into the running agent):
maritime env set my-agent OPENAI_API_KEY=sk-... --reload --json
maritime env import my-agent ./prod.env --reload --json
maritime env list my-agent --json

# Inspect and debug:
maritime list --json
maritime logs my-agent --level error --json
maritime info my-agent --json
maritime history my-agent --json         # build/deploy attempts + buildLog
maritime exec <agent_id> --body-json '{"command":"ls /data"}' --json | jq -r '.results.stdout'

# Message an agent AS one of your end users (sticky per-user routing through the project front door; same --user always reaches the same instance):
maritime message my-agent --user cust_42 "where is my order?" --json | jq -r '.data.reply'

# Your end users (the people your product serves), each with their own agent:
# (end-users, alias users; bodies go in --body-json, fields as in the REST API)
maritime users upsert user_42 --display-name "Ada" --tags pro --json      # safe on every sign-in
maritime users create user_42 --body-json '{"templateId":"openclaw"}' --json   # their agent, exactly once
maritime users set-policy user_42 --body-json '{"policy":{"llm_spend_cap_cents_per_month":500}}' --json
maritime users policy user_42 --json                                      # read; clear-policy removes it
maritime users token user_42 --body-json '{"scopes":["chat"],"ttlSeconds":900}' --json   # eu_ token
maritime users list --json · get · agents · suspend · resume · delete
maritime usage get --from 2026-09-01 --to 2026-10-01 --json               # per agent, each row has externalUserId
maritime usage daily --json                                               # per day

# Lifecycle (rarely needed; agents sleep and wake on their own):
maritime start my-agent --json
maritime sleep my-agent --json
maritime restart my-agent --json         # also picks up new env vars
maritime delete my-agent --yes --json    # --yes is REQUIRED non-interactively

## Scheduling

Timers inside a sleeping agent do not fire. Put scheduled work on platform triggers: maritime triggers list <agent>, and maritime triggers create <agent> --type cron --cron "0 9 * * *" --prompt "...". A cron trigger wakes the agent and delivers --prompt as a message on each fire. A custom container can instead serve GET /schedules and Maritime registers the wakes for it. Details: https://maritime.sh/docs/triggers

maritime exec takes the full agent ID and a required --body-json:
{"command": "..." or ["argv", ...], "detached": false, "timeout": 60}. Words after the agent ID are not sent. timeout defaults to 60 seconds attached or 3,600 seconds detached. A detached call returns executionId at once; fetch or cancel it through the REST API: https://maritime.sh/docs/api#exec
On agents with detached support, disconnecting does not cancel execution. Do not repeat submission after a lost response without accounting for a command that may already be running. Older agents support attached exec with combined output and no execution ID. Detached submission on those agents returns HTTP 501 with unsupported: outdated vm. Please use a new agent. Use a new agent for detached execution. Restarting an older agent does not add this feature.

## Gotchas

- create and delete never hang without a terminal: a missing name, or delete without --yes, exits 2.
- Env changes apply on the next boot unless you pass --reload (or run maritime env reload <agent>).
- If chat fails with agent_unavailable in the error, the agent is not running: maritime start <agent>, wait a few seconds, retry.
- A 404 or "not running" on file/exec operations usually means the agent is asleep or errored: start it and retry; do not recreate it.
- mk_ keys never expire unless created with --expires-in-days; login tokens do. Use mk_ for automation.
- LLM access: most templates work with no model key (Maritime's model proxy, charged at the provider's list price, no markup, from your AI credits). The claude_code template is the exception: it always needs your own ANTHROPIC_API_KEY as a secret env var.

## Something wrong with these docs?

If a command, flag or example here does not match what the CLI does, or a call fails and the error does not tell you what to do, tell Maritime. Any mk_ key works. Put the X-Request-Id of the failing call in requestId when there is one and name yourself in tool. Never include keys, tokens or env values.

curl -X POST https://api.maritime.sh/api/v1/feedback \
    -H "Authorization: Bearer $MARITIME_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"kind":"cli","subject":"maritime env set","message":"<what went wrong and what you expected>","tool":"claude-code/2.1.0"}'

kind is one of docs, sdk, api, cli, computers, dashboard, other. Capped at 5 reports a minute and 20 a day per account. Reference: https://maritime.sh/docs/sdk/feedback
