# shark code: your coding agent in the cloud

> **Status: beta, validated locally only.** Every check below passed on our local rig with a fake terminal agent and
> a fake microVM (`proofs/p116-shark-code`, results `r3`). The real agents (versions below) were then run headless on
> a local machine with Shark's shell and a fake model: each one started with Shark's configuration, sent its shell
> command through Shark's shim and exited 0 (`proofs/p116-shark-code/HARNESSES.md`, results `smoke-r1`). The same
> checks on real microVMs are pending. Until then, treat this page as the design you will get, not a measured promise.

`shark code` runs Claude Code, Codex or OpenCode **inside your Space**, and shows its full terminal UI in your
terminal: colors, resizing, Ctrl-C, everything. Close the laptop and the agent keeps working; attach again from any
machine. No SSH, no keys to copy, no ports to open.

```text
$ shark login
$ shark secrets set myproj ANTHROPIC_API_KEY --preset anthropic --allow-egress
Value for ANTHROPIC_API_KEY (input hidden):
set ANTHROPIC_API_KEY (inject → api.anthropic.com, header:x-api-key) on myproj
egress: profile anthropic-api on (shark egress show myproj)
egress: now allows api.anthropic.com
$ shark code --repo acme/api myproj
[shark: cloning acme/api into myproj]
shark: on branch shark/myproj of https://github.com/acme/api.git
[shark: agent on myproj (writer); Ctrl-\ detaches, the agent keeps running]
… the agent's UI …
^\
[shark: detached; the agent keeps running. shark attach myproj | shark logs myproj | shark stop myproj]

$ shark ls
tti-3f…   ready   myproj   2026-11-05T09:12:03Z   claude waiting-for-you 4m0s
$ shark attach myproj            # from any machine; shows the last 64 KiB first
$ shark attach myproj --view     # watch without typing (any number of viewers, one keyboard)
$ shark logs myproj              # what it printed since you last left
$ shark stop myproj
$ shark run-agent --agent codex myproj "fix the failing test"   # no terminal: streams output, exits with the agent's code
```

Without a Space name, `shark code` uses (or creates) a Space named after the repository (`--repo`), else after the
current directory. Every verb takes `--help` with examples; `stop`, `logs` and `ls` take `--json`.

## Choosing the agent: `--agent`, `--remember`, `--forget`

The first time you run `shark code` on a terminal without `--agent`, it asks which agent to run and how it logs in,
then offers to remember the answer. Nothing is saved before the last answer; `q` or Ctrl-C cancels.

```text
$ shark code myproj
shark code: first run in myproj. Pick the coding agent (asked once if you remember it; scripts pass --agent).
Which coding agent?
  › 1) Claude Code  Anthropic's coding agent in the terminal (your Anthropic subscription login or an Anthropic API key)
    2) Codex  OpenAI's coding agent CLI (your OpenAI subscription login or an OpenAI API key)
    3) OpenCode  the open-source coding agent, any provider (its own login, or an Anthropic or OpenAI API key)
How does Claude Code log in?
  › 1) subscription login  log in inside the agent on its first start (your plan with the provider); …
    2) API key  a provider key (ANTHROPIC_API_KEY) stored as a Space or account secret; the VM only sees a placeholder
ANTHROPIC_API_KEY is not set for myproj. Add it now?          (API key only, and only when it is missing)
  › 1) add it to myproj  the guided secrets setup (preset anthropic); the value is asked hidden
    2) add it to the account  every devbox Space of the account gets it
    3) not now  shark code stops here; add it later with shark secrets
Remember Claude Code (api-key)?
  › 1) for this Space (myproj)  shark code myproj starts it without asking
    2) as the account's default  every devbox Space without its own choice
    3) no  ask again next time
[shark: created Space myproj (tti-…)]
[shark: remembered claude (api-key) for myproj; change: shark code --agent A --remember space, forget: shark code --forget space]
… the guided secrets setup (shark secrets setup), then the agent starts …
```

