sharkmagic.docs
CLI

Agent? Fetch sharkmagic.si/docs/llms.txt or this page as markdown

CLI: shark#

One static binary for you and your agents (Linux, macOS; amd64, arm64). Install:

curl -fsSL https://dl.sharkmagic.si/install.sh | sh

It verifies the binary's sha256 against the release's SHA256SUMS before installing (details and the manual path: quickstart.md). shark version [--json] prints the build (version, commit, os, arch); shark update [--json] says whether a newer release exists (update_available, latest) and changes nothing; shark update --install downloads it, checks the same checksum and replaces the binary in place (error codes checksum_mismatch, update_check_failed, update_not_writable). SHARK_RELEASES_URL points both at a mirror with the same layout.

Cheat sheet#

shark login                                                # browser login with a one-time code
shark login --api-key -                                    # an account API key (stdin or a hidden prompt; CI: < keyfile)
shark whoami                                               # tenant (your account), credential, scopes
shark keys create --name ci > ci.key                       # an API key for CI (secret shown once)
shark org ls ; shark org use acme                          # personal workspace + teams; switch this login
shark org invite acme --email ana@example.com              # invite someone into a team (owner/admin)
shark code --repo acme/api                                 # a coding agent in the cloud on that repo (shark-code.md)
shark secrets set demo ANTHROPIC_API_KEY --preset anthropic --allow-egress   # value from a hidden prompt
shark egress add demo api.stripe.com                       # let the Space's commands reach one more host
shark quotas                                               # your limits, current use, limits class, node admission
shark quotas labels set end_user --awake 3                 # per end user: 3 Spaces awake (platforms)
shark create --name demo                                   # prints the Space id
shark run demo -- 'npm ci && npm test'                     # stream output, exit with the command's code
shark run demo --cwd web -- 'npm run build'                # run in a subdirectory
shark run demo --detach -- 'make long-job'                 # start it, print the op id, return
shark logs demo <op-id>                                    # re-attach to an op's output from anywhere
shark ops demo                                             # recent commands: op id, state, exit, shark_status
shark ps demo                                              # background jobs (bg-N); shark kill bg-1 demo stops one
shark fork demo -n 3                                       # three O(1) copies at the same commit
shark cp -r ./repo demo:repo                               # upload a directory (one commit)
shark cp demo:out/report.json .                            # download a file (no VM starts)
shark files ls demo [path]                                 # list files
shark hibernate demo ; shark wake demo                     # sleep now / wake now
shark usage --by space                                     # metered usage
shark ls ; shark rm demo                                   # list, delete

<space> is always a Space id or its name. Every verb has --help with examples (shark secrets --help), and shark --help lists them all.

Output, errors and exit codes#

Made to be driven by people and by agents alike:

  • --json on every verb that prints a result (ls, whoami, create, rm, fork, ops, hibernate, wake, quotas, usage, balance, topup, secrets, egress, github, stop, logs <space>, run --detach, login, keys, org, logins): stdout is then exactly one JSON document, the result or {"error": {"code", "message", "hint", "status"}}. Progress lines ([shark: …]) always go to stderr.
  • Errors in text mode: shark: <message> and, when there is a next step, next: <exact command> on stderr. The code is stable (not_logged_in, usage, not_found, missing_key, credits_exhausted, insufficient_scope, quota_exceeded, out_of_capacity, github_not_connected, clone_failed, not_a_terminal, confirmation_required, harness_required, cancelled, …; API codes as in errors.md). Never a stack trace.
  • Exit codes: 0 done; 1 the API refused or failed (the code says why); 2 usage error, or not configured / not logged in; 125 Shark could not report a result (run, logs), or the agent's session VM, its terminal channel or the agent's start failed (code, attach: the [shark: … (code 125) …] line says which); otherwise the command's own exit code (run, logs <space> <op>, run-agent).
  • No prompts you cannot avoid: a secret value comes from stdin when it is piped (a hidden prompt only on a terminal); --no-browser / --print where a browser would open; every confirmation has --yes. When stdin is not a terminal nothing asks: the command fails with the flag form in next: (not_a_terminal, confirmation_required). A question cancelled with q / Ctrl-C exits 130 (cancelled), nothing changed.
  • Colors only when stderr is a terminal; NO_COLOR=1 (or TERM=dumb) turns them off. Tables have a header only on a terminal, so scripts can split lines on tabs.

Configuration#

