sharkmagic.docs
Quickstart

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

Quickstart#

From zero to a command running in a Space, then a fork of it. Two paths, each under 10 minutes: the CLI and the SDK (Python or TypeScript). Both talk to the same API.

0. What you need#

  • An account API key. Shark is in early access: your invite contains an account key (shk_live_…). Request access at sharkmagic.si. Once you are logged in, shark keys create --name ci makes more (cli.md).
  • Keep the key in the environment or in the CLI's credentials file. Never paste it into code, into a file of the Space, or into an agent's prompt.
export SHARK_API_KEY=...        # from your invite; the CLI can store it for you instead (step A1)

That is the whole configuration. The key belongs to the account service (https://api.sharkmagic.si, the default): the CLI and the SDKs exchange it there for short-lived node tokens and send each call to the node that holds your Space (the hosted node (https://hel1.sharkmagic.si)). The key itself never goes to a node.

Do not set SHARK_API. SHARK_API (or shark login --api …) puts the CLI and the SDKs in direct mode: the key goes to that one node as is, and only a self-hosted node's own keys work there. An account key in direct mode answers 401 unauthenticated. If you set it earlier: unset SHARK_API, then shark login --api-key - again.

Installing.

  • CLI (Linux and macOS, amd64 and arm64; one static file):

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

    The script picks the build for your OS and CPU, checks its sha256 against the release's SHA256SUMS and refuses on any mismatch, then installs shark into ~/.local/bin (/usr/local/bin as root; SHARK_INSTALL_DIR=… to choose, SHARK_VERSION=0.2.0 to pin). Prefer to look first: download install.sh from the same page, read it, run it. Or take the binary for your platform and SHA256SUMS from the download page and check it with sha256sum -c --ignore-missing SHA256SUMS. Later: shark update says whether a newer release exists; shark update --install replaces the binary (same checksum check).

  • SDKs: not on PyPI/npm yet (the package names are being decided); your invite links the build. The commands below use these placeholders:

placeholder what it installs provisional name
<PY_SDK_INSTALL> the Python SDK (3.9+, no dependencies) pip install shark-sdk, imported as shark
<TS_SDK_INSTALL> the TypeScript SDK (Node 22.18+, no dependencies; ESM with types) npm install @sharkmagic/sdk

From a checkout of the source, the same things are go build -o ~/bin/shark ./cmd/shark, pip install ./sdk/python and npm install ./sdk/typescript (its prepack builds dist/).


Path A: the CLI#

A1. Install and log in (1 min)#

curl -fsSL https://dl.sharkmagic.si/install.sh | sh
shark login --api-key -        # paste the key at the prompt; it never enters argv or history

Expected:

Logged in with an API key: tenant <your-account>. Credentials in /home/you/.shark/credentials (0600).

From a script or CI, pipe it (shark login --api-key - < keyfile) or just export SHARK_API_KEY. At a terminal, shark login alone logs you in through the browser with a one-time code instead (a login of yours, not a key).

Check it:

shark whoami
tenant <your-account> (<your-workspace>), api_key key:…, scopes [exec files:read files:write spaces:read spaces:write usage:read]
control plane https://api.sharkmagic.si

Every account starts with a personal workspace (just you; nothing to set up). Teams are opt-in: shark org create, shark org invite (cli.md).

A2. Create a Space (30 s)#

shark create --name demo
tti-ec8e46415dcc2dc3182daae2bc8a8aa2

The id is <prefix>-<32 hex>; every command below accepts the id or the name (demo).

A3. Run commands (1 min)#

Quote the whole command line: shark run sends it to bash in the Space.

shark run demo -- 'echo hello > hello.txt && cat hello.txt && uname -s'
hello
Linux

That command ran in a fresh microVM that no longer exists; hello.txt was committed to the Space. The next command sees it, and shark run exits with the command's own exit code:

shark run demo -- 'ls && exit 3'; echo "exit code: $?"
hello.txt
exit code: 3

Output streams while the command runs. If your connection drops, shark run reconnects and resumes the output; the command never runs twice (each call has an op_id). Ctrl-C stops the command, not just the CLI.

A4. Fork it (30 s)#

shark fork demo -n 2
tti-376de78ec13b0d33216c9abf966d6267
tti-9909f1e3035a270e6baeb024b6f54f87

Each fork is a new Space at the same commit, with nothing copied. Run something in one:

shark run tti-376de78ec13b0d33216c9abf966d6267 -- 'cat hello.txt'
hello

A5. Move files in and out (1 min)#

shark cp -r ./my-project demo:my-project      # upload a directory (one commit)
shark cp demo:hello.txt -                     # print a file; reads never start a VM
shark files ls demo

A6. Exposed server and preview#

Live on the hosted node: the server runs in its own microVM, private by default, sleeps when idle and wakes on the next request (concepts.md). It is billed while awake, by size: remove it when you are done.

shark expose demo 3000 --cwd app -- node server.js  # a server that sleeps when idle and wakes on request
shark open demo 3000                                # one-time login link (60 s) to https://<id>-3000.preview.sharkmagic.si
shark expose demo --logs 3000                       # its output
shark expose demo --rm 3000                         # stop it and revoke the preview cookies

shark connect demo (an interactive shell where each line runs as one command and cd/export carry over) is coming: it passed its local gates but is not yet verified on the hosted node.

A7. Clean up#

shark ls
shark rm demo

Path B: the SDK#

B1. Install (1 min)#

<PY_SDK_INSTALL>       # Python
<TS_SDK_INSTALL>       # TypeScript

Both read SHARK_API_KEY from the environment (Python also ~/.shark/credentials, written by shark login) and go through the account service for you, like the CLI. Leave SHARK_API unset (direct mode is for self-hosted nodes).

B2. Python: create, run, read a file, fork (3 min)#

from shark import Shark

shark = Shark()                                    # SHARK_API_KEY
space = shark.spaces.create(name="quickstart")
print(space.id, space.state)

r = space.run("echo hello > hello.txt && cat hello.txt")
print(r.exit, r.stdout.strip(), r.shark_status)

print(space.files.read_text("hello.txt").strip())  # served from the committed tree, no VM

for fork in space.fork(count=2):
    print(fork.id, fork.run("cat hello.txt").stdout.strip())

print("exit", space.run("exit 7").exit)            # a failing command is a result, not an exception

Expected (ids differ):

tti-a47ffe076f139d770d99dce9a6db0ffe ready
0 hello ok
hello
tti-e0b78dd7df202284936c5803a9cdb88a hello
tti-79083d0f699a6b800adba0a1742329d3 hello
exit 7

Useful next calls: space.run(cmd, cwd=..., env=..., timeout=..., check=True), space.stream(cmd) (live events), space.start(cmd) (returns an Op at once), space.files.write(...), space.files.upload_dir(...), space.hibernate(), space.delete(). Async: AsyncShark.

B3. TypeScript: the same (3 min)#

import { Shark } from "@sharkmagic/sdk";

const shark = new Shark();                          // SHARK_API_KEY
const space = await shark.spaces.create({ name: "quickstart-ts" });
console.log(space.id);

const r = await space.run("echo hello > hello.txt && cat hello.txt");
console.log(r.exit, r.stdout.trim(), r.sharkStatus);

const [fork] = await space.fork({ count: 1 });
console.log(fork.id, (await fork.run("cat hello.txt")).stdout.trim());

Expected:

tti-8d5d6fa7ea7287b3cc76d720830cbb92
0 hello ok
tti-d795718d53c4d099f95ada76cdac1e87 hello

B3b. What an SDK Space gets (1 min)#

Spaces made by the SDK are sdk Spaces: your account's secrets, your GitHub connection and background jobs do not reach them unless you ask, per Space (a platform's end users never see your keys):