The key is never typed into `shark code` itself: the API-key choice runs the same guided setup as `shark secrets`
(hidden value, preset for the provider's host).

**Which agent runs, first match wins:**

1. `--agent A` (or `$SHARK_AGENT`)
2. the Space's remembered agent
3. the account's default (devbox Spaces only)
4. on a terminal: the first-run questions above
5. otherwise: exit 2, `harness_required`, with the command to run. **Without a terminal, or with `--json`, nothing is
   ever asked.**

`--auth` follows the same order: the flag, else the remembered login when the agent is the remembered one, else the
node's default (`api-key`). `--auth key` is short for `api-key`.

```bash
shark code --agent list [--json]                                  # the agents this node runs, and how each logs in
shark code --agent codex --auth subscription --remember space myproj   # remember for this Space (then starts it)
shark code --agent claude --remember account myproj               # the account's default for every devbox Space
shark code --forget myproj                                        # forget the Space's choice (--forget account | all)
shark space set myproj --harness codex --harness-auth subscription     # the same, without starting anything
shark space defaults --harness claude                             # the account's default, without starting anything
```

The choice lives in the API, not on your laptop: the Space's in its settings (`PATCH /v1/spaces/{id}`
`{"harness": {"agent", "auth"}}`, inherited by forks), the account's in `PUT /v1/space-defaults` `{"harness": …}`;
`null` forgets. `GET /v1/spaces/{id}` shows `settings.harness` with its `source` (`space`, `account`, `none`).
`GET /v1/agents` lists what the node's image has. SDK Spaces (`profile: "sdk"`) never get a harness: the account's
default does not reach them and setting one is refused (`invalid_request`, `reason: sdk_profile`).

## Your repository: `--repo`

```bash
shark code --repo acme/api                         # owner/name = github.com; or any https:// clone URL
shark code --repo acme/api --branch develop        # start from that branch (default: the repository's default)
shark code --repo acme/api --work-branch fix-login # work on that branch (default: shark/<space>; created if missing)
```

- The clone runs **once**, as one command in the Space (through its egress, like any command), into the Space's root.
  Run `shark code --repo …` again and the clone is kept as it is; a Space that already holds another repository is
  refused (`clone_failed`, with the fix).
- Your `git config user.name` / `user.email` from this machine are set in the clone (if you have them).
- **Git credentials never enter the VM.** With the Shark GitHub App connected (`shark github connect`, coming), the CLI
  stores a token for that one repository (contents and pull requests, ≤ 1 h, refreshed every 45 min while `shark code`
  runs) as the Space's `GITHUB_TOKEN` inject secret. Without it, a public clone works, and for pushes:
  `gh auth token | shark secrets set <space> GITHUB_TOKEN --preset github`. In both cases the clone's git credential
  helper hands git the placeholder, and the egress proxy swaps in the token only toward `github.com` and
  `api.github.com`.

## Agents and versions

| `--agent` | agent | version in the session VM image |
|---|---|---|
| `claude` | Claude Code | 2.1.318 |
| `codex` | Codex CLI | 0.166.0 |
| `opencode` | OpenCode | 1.19.5 (2.x is not supported yet) |

The versions are pinned in the image (they do not self-update); a new version ships only after the same checks pass
with it. There is no built-in default agent: see [Choosing the agent](#choosing-the-agent---agent---remember---forget).

**First start.** With an API key, Claude Code skips its first-run setup and asks one question: whether you trust the
folder (once per Space; it is remembered). Codex and OpenCode open ready. With `--auth subscription`, each agent shows
its own login the first time.

**`shark run-agent` runs the agent unattended**: it approves the agent's own permission prompts (Claude Code
`--permission-mode bypassPermissions`, Codex without approvals, OpenCode `--auto`) and prints the agent's JSON events.
Every command it runs still goes to its own microVM, as always. Interactive `shark code` keeps the agent's normal
approval prompts.

## How it works

- The agent runs in the Space's **session VM**. Its shell is Shark's shim: every command the agent runs (`npm test`,
  `git`, builds) runs in **its own fresh microVM** next to your files, exactly as with `shark run`. The session VM only
  holds the agent itself.
- Your terminal talks to the agent over the same authenticated channel as the rest of the API (`shark-pty.v2`). A
  browser can attach the same way over WebSocket (protocol: `docs/spec/v1/shark-pty-v2.md`).
- **States** in `shark ls`: `working`, `waiting-for-you` (it is asking you something or finished), `idle`, `asleep`.
- **Notifications**: when the agent waits for you and nobody is attached, Shark can call your webhook, send a
  Telegram message or an email (configured per account; the message never contains anything from the screen).

