# 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](https://sharkmagic.si). Once you are logged in, `shark keys create --name ci` makes more
  ([cli.md](cli.md#account)).
- 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.

```sh
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):

  ```sh
  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](https://dl.sharkmagic.si/) 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)

```sh
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:

```text
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:

```sh
shark whoami
```

```text
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](cli.md#account)).

### A2. Create a Space (30 s)

```sh
shark create --name demo
```

```text
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.

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

```text
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:

```sh
shark run demo -- 'ls && exit 3'; echo "exit code: $?"
```

```text
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)

```sh
shark fork demo -n 2
```

```text
tti-376de78ec13b0d33216c9abf966d6267
tti-9909f1e3035a270e6baeb024b6f54f87
```

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

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

```text
hello
```

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

```sh
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](concepts.md#exposed-processes-and-preview-urls)). It is **billed while awake**, by size: remove it
when you are done.

```sh
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

```sh
shark ls
shark rm demo
```

---

## Path B: the SDK

### B1. Install (1 min)

```sh
<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)

```python
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):

```text
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)

```ts
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:

```text
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):

```python
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)
```

```ts
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](api.md#sdk-spaces-and-devbox-spaces-profile).

### B4. Clean up

```python
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:

```sh
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:

```sh
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](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](errors.md). Next: [concepts.md](concepts.md), then point your agent at Shark with
[agents.md](agents.md) and the [AGENTS.md snippet](agents-md.md).

<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](numbers.md).</sub>