sp = shark.spaces.create(labels={"customer": "acme"}, egress="coding")   # nothing of the account
sp.secrets.set("STRIPE_KEY", key, hosts=["api.stripe.com"])             # this Space's own secret (inject)
sp.update(account_secrets=["OPENAI_API_KEY"], github=True)               # opt in: one account secret, GitHub
sp.run("python worker.py &", background=True)                           # keep what it leaves running
for s in shark.spaces.list(label="customer:acme"): print(s.id)
const sp = await shark.spaces.create({ labels: { customer: "acme" }, egress: "coding" });
await sp.secrets.set("STRIPE_KEY", key, { hosts: ["api.stripe.com"] });
await sp.update({ accountSecrets: ["OPENAI_API_KEY"], github: true });

profile: "devbox" (profile="devbox") makes one like shark create does. Every field: api.md.

B4. Clean up#

for s in shark.spaces.list(name="quickstart"):
    s.delete()

Path C: raw HTTP (for any language)#

An account key goes only to the account service. Exchange it there for a node token (5 minutes; ask for a new one before it expires); the answer also names the node to call:

curl -sS -X POST https://api.sharkmagic.si/v1/token -H "Authorization: Bearer $SHARK_API_KEY" \
  -H 'Content-Type: application/json' -d '{}'
# {"token":"…","token_type":"Bearer","expires_in":300,"tenant":"…","scopes":[…],"space":null,"node":{"id":"hel1","url":"https://hel1.sharkmagic.si"}}
export SHARK_NODE=https://hel1.sharkmagic.si    # node.url from the answer
export SHARK_TOKEN=…                            # token from the answer (never log it)

Then call the node with the token:

curl -sS -X POST "$SHARK_NODE/v1/spaces" -H "Authorization: Bearer $SHARK_TOKEN" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: qs-create-1' -d '{"name":"curl-demo"}'
# {"id":"tti-…","state":"ready","name":"curl-demo",…}

curl -sS -X POST "$SHARK_NODE/v1/spaces/curl-demo/exec" -H "Authorization: Bearer $SHARK_TOKEN" \
  -H 'Content-Type: application/json' -d '{"cmd":"echo hello","op_id":"qs-1"}'
# {"exit":0,"shark_status":"ok","stdout":"hello\n","stderr":"","op_id":"qs-1","replayed":false,…}

curl -sS -X POST "$SHARK_NODE/v1/spaces/curl-demo/fork" -H "Authorization: Bearer $SHARK_TOKEN" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: qs-fork-1' -d '{"count":1}'
# {"spaces":[{"id":"tti-…","fork_of":"tti-…","fork":{"bytes_copied":0,…},…}]}

For an existing Space, {"space": "<id>"} in the token request returns the node that holds it. The SDKs and the CLI do all of this (tokens, refresh, placement) for you.

Send the same exec again with "op_id":"qs-1": you get the stored result with "replayed": true, and the command does not run a second time. Every endpoint: api.md.

If something fails#

you see it means do
not logged in / 401 unauthenticated no key, a wrong or revoked one, or SHARK_API is set (direct mode: account keys do not work there) unset SHARK_API, then shark login --api-key -, or check SHARK_API_KEY
404 not_found on your Space wrong id or name, or it belongs to another account shark ls
429 lease_cap_reached your account already runs the maximum commands at once wait Retry-After seconds; the CLI and SDKs do it for you
429 quota_exceeded an account cap: Spaces held (details.quota: spaces) or awake at once (awake) shark quotas; delete a Space, or shark hibernate one you are not using
402 credits_exhausted balance at zero top up; files stay readable
422 unsupported the route exists in the spec but not on this node yet see the page for that feature

All codes: errors.md. Next: concepts.md, then point your agent at Shark with agents.md and the AGENTS.md snippet.

<sub>Expected outputs were captured on 2026-11-05 against the real sharkfs api and these exact CLI and SDK builds, on a local server whose commands ran without a VM (so no timings are shown here); the login and whoami lines show the CLI's format with an account key (updated 2026-11-07). Account keys working on the hosted node through the account service (key → node token → command → metered usage → balance → revocation) was checked end to end on 2026-11-07. Measured timings on metal: numbers.md.</sub>