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:
--jsonon 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. Thecodeis 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:
0done;1the API refused or failed (the code says why);2usage error, or not configured / not logged in;125Shark 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/--printwhere a browser would open; every confirmation has--yes. When stdin is not a terminal nothing asks: the command fails with the flag form innext:(not_a_terminal,confirmation_required). A question cancelled with q / Ctrl-C exits130(cancelled), nothing changed. - Colors only when stderr is a terminal;
NO_COLOR=1(orTERM=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.