## The agent and the terminal

`shark code` starts the agent. `shark attach <space>` (when no agent runs, or with `--shell` or `-- cmd`) opens a
**terminal**: a login shell (`bash -l`) in the session VM, as the Space's user, in the Space's root. It never starts an
agent by itself.

- **Commands you type in the terminal run in the session VM** (2 vCPUs, the Space's files), like on any machine.
  **The agent's commands go each to its own microVM.** Use `shark run` or the agent for heavy work.
- **An agent you start by hand in the terminal** (`claude`, `codex`, `opencode` typed at the prompt, or after the
  agent exits) runs as under `shark code`: a small wrapper Shark puts first on the terminal's `PATH` gives it Shark's
  shim as its shell (each command in its own microVM), its config and login in the VM's memory-only directory
  (never in the Space's files, commits or forks), and Shark's context (`$SHARK_CONTEXT`, its instructions file). The
  terminal itself also carries the few variables only these agents read (`CLAUDE_CODE_SHELL`, `CLAUDE_CONFIG_DIR`,
  `CODEX_HOME`), so Claude Code started by its full path routes too; your shell's `$SHELL`, `$HOME` and `XDG_*` stay
  yours (Shark's shim refuses interactive shells, so it is never your `$SHELL`).
- A login made this way is saved to your encrypted store on the node only while a `shark code --auth subscription`
  session of the same agent runs in the Space (they share the directory); otherwise it stays in the VM's memory and is
  gone when the VM is replaced. For a login that lasts, use `shark code --auth subscription`. When a `shark code`
  agent stops, its directory is erased, including a login made by hand.
- Not routed: an agent your login profile puts ahead of Shark's wrapper on `PATH` (Codex and OpenCode then run their
  commands in the session VM; Claude Code still routes and keeps its login out of the Space through the variables
  above), or an image without Shark's shim (the wrapper says so on start).

## Conversations: `--continue`, `--resume`, `--sessions`

The agent's conversations are kept **in the Space**, so the next `shark code` can pick one up, after the agent
exits, the VM sleeps or restarts, or the node restarts:

```text
$ shark code --continue myproj                 # the last conversation (claude --continue)
$ shark code --resume myproj                   # the agent's own picker (Claude Code, Codex)
$ shark code --resume 0b4f3c2e-1d5a-4e6f-8a9b-0c1d2e3f4a5b myproj
$ shark code --sessions myproj                 # past conversations: id, start, last activity, size
ID                                    STARTED           LAST ACTIVITY          SIZE
0b4f3c2e-1d5a-4e6f-8a9b-0c1d2e3f4a5b  2026-11-06 10:00  2026-11-06 11:42   1.2 MiB
$ shark code --sessions myproj --json          # {"sessions":[{"id","started","last_activity","bytes"}],"bytes",...}
```

| | `--continue` | `--resume [ID]` | `--sessions` |
|---|---|---|---|
| Claude Code | `claude --continue` | `claude --resume [ID]` | yes |
| Codex | `codex resume --last` | `codex resume [ID]` | yes |
| OpenCode | `opencode --continue` | `opencode --session ID` (an ID is required: error `resume_needs_id`) | one database: `opencode session list` in a terminal |

- An id that is not a UUID (a Codex thread name): `--resume=NAME`. For an agent this `shark` does not know, the error
  is `resume_unsupported`; pass the agent's own flag after `--`.
- Both flags apply when the agent **starts**. If it is already running, `shark code` joins it as it is and says so;
  `shark stop myproj` first to start it on another conversation.
- **Where**: `<space>/.shark/agents/<agent>/` (Claude Code: `projects/` transcripts and the todo, task and plan files
  they refer to; Codex: `sessions/`; OpenCode: `opencode.db`), linked from the agent's config directory at every
  start, also for an agent you start by hand in a terminal. `.shark` is Shark's hidden directory of the Space (where
  its tool caches live): left out of the root's listings and of the files API, so `git add -A` does not pick it up.
  **A fork copies it**: the fork's agent can continue the same conversation.
- **Never credentials**: only these paths are kept in the Space. The agent's login (`.credentials.json`, Codex's and
  OpenCode's `auth.json`) and settings stay in the VM's memory and your encrypted store, as before. Checked with the
  real agents: the API key they ran with is in none of the kept files.
- **Size**: a long conversation is a few MB, and transcripts only grow. They count toward the Space's storage like any
  file; snapshots are deduplicated by content, so an append stores the new part, not a new copy. Claude Code deletes its
  own transcripts older than 30 days (its `cleanupPeriodDays` setting). Delete old ones from a terminal of the Space
  (`rm .shark/agents/claude/.claude/projects/*/ID.jsonl`); `--sessions` shows sizes.
- The listing reads the Space's last commit (taken every 30 s while a terminal is attached, and when it leaves), and
  never shows what a conversation says. API: `GET /v1/spaces/{id}/agent/sessions?agent=claude` (scope `files:read`).
- Conversations from before this version were in the VM's memory and are gone.

If a start ends with `[shark: the agent exited (code 125) …]`: 125 is Shark's own code, not the agent's (the session
VM, its terminal channel or the agent's start failed); the line says why. `shark logs myproj` shows what the agent
printed, and `shark code --continue myproj` picks the conversation up again.

## What the agent knows

The agent works as on your laptop, and it is told where it runs and what it can and cannot do, so it does not try
things that cannot work here and tells you the right thing. Validated locally only (no model was run:
`go test ./cmd/sharkfs -run TestAgentAware`, `./internal/codeagent`, `./cmd/shark -run TestCLIAgentContext`); that
each real agent reads these files is checked on metal next.

- **Instructions, written at every start** from the copy inside Shark (never a stale one): Claude Code gets the Shark
  skill in its skills folder and a short `CLAUDE.md` in its config folder; Codex its global `AGENTS.md`; OpenCode its
  global `AGENTS.md`. Nothing is written into your repository. They end with a block about this session: your Space's
  name, whether previews exist on this node, how you add a key or allow a host.
- **`$SHARK_CONTEXT`** in the agent and in every command it runs: a small JSON document (mode `devbox`, Space, what
  works: background processes yes as jobs `bg-N` in their own VM (`shark ps` / `kill` / `logs`), browser inside one
  command yes (`shark screenshot` where the image has one), a browser kept open no, exposed processes per node;
  the egress policy; how you add a secret; where usage is; `profile` (`devbox` or `sdk`); `github` (`connected`, or
  `why` and the `fix` the user runs, e.g. `shark github connect`); for a `can` or `cli` that is off, why and the
  command that turns it on, e.g. `can_why.background_after_command`). `shark context --json` prints it.
- **Your secrets and GitHub in the agent's commands.** Every command the agent runs gets the Space's secrets as its
  other commands do: placeholders `shk_ph_…` the egress swaps on the way to each secret's hosts, the Space's CA, and
  with GitHub connected (`shark github connect`, once per account) `GITHUB_TOKEN` and `GH_TOKEN` (one placeholder) and a
  git setup (credential helper for `https://github.com`, `git@github.com:` rewritten to https, the connection's noreply
  commit identity). A connection made while the agent runs reaches its commands within about 15 s; a secret set while
  it runs, at its next command. The agent's own process (not its commands) sees the secrets of its start.
- **A `shark` CLI limited to this Space** (on the hosted node; a self-hosted node sets `-agent-api-url`): the agent's
  commands get a credential minted by the node for this Space only, and their egress allows the node's API host. It
  can read this Space's usage, ops and logs, list, read and stop its background jobs (`shark ps`, `shark logs bg-N`,
  `shark kill bg-N`), list secret names, read the egress policy, start detached commands and expose ports where the
  node offers them. It cannot read
  your account balance, top up, manage keys, set secrets, change egress, touch other Spaces, delete or fork, or attach
  to the agent itself. It is valid 2 h, re-minted every hour while the agent runs, revoked when the agent stops, and
  never placed in the agent's own environment or instructions. A node restart invalidates it until the next start.
- **What it tells you.** "How much credit is left?": it reads this Space's usage and tells you to run `shark balance`.
  It needs an API key: it asks you to run `shark secrets set <space> NAME --preset …` (hidden prompt), never for the
  key in chat. A blocked host: it reads `shark egress denials <space>` and relays `shark egress use <space>
  +PROFILE` (the profile that has the host), or `shark egress add <space> HOST` when none has it. A server that should stay up: an exposed
  process where the node has them; otherwise it says so. Shark's own notices in a command's output start with
  `shark:` ([errors.md](errors.md#lines-shark-adds-to-a-commands-output)).

## Egress: nothing to configure

The agent runs in the Space's session VM under the Space's egress policy, and `shark code --agent X` turns on X's
egress profile by itself: `claude-code` (the Anthropic API, the Claude login and token refresh, the model catalog),
`codex` (the OpenAI API, the ChatGPT login and its backend) or `opencode` (its model catalog
`models.opencode.ai`, which OpenCode fetches at every start, and its own Zen / Go service). With an API key, the key's
provider profile goes on too (`anthropic-api`, `openai-api`), and `shark-docs` (these docs, `sharkmagic.si`) when the
Space's list lacks it. Devbox Spaces already have `essentials` (package registries, GitHub, GitLab, Ubuntu mirrors,
Docker Hub, Shark's docs). They show in `shark egress show <space>` as "added by shark
itself"; `shark egress use <space> -opencode` removes one, `PATCH …/egress {"auto_profiles": false}` stops it. Another
provider (OpenRouter, Gemini) is one more profile: `shark egress use <space> +openrouter`. The session VM takes a new
policy at its next start (an idle one is restarted when the agent starts). See what was refused with
`shark egress denials <space>`. Each profile's hosts, why each is there and how it was checked:
`shark egress profiles opencode`.

## Model credentials: two ways

### 1. API key (default, recommended)

Store your provider key as an **inject** secret of the Space. The value is read from a hidden prompt or stdin, never
from the command line:

```bash
shark secrets set myproj ANTHROPIC_API_KEY --preset anthropic --allow-egress
shark code myproj
```

(For Codex: `shark secrets set myproj OPENAI_API_KEY --preset openai --allow-egress`.) `--allow-egress` adds the
provider's egress profile (`anthropic-api`, `openai-api`) to the Space. Without the key, `shark code` stops before starting the agent and
prints this exact command (`missing_key`).

The agent sees a placeholder (`shk_ph_…`), never the key. Shark's egress proxy swaps the placeholder for the real key
only on requests to the provider's API host. **The key never enters the VM**: code the agent runs (a malicious
package, a prompt-injected command) cannot read it. A key stored as plain environment is removed from the agent's
environment on purpose.

### 2. Your own subscription

```bash
shark code --auth subscription myproj      # then log in inside the agent's UI, as on your laptop
```

- **Use of a subscription in a cloud machine is at your discretion and under your provider's terms.** Check them
  before you do it; Shark does not decide this for you.
- With a subscription, **the login token does live inside the VM while the agent runs** (unlike API-key mode), so
  code the agent runs could read it. Prefer API-key mode for untrusted repositories.
- Where it is kept: the agent's login files are stored **encrypted, per account**, on the node, outside your Space.
  They are written to a memory-only directory of the VM when the agent starts, saved back when they change, and erased
  when the agent stops. They are never part of the Space's files, its commits, its backups or its forks.
- While the VM is asleep, its memory image (including the token) is kept on the node's local disk; it is not
  uploaded.

## Billing

- While the agent runs, the session VM is billed as an **awake service** (at a fraction of the command rate; see
  pricing). Commands the agent runs are billed as usual, per command.
- After the agent has been **waiting for you** for 10 minutes (configurable), the session VM goes to sleep and costs
  nothing but storage. Your next key, or `shark attach`, wakes it.
- "The VM sleeps while the model thinks" is **not** claimed: it is an open measurement.

## Limits and what is not validated yet

- Real agents in real microVMs are validated on metal next. Locally, with the real agents: Claude Code's state hooks
  work, and all three send their shell commands through Shark's shim.
- Codex's own sandbox is off in the session VM (each command's microVM is the sandbox). Codex finds its shell by name
  rather than `$SHELL`, so Shark puts its shim first on Codex's `PATH`; this relies on the agent's user having no
  login shell of its own in the VM, which is how Shark runs it.
- The agent's state is detected from its screen (and from its hooks where it has them); a silent long-running tool
  call can read as `waiting-for-you`.