source keys
environment SHARK_API_KEY (an account key), SHARK_CP (account service URL; default https://api.sharkmagic.si), SHARK_API (a self-hosted node's URL: direct mode, below), SHARK_API_CA (a PEM to trust, for private nodes)
~/.shark/credentials written by shark login (mode 0600; ignored if group- or world-readable)
~/.shark/config KEY=VALUE lines, same keys (mode 0600)

The environment wins over the files. URLs must be https:// (plain http:// only to a loopback address, so a key never travels in clear).

Two modes. With an account key or a shark login (the hosted service), the CLI talks to the account service: it exchanges the key there for 5-minute node tokens and sends each call to the node that holds the Space. The key never reaches a node. Direct mode (SHARK_API set, or shark login --api-key - --api URL) sends the key to that one node as is: it is for a self-hosted node and that node's own keys. An account key in direct mode answers 401 unauthenticated, and the account verbs (balance, keys, org, …) answer needs_control_plane. To leave direct mode: unset SHARK_API and shark login --api-key - again (the login rewrites ~/.shark/credentials).

Account#

command what API
shark login --api-key [KEY|-] [--cp URL] an account API key (servers, CI, agents): checked against the account service (default https://api.sharkmagic.si) and stored in ~/.shark/credentials (0600). - (or nothing) reads stdin or a hidden prompt, so the key stays out of argv and shell history. Exchanged for node tokens, never sent to a node GET /v1/me, POST /v1/token
shark login [--cp URL] [--no-browser] browser login with a one-time code (RFC 8628): the CLI prints a link and a code, you confirm it on https://api.sharkmagic.si/activate (not signed in there: it sends you to its sign-in page, GitHub / Google / e-mail + password, and back to the code). The CLI then says Logged in, or exits 1 with the request was denied in the browser or the code expired; run shark login again (10 minutes). No browser at hand (a server, an agent): shark login --api-key - with an account key instead. Stores a refresh token (0600); every node call then carries a 5-minute token the CLI mints and refreshes itself. --no-browser only prints the link POST /v1/auth/device, /v1/auth/token
shark login --api-key [KEY|-] --api URL direct mode, for a self-hosted node: that node's own key, checked against its /v1/me and stored with the URL. Not for account keys GET /v1/me (node)
shark logout revokes the login session (if any) and deletes the credentials and token cache POST /v1/auth/revoke
shark whoami [--json] who this CLI acts as. With the account service (login or account key), --json is GET /v1/me of the account service plus control_plane: tenant (your account), kind (session or api_key), credential (its id), scopes, space (a Space-bound key, else null), expires_at, organization {id, name, slug, personal}, billing_mode, suspended. In direct mode (a node key, SHARK_API): tenant and credential {kind, id, scopes} GET /v1/me
shark quotas [--json] every limit that applies to you with current use, your limits_class (new, standard, partner, …), raise (how to get more), per-label limit rows (scope: label), and node_admission (is the node admitting new work: state open/closed, reason) GET /v1/quotas
shark quotas labels [ls] [--json] / set <key|key=value> [--awake N] [--spaces N] / rm <key|key=value> / clear per-label limits for a platform's end users (set end_user --awake 3: each end_user value gets 3 awake; end_user=acme caps that one value). Account service only (needs_control_plane in direct mode); a bad value exits 2 before any call (limits.md) GET /v1/limits, PATCH/PUT /v1/limits/labels
shark balance [--json] / shark usage [--by day|space|kind] [--json] prepaid balance; rated usage (account service; against a node directly: the node's meter) GET /v1/balance, /v1/usage
shark topup [USD] [--print] opens a Stripe Checkout page for prepaid credit (default $5); the credit shows once the payment is confirmed. At zero balance commands get 402 and files stay readable POST /v1/checkout
shark plan [status|subscribe [--print]|cancel] [--json] the monthly plan: a usage allowance each month, spent before prepaid credit, gone at the end of each period (no roll-over); subscribe opens Stripe Checkout (or resumes a pending cancel); cancel ends it at the period end GET /v1/plan, POST /v1/plan/subscribe, POST /v1/plan/cancel
shark invoices [--json] a one-time link to the Stripe customer portal (invoices, receipts, payment methods); 409 no_billing_customer before the first payment POST /v1/billing/portal
shark new [--name N] same as create; with the account service the Space goes to the node placement picks POST /v1/placement
shark keys ls|create|rotate|rm [--json] API keys of the org the CLI acts in. create --name N [--scopes "a b"] [--space SP] prints the secret once (alone on stdout; --json: the token field); rotate <id> [--overlap 1h] issues a new key and keeps the old one for the overlap (default 1 day); rm <id> revokes. Needs your login as owner/admin of the org, or a key with admin GET/POST /v1/keys, POST /v1/keys/{id}/rotate, DELETE /v1/keys/{id}
shark org ls|use|create|invite [--json] every account starts with a personal workspace (just you; nothing to set up); teams are opt-in. ls marks the one this login acts in; use <id|slug|personal> switches this login (keys, balance, usage and Spaces follow); create --name N [--slug S] makes a team you own; invite <id|slug> --email E [--role member|admin] invites someone into a team (owner/admin; they accept in the dashboard; the invitation lasts 48 h; the personal workspace takes no other members: 403 personal_workspace). An API key belongs to one org: use, create and invite need shark login (403 user_credential_required) GET/POST /v1/orgs, POST /v1/orgs/{org}/use, POST /v1/orgs/{org}/invitations
shark logins ls|rm <id> [--json] your CLI logins (each shark login): device, org, last use; rm ends one. (shark sessions is agent-session telemetry) GET /v1/logins, DELETE /v1/logins/{id}
shark sessions [--kind agent|api|shim] [--harness H] [--space ID] [--days N], sessions show <id>, sessions summary your agent sessions: turns, tool calls by type, MCP servers, commands by class, thinking vs executing time, errors by cause. Metadata only: never a prompt, command text, file or argument GET /v1/sessions, /v1/sessions/{id}, /v1/telemetry/summary
shark context [--json] inside a Space: where this shell runs and what works here (read from $SHARK_CONTEXT; no network, no key) —

Spaces#

command what API
shark create [--name N] [--label k=v]... [--idempotency-key K] [--profile devbox|sdk] [--account-secrets all|none|NAME,NAME] [--github on|off] [--git-name N] [--git-email E] [--keep-background on|off] [--keep-awake on|off] [--space-cli on|off|SCOPE,SCOPE] prints the new id; retried safely (an idempotency key is generated if you give none). The CLI makes devbox Spaces (the account's secrets and GitHub, background jobs, the Space CLI); --profile sdk makes one as the SDKs do (nothing of the account unless asked: api.md) POST /v1/spaces
shark ls [--label k:v] id state name created_at, all pages GET /v1/spaces
shark rm <space> [--purge] delete; --purge also deletes the durable copy DELETE /v1/spaces/{id}
shark fork <space> [-n N] [--account-secrets …] [--github on|off] [--keep-background on|off] [--keep-awake on|off] [--space-cli …] prints one id per fork (N ≤ 50); a fork keeps its source's profile and choices unless a flag says otherwise POST /v1/spaces/{id}/fork
shark hibernate <space> / shark wake <space> <id> hibernated / <id> ready (woke in X ms) POST …/hibernate, …/wake
shark checkpoint <space> waits until HEAD is in the durable store; prints JSON POST …/checkpoint
shark status <space> replication status JSON GET …/durable
shark space get <space> [--json] every setting of the Space in one view: name, labels, env, egress (policy and effective hosts), secrets (names, exposure, hosts), exposed ports (public or not), hibernation, durability, agent, limits GET /v1/spaces/{id} (settings)
shark space set <space> [--name N] [--label k=v] [--unlabel k] [--env K=V] [--unenv K] [--idle 30m|off|default] [--allow H] [--disallow H] [--public PORT=on|off] [--account-secrets all|none|NAME,NAME|default] [--github on|off|default] [--git-name N] [--git-email E] [--keep-background on|off|default] [--keep-awake on|off|default] [--space-cli on|off|default|SCOPE,SCOPE] [--harness AGENT|default [--harness-auth subscription|api-key]] change several settings in one request; every field is checked first and a refused one changes nothing. --allow/--disallow need network:write, --public, --keep-* and --space-cli need exec, --account-secrets/--github/--git-* need secrets:write. default = the profile's. --harness: the agent shark code starts without asking (devbox Spaces only) PATCH /v1/spaces/{id}
shark space defaults [--account-secrets all|none|default] [--github on|off|default] [--harness AGENT|default [--harness-auth M]] [--json] the account's defaults for every Space that does not set them (then the profile's); --harness: the account's default agent for devbox Spaces; no flag: show them GET/PUT /v1/space-defaults

Commands#

command what API
shark run <space> [--op-id ID] [--cwd DIR] [--detach] [--keep-background|--no-keep-background] -- <command...> runs the command line in a fresh microVM; streams stdout/stderr live; exits with the command's exit code. --keep-background: what it leaves running (cmd &) stays as a job bg-N; --no-keep-background: it ends with the command (unset: the Space's keep_background; sdk off, devbox on). A dropped connection resumes from the last event; the generated op_id makes retries exactly-once. Ctrl-C signals the command (INT), not only the CLI. --detach prints the op id and returns. Timeout: 2 h POST …/exec (SSE), …/ops/{op_id}/signal
shark ops <space> op_id state exit shark_status created_at, newest first GET …/ops
shark logs <space> <op-id> the op's full output (live if it still runs); exits with its code GET …/ops/{op_id}/events
shark ps [<space>] [--json] (alias processes) background jobs a command left running: id (bg-N) state ports started op cmd (the command redacted and cut as op logs show it). Inside a Shark session the Space is $SHARK_SPACE GET …/processes
shark kill bg-N [<space>] stops the job (TERM); what it wrote lands in the Space. shark service rm bg-N (what older job lines said) does the same POST …/processes/{pid}/signal
shark logs bg-N [<space>] [--tail BYTES] [-f] the job's output as committed (grows at its snapshots and end); -f follows it until the job ends GET …/processes/{pid}/logs
shark screenshot <url> [out.png] [--size WxH] [--wait S] [--budget-ms N] [--json] local, no API: waits for the URL, runs the headless browser once ($CHROME_PATH, else chrome-headless-shell/chromium on PATH), writes the PNG at the path given (relative to the command's cwd) and checks it whole (IEND, the asked size, synced); exit 0 only then. Errors no_browser (127), not_ready, screenshot_failed (no file left), bad_path (2: directory missing) —

shark run joins its arguments with spaces into one bash command line: quote the whole thing, shark run demo -- 'cd web && npm test'. Exit codes: the command's own code when it ran; 1 when the API refused it and nothing ran (for example the Space does not exist; the next: line says what to do); 125 when Shark could not report a result (the op goes on: shark logs <space> <op-id>); 2 for usage or configuration errors. When shark_status is not ok (conflict, timeout, infra error) it is printed on stderr.

Files#

command what API
shark cp <local> <space>:<path> upload a file (a path ending in / keeps the file name) PUT …/files
shark cp -r <localdir> <space>:<dir> upload a directory as one commit (skips .git/) POST …/files/archive
shark cp <space>:<path> <local>|- download a file (- = stdout) GET …/files
shark cp -r <space>:<dir> <localdir> download a directory GET …/files/archive
shark files ls <space> [path] type size path GET …/tree
shark files get <space> <path> [local] / put <space> <local> <path> one file GET / PUT …/files
shark files rm <space> <path> [-r] / mv <space> <from> <to> remove / move (one commit each) DELETE …/files, POST …/files/move
shark files push <space> <localdir> [remote] / pull <space> <remote> <localdir> directories archive routes

Reads never start a VM and never wake a hibernated Space.

Live: exposed processes, previews, terminal#

expose and open are live on the hosted node (P130 flow PASS on metal, 2026-11-07): each exposed process runs in its own microVM, private by default, sleeps after its idle time and wakes on the next request. It is billed while awake, by size; asleep it is not (billing.md): remove the ones you no longer need with --rm. shark connect is coming: it passed its local gates (P112) but is not yet verified on the hosted node. On a node without exposed processes every verb here prints the node's 422 unsupported.

command what API
shark connect <space> (coming) interactive shell: each line runs as one op; cd, export/unset and $? carry over; Ctrl-C interrupts; exit or Ctrl-D leaves. No TTY inside the command (use it for shells, not full-screen programs) WebSocket GET …/terminal
shark expose <space> <port> [--public] [--idle S] [--cwd DIR] [-e K=V]... -- <cmd...> run cmd as a sleeping server of the Space on port: starts on the first connection, sleeps after S s idle (default 30), sees every commit PUT …/ports/{port}
shark expose <space> --ls | --rm PORT | --logs PORT | --sleep PORT | --public PORT on|off list, remove (stops it and revokes every preview cookie), logs, put to sleep now (wakes on the next request), toggle public GET/DELETE/POST/PATCH …/ports…
shark expose --ls --all [--awake] [--json] every exposed process of the account, awake first: Space, port, state, public, last request, URL, and how many are awake (billed now); --awake only those running or starting. --json: {"data": [{space, space_name, port, state, awake, public, url, last_request_at, awake_since, …}], "awake": N} GET /v1/ports
shark open <space> <port> [--print] [--path /p] mints a one-time login link (60 s) to https://<id>-<port>.preview.sharkmagic.si and opens your browser (--print: only print it) POST …/ports/{port}/url

Expected (the output shape; the Space id is an example):

$ shark expose demo 3000 --cwd app -- node server.js
exposed node server.js on port 3000 (declared); it starts on the first connection and sleeps after 30s idle
preview: https://tti-ec8e46415dcc2dc3182daae2bc8a8aa2-3000.preview.sharkmagic.si/
private: open it with `shark open demo 3000`

Agent instructions: shark skills#

Local files only (no key, no network). The text ships inside the CLI; its source is skills/shark/ in the Shark repository (agents-md.md).

command what
shark skills install writes the Claude Code skill to $CLAUDE_CONFIG_DIR/skills/shark (else ~/.claude/skills/shark); files already identical are not rewritten
shark skills install --project DIR / --dir DIR the skill into DIR/.claude/skills/shark (a project skill you can commit, or copy into a Space with shark cp -r) / into DIR
shark skills install --agents-md FILE [--space NAME] [--no-skill] also (or only) writes the AGENTS.md snippet into FILE between <!-- shark:begin --> and <!-- shark:end -->, replacing an earlier block and keeping the rest of the file; --space puts your Space's name in it
shark skills install … --dry-run prints what it would create or update, writes nothing
shark skills snippet [--space NAME] prints the snippet

Secrets#

The value is read from stdin (pipe it), a hidden prompt or --from-file, never from argv (shell history and ps would keep it). Inject secrets (the default) never enter the VM: commands see a placeholder shk_ph_… and the node's egress proxy puts the real value only into requests to the scope's hosts (concepts.md).

Guided setup (a person at a terminal)#

shark secrets (or shark secrets setup [--account | <space>] [NAME] [--preset P …]) asks, one step at a time: where (the account, or one of your Spaces), the kind (a preset, custom, or the account's secret for a Space), the name, for custom the exposure (inject / exec, exec confirmed twice), hosts and where in the request, for the account which Spaces get it, for a Space whether to add the hosts to its egress, then the value (hidden, paste-friendly: a pasted newline does not submit; it says how long it is and whether it starts as the provider's keys do, e.g. sk-ant-, never what it is), and a review with the equivalent flag line. Nothing is sent before Save.

Keys: Enter = the default (forward), b or ← = back (the answers are kept; going forward again walks through them), q, Ctrl-C or Ctrl-D = cancel (nothing saved, exit 130), menus: ↑/↓ or a number. From the review you can jump to any step and come straight back. Flags given on the command line fill their steps. After saving it says what happens now ($NAME=shk_ph_…, the egress) and the next commands (shark secrets status, shark code). shark secrets set with a missing name or hosts on a terminal opens the same setup with what you gave; off a terminal it fails with the flag form.

Commands#

command what API
shark secrets set <space> NAME --preset P [--allow-egress] an inject secret for a known provider (below); --allow-egress also adds its hosts to the Space's egress allowlist PUT /v1/spaces/{id}/secrets
shark secrets set <space> NAME --host H [--host H2] [--in LOC]... an inject secret for any host; LOC is bearer (default), basic, header:NAME or query:PARAM same
shark secrets set <space> NAME --exec the value in the environment of commands that ask for it (exec with "secrets": ["NAME"]) and of terminals; any code they run can read it same
… --from-file F the value from a file instead of stdin
shark secrets set --account NAME --preset P [--apply all|ref] an account secret: set once, used by every Space of the account (all, default; new Spaces and forks too) or only by Spaces that reference it (ref). Same flags as a Space's (--host, --in, --exec, --from-file) PUT /v1/secrets
shark secrets set <space> NAME --from-account the Space uses the account secret NAME (needed for --apply ref); no value PUT /v1/spaces/{id}/secrets ({"ref": "account"})
shark secrets status [<space> | --account] [--agent claude|codex|opencode]... [--preset P]... what is missing: every secret of the scope (names only) with its state (ok, shadowed, unset reference, egress blocks a host, available to reference), last use, and each gap with the exact command that fixes it. Without --agent a Space is checked for the agent running in it. The node computes it (--json is its document); against an older node without that route the CLI computes the same document itself GET /v1/spaces/{id}/secrets/status, GET /v1/secrets/status
shark secrets ls (<space> | --account) a table: scope (space/account), name, preset, hosts, mode, apply, shadowed, last used (--json: the node's rows plus preset, uses, last_used) GET …/secrets, …/usage
shark secrets rotate (<space> | --account) NAME [--from-file F] a new value, the same exposure, hosts, locations and apply PUT …/secrets
shark secrets edit (<space> | --account) NAME [--preset P | --host H... | --add-host H | --rm-host H] [--in LOC...] [--exec | --inject] [--apply all|ref] [--allow-egress] change how a stored secret is used without typing the value again (on a terminal with no flags: the review screen of the setup) PUT …/secrets with "keep_value": true
shark secrets rm (<space> | --account) NAME [--yes] delete (from the next command); asks on a terminal, needs --yes otherwise DELETE …/secrets/{name}
shark secrets usage (--account | <space>) NAME per Space and host: requests that got the value, requests refused for carrying it out of scope, last use (never the value) GET /v1/secrets/{name}/usage, GET /v1/spaces/{id}/secrets/{name}/usage
shark secrets presets the presets GET /v1/secret-presets (same catalog)

A Space secret with the same name as an account secret always wins in that Space (ls and status show the account one as shadowed).

preset suggested name hosts where the value goes
anthropic ANTHROPIC_API_KEY api.anthropic.com header x-api-key
openai OPENAI_API_KEY api.openai.com Authorization: Bearer
github GITHUB_TOKEN github.com, api.github.com Basic (git over https, user x-access-token), Bearer or token header (REST API)
npm NPM_TOKEN registry.npmjs.org Authorization: Bearer (_authToken)
shark secrets                                                                  # guided setup (a terminal)
shark secrets set demo ANTHROPIC_API_KEY --preset anthropic --allow-egress    # prompts, input hidden
gh auth token | shark secrets set demo GITHUB_TOKEN --preset github
shark secrets set demo STRIPE_KEY --host api.stripe.com < stripe-key.txt
shark secrets status demo --agent claude                                      # what is missing, and the fix
shark secrets edit demo STRIPE_KEY --add-host files.stripe.com                 # no value needed
pbpaste | shark secrets rotate demo STRIPE_KEY
shark secrets rm demo STRIPE_KEY --yes
pbpaste | shark secrets set --account OPENAI_API_KEY --preset openai   # every Space, forks included
shark secrets usage --account OPENAI_API_KEY --json

Every setup step has a flag#

The review prints the flag line of what you chose (never the value); shark secrets set with those flags and the value on stdin stores exactly the same thing (tested: cmd/shark/secrets_wizard_test.go, TestSetFlagParity).

setup step answer flag
where the account / a Space --account / <space>
kind a preset / custom / the account's secret --preset P / --host … or --exec / --from-account
name NAME NAME
exposure inject / exec (default) / --exec
hosts api.x.com, *.x.com --host api.x.com --host '*.x.com'
where in the request Bearer / a header / Basic / a query parameter --in bearer / --in header:X-Api-Key / --in basic / --in query:key
which Spaces (account) every Space / only those that reference it (default) / --apply ref
egress (Space) add the hosts --allow-egress
value hidden prompt stdin, or --from-file F
value, kept (a stored secret) Enter shark secrets edit …
review Save / Cancel (the command runs) / (not running it)
rm's question yes --yes

status --json#

{"scope": "space", "space": "demo", "agents": ["claude"], "presets": [], "egress": {"mode": "allowlist", "source": "space"},
 "secrets": [{"name": "STRIPE_KEY", "scope": "space", "expose": "inject", "hosts": ["api.stripe.com"], "in": [{"kind": "bearer"}],
              "preset": "", "active": true, "shadowed": false, "missing": false, "egress_blocked": ["api.stripe.com"],
              "usage": {"available": true, "count": 7, "blocked": 0, "last_used": "2026-11-06T10:00:00Z"}}],
 "gaps": [{"code": "missing_secret", "severity": "error", "secret": "ANTHROPIC_API_KEY", "agent": "claude", "message": "…",
           "fix": "shark secrets set demo ANTHROPIC_API_KEY --preset anthropic --allow-egress < value.txt"},
          {"code": "egress_not_allowed", "severity": "error", "secret": "STRIPE_KEY", "hosts": ["api.stripe.com"], "message": "…",
           "fix": "shark egress add demo api.stripe.com"}],
 "ok": false}

ok is false while any gap has severity: "error"; the exit code is 0 either way (1 only when the API fails). Gap codes: missing_secret (a harness or --preset expects it; the fix is --from-account when the account has it as apply: ref), egress_not_allowed, account_secret_unset (a reference to no account secret), and the info ones shadowed, placeholder_blocked (requests carried the placeholder to a host outside the scope: usage shows where), egress_unknown (an older node does not say which hosts its egress allows). fix is a command to run as printed (a < value.txt means the value on stdin; HOST is yours to fill in).

Errors (stable codes, --json as everywhere): not_a_terminal (2: the guided setup without a terminal; the hint is the flag form), confirmation_required (2: rm without --yes off a terminal), cancelled (130: q, Ctrl-C or Ctrl-D in a question; nothing saved), no_value / bad_value (2: empty, under 8 or over 16 KiB, a control character, or a shk_ph_ placeholder pasted back), unknown_preset, unknown_agent (2), secret_is_account (2: rotate/edit of an account secret through a Space: the hint is the --account form), not_found (1), unsupported (1: a node too old for edit's keep_value; set it again with the value), usage (2).

A host the Space's egress does not allow is warned, with the command that fixes it. The old argv form NAME=VALUE still works (stored as --exec) but prints a deprecation warning; NAME=- and NAME=@file too.

Egress#

command what API
shark egress profiles [NAME] [--json] Shark's named host lists; with a name, its hosts with why each is there and how it was verified (binary:<pkg>@<version>, probe, docs:…, legacy), and what is left out on purpose GET /v1/egress/profiles
shark egress use <space> +PROFILE -PROFILE... add / remove profiles in one atomic change (-npm also takes npm out of essentials; +open/-open = mode open) PATCH …/egress {use}
shark egress show <space> [--effective] (get is the same) mode, profiles (and those Shark added by itself), your hosts, your deny list, where the policy comes from (space, default, host); --effective lists every host with the profile it comes from; --json has it all in effective GET /v1/spaces/{id}/egress
shark egress add <space> HOST... / rm <space> HOST... allow or stop allowing hosts of your own, keeping the rest (example.com, *.example.com, db.example.com:5432, 203.0.113.0/24:5432): one atomic change on the node. rm of a host not listed exits 1 not_found PATCH …/egress {add|remove}
shark egress deny <space> HOST... / undeny <space> HOST... refuse hosts (name or *.suffix) even when a profile has them (deny wins) / drop deny entries PATCH …/egress {add_deny|remove_deny}
shark egress denials <space> [--since 1h|30m|2d|RFC3339] hosts the egress proxy refused (names, ports, counts, first/last; never content), each with the profile that contains it and the command to allow it GET …/egress/denials
shark egress clear <space> remove every host you added (the profiles stay) PATCH …/egress {clear}
shark egress set <space> [--mode allowlist|deny|open] [--base coding|none] [--allow H,H] [--add H] [--remove H] [--auto on|off] change the policy (--allow replaces the list); open only where the account may (403 plan_restriction) same

Changes apply from the next command (the agent's session VM: from its next start); forks inherit them. shark code --agent X adds X's profile by itself (and with an API key the key's provider); shark secrets set … --preset P --allow-egress adds P's profile. A refused entry is 400 invalid_request with details.field (add[i]/allow[i]/use[i]/add_deny[i]), details.host or details.profile and details.reason (ip_without_port, private_range, cidr_without_port, bad_cidr, bad_port, bad_name, invalid_host, too_many_entries, unknown_profile, included_by, deny_needs_allowlist); --json prints them.

shark egress profiles                               # the catalog
shark egress use demo +opencode +openrouter -docker-hub
shark egress show demo --effective                  # every host and the profile it comes from
shark egress denials demo --since 1h --json         # what was refused, and which profile has it

GitHub#

Connect GitHub once; every Space of your account can then use git and gh on the repositories you chose (new Spaces, existing ones and forks, with no per-Space step). The node keeps a managed GITHUB_TOKEN account secret (inject, apply all) fresh: short-lived App installation tokens, minted before a command when the current one is missing or expires within 15 min. Your commands see a placeholder; git clone/fetch/push over https and gh work as usual (git@github.com: URLs are rewritten to https), and commits default to your GitHub noreply address unless the Space sets its own user.name/user.email.

command what API (account service)
shark github connect [--no-browser] [--wait] [--install] opens GitHub's authorization for the Shark GitHub App: installations of the App you can already access (your account, your organizations) are found and linked; with none, GitHub continues to the install page, where you pick your account and/or organizations and the repositories. --wait returns once the installation arrived. --install opens the install page itself (to add an organization). --json: url (the link to open), install_url, connect_url. Needs secrets:write (it puts an account credential in your Spaces, like an account secret): a shark login has it; an API key needs it in its scopes (403 insufficient_scope, details.required) POST /v1/github/install
shark github status [--no-check] the installations (default owner, token health, repository count), the commit identity, the node's managed GITHUB_TOKEN (expiry, owners) and the Spaces that used it GET /v1/github/status?check=1, node GET /v1/secrets, …/GITHUB_TOKEN/usage
shark github disconnect [--installation ID] [--keep-on-github] --yes forget the connection (all, or one installation). By default the App is uninstalled on GitHub, which revokes its tokens; every Space loses GITHUB_TOKEN within seconds DELETE /v1/github[/installations/{id}]
shark github token <space> --repo owner/name a token for that one repository (contents and pull requests, ≤ 1 h) stored as the Space's own GITHUB_TOKEN (it wins over the account's there); never printed POST /v1/github/token, then PUT …/secrets
shark github link <space> / unlink <space> which repository a per-Space token is for (its node keeps it fresh while linked) / stop that GET / DELETE /v1/spaces/{id}/github

With several installations (your account and organizations), requests that name a repository (github.com/<owner>/…, api.github.com/repos/<owner>/…) get that owner's token; others (GraphQL, /user) get the default one (your own account's). shark code --repo needs no token step when the account is connected. Without the App (503 github_not_configured), gh auth token | shark secrets set --account GITHUB_TOKEN --preset github works.

The account's connection reaches devbox Spaces. An sdk Space (made by the API/SDK) gets it only with github: true (shark space set <space> --github on); its commit identity is --git-name/--git-email (git: {name, email}). A platform whose users install its own GitHub App registers it once and links each Space to one installation:

command what API (account service)
shark github app register --app-id ID --private-key-file app.pem [--json] the App once; the key is sealed, never shown again PUT /v1/github/app
shark github app status [--json] registered?, app id, slug, linked Spaces GET /v1/github/app
shark github app link <space> --installation ID [--repo NAME]... [--json] a token of that installation (narrowed to --repo) stored as the Space's GITHUB_TOKEN; its node refreshes it while the Space is used POST /v1/github/app/link, then PUT …/secrets
shark github app rm --yes [--json] forget the App and its links DELETE /v1/github/app

SDK: shark.github_app.register(app_id, pem), .link(space, installation_id, repos=[…]), .status(), .remove() (TS: shark.githubApp.*). Or, with a token you mint yourself, per Space: shark secrets set <space> GITHUB_TOKEN --preset github < token.txt (SDK: space.secrets.set("GITHUB_TOKEN", tok, preset="github")).

Coding agent and terminals in a session VM#

Details: shark-code.md.

command what
shark code [--repo owner/name|URL] [--branch B] [--work-branch W] [--agent claude|codex|opencode] [--auth subscription|api-key] [--remember space|account] [--view] [--continue | --resume [ID]] [space] create or wake the Space, clone the repository once (work branch shark/<space>), start or join the agent, attach. --continue / --resume [ID] start the agent on a past conversation kept in the Space. Ctrl-\ detaches. Which agent: --agent (or $SHARK_AGENT) > the Space's remembered one > the account's > on a terminal, the first-run questions > exit 2 harness_required (no terminal, or --json, never asks; shark-code.md)
shark code --agent list [--json] the coding agents this node runs, with the logins each supports (GET /v1/agents; the CLI's own list, source: "cli", from an older node)
shark code --forget [space|account|all] [space] forget the remembered agent of the Space (default), the account's, or both; starts nothing. --json: {"space", "forgot": [...], "still"}
shark code --sessions [--agent A] [--json] [space] the agent's past conversations in the Space: id, start, last activity, size (never their content)
shark attach <space> [--view] [--shell] [--resume ID] [--no-escape] [-- cmd...] join the running agent; with no agent running (or --shell) a terminal: a login shell in the Space's session VM. Plain attach never starts an agent; an agent you type there (claude, codex, opencode) runs as under shark code (shark-code.md). ~. after Enter detaches
shark logs <space> / shark stop <space> / shark run-agent <space> "<task>" what the agent printed / end it / a headless run

Usage#

command what API
shark usage [--by space|day] [--json] key commands N command_s S per row GET /v1/usage

Other binaries (self-hosted and harness integration)#

binary for page
shark-bash runs unmodified Claude Code's Bash tool in Shark leases (a shell shim) agents.md
shark-rg answers Claude Code's Grep and Glob from an index instead of walking a FUSE mount agents.md
sharkfn Read, Write, Edit, Glob, Grep served from a Space's committed tree; an MCP server (sharkfn serve -stdio) agents.md
sharkfs the node itself (sharkfs api), the SharkFS mount, durability and the usage meter operators only
shark service … declares Postgres/Redis on the host side of a Space (catalog, add postgres@17, rm, ls, checkpoint, env); as a client, commit .shark/services.toml instead (concepts.md)

shark service catalog (works anywhere):

postgres@16  port 5432  2 vCPU 1024 MiB  env DATABASE_URL PGDATABASE PGHOST PGPASSWORD PGPORT PGUSER POSTGRES_URL
postgres@17  port 5432  2 vCPU 1024 MiB  env DATABASE_URL PGDATABASE PGHOST PGPASSWORD PGPORT PGUSER POSTGRES_URL
redis@7      port 6379  1 vCPU  512 MiB  env REDIS_HOST REDIS_PORT REDIS_URL

Without SHARK_API and a key, shark create <dir>, shark fork, shark log, shark head and shark checkout work on a local Space store (SHARK_HOME): the early prototype of the content-addressed Space, no server and no VM.