> ## Documentation Index
> Fetch the complete documentation index at: https://arkor-92aeef0e-eng-635.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# arkor dev

> Launch Arkor Studio locally.

# `arkor dev`

<CodeGroup>
  ```bash pnpm theme={null}
  pnpm dev
  ```

  ```bash npm theme={null}
  npm run dev
  ```

  ```bash yarn theme={null}
  yarn dev
  ```

  ```bash bun theme={null}
  bun dev
  ```
</CodeGroup>

Boots [Studio](/concepts/studio), the local web UI, on `http://localhost:4000`. Studio is where you click **Run training** to spawn `arkor start` against your `src/arkor/index.ts`, watch the run stream in, and chat with the resulting adapter in the Playground.

`arkor dev` itself does **not** start a training run; it only serves the UI plus a small loopback API the SPA talks to.

## Synopsis

```
arkor dev [options]
```

## Options

| Flag                | Default | Description                                                                                                                                                                                                                             |   |                                                                                                                                                                                                                                                                                               |
| ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-p, --port <port>` | `4000`  | Port to bind. The displayed URL uses `localhost`, but the listener binds `127.0.0.1` directly so it cannot end up IPv6-only on hosts where `/etc/hosts` lists `::1` before `127.0.0.1`. The CLI parses the value as \`Number(opts.port) |   | 4000`, so falsy results (`0`, non-numeric) normalize to `4000`. **Truthy invalid values pass through unsanitized**: a negative port or a port above `65535`reaches the listener as-is and surfaces as a`serve()`failure (e.g.`RangeError\`). Stick to the standard 1 to 65535 range yourself. |
| `--open`            | off     | Open the Studio URL in a browser after the server is up.                                                                                                                                                                                |   |                                                                                                                                                                                                                                                                                               |

## What happens at launch

1. **Credential bootstrap.** If `~/.arkor/credentials.json` does not exist, the CLI always tries to bootstrap an anonymous session: it calls `/v1/auth/cli/config`, then requests an anonymous token from `/v1/auth/anonymous`. The pre-bootstrap line depends on whether the deployment advertises OAuth: when OAuth is configured the CLI prints ``No credentials on file — bootstrapping an anonymous session. Run `arkor login --oauth` to sign in to your account instead.`` so you can upgrade to a real account whenever you want; on anon-only deployments it prints `No credentials on file — requesting an anonymous token.` instead, omitting the OAuth hint because `arkor login --oauth` would fail there. Either way, it never auto-launches the OAuth flow. Once the token lands, `arkor dev` prints ``Anonymous id: <id> — Arkor Cloud uses this id to recognise this client across sessions. Keep `<home>/.arkor/credentials.json` to stay signed in as the same anonymous identity.`` (the path is the resolved `credentialsPath()`, typically `~/.arkor/credentials.json` on Linux and macOS). Only when the deployment advertises OAuth, a follow-up warn (``Anonymous sessions aren't guaranteed to persist — sign in with `arkor login --oauth` to tie future work to your Arkor Cloud account.``) fires alongside the success line so the upgrade hint is visible at issuance time. On anon-only deployments the warn is suppressed because pointing at `arkor login --oauth` would surface a command that fails. Transport failures (`fetch failed`) are handled differently depending on when they hit. If `/v1/auth/cli/config` already succeeded and `/v1/auth/anonymous` then fails the same way, the CLI warns and continues; the Studio server retries on the first `/api/credentials` hit. If `/v1/auth/cli/config` itself is unreachable, the same transport error is rethrown and `arkor dev` exits fast (restore connectivity and re-run). If `/v1/auth/anonymous` is rejected with a 4xx (for example because anonymous sign-in is disabled on this deployment), it surfaces an error wrapping the HTTP status and pointing at `arkor login --oauth` (full message: ``Failed to bootstrap an anonymous session (HTTP <status>). This deployment may require sign-in — run `arkor login --oauth` and try again.``).
2. **CSRF token.** A 32-byte token (base64url, \~43 chars) is generated for this launch. It is injected into `index.html` as `<meta name="arkor-studio-token">` so the same-origin SPA can read it. Cross-origin tabs cannot read the meta and are rejected by the `/api/*` middleware.
3. **Token persistence (best-effort).** The same token is written to `~/.arkor/studio-token` (mode `0600`) so the studio-app Vite dev server (`pnpm --filter @arkor/studio-app dev`) can pick it up. If writing fails (read-only `$HOME`, locked-down umask), `arkor dev` continues; only the standalone Vite dev workflow is affected.
4. **Listener.** Hono on `127.0.0.1:<port>`. The `Host` header guard accepts both `127.0.0.1` and `localhost`, so the URL the CLI prints (`http://localhost:<port>`) works without surprising DNS-rebinding fallout.

When the process exits (normal exit, `SIGINT`, `SIGTERM`, or `SIGHUP`) the studio-token file is removed on a best-effort basis. A crash can leave the file on disk; the next `arkor dev` rotates it.

## Loopback + CSRF: the security model in one paragraph

The Studio server enforces three checks on every `/api/*` request:

1. The `Host` header must be `127.0.0.1` or `localhost` (defense against DNS rebinding).
2. The CSRF token must be present as the `X-Arkor-Studio-Token` header. The job-event stream also accepts `?studioToken=...` because `EventSource` cannot send custom headers; mutation routes do not accept query-string tokens. Token comparison is `timingSafeEqual`.
3. CORS is intentionally not configured: the SPA is same-origin so CORS adds no value, and reflecting `*` would let "simple" cross-origin POSTs (`text/plain`, `urlencoded`) skip preflight. Without a token, the middleware rejects them.

This means `arkor dev` is safe on a shared dev machine: another tab cannot read the meta, a stale tab from a previous launch holds an old token that no longer matches, and an attacker page in a different origin cannot forge requests.

## When the port is in use

`arkor dev` does not auto-pick a free port. If the chosen port is already taken (another `arkor dev` left running, an unrelated dev server, etc.), `serve()` surfaces the underlying `EADDRINUSE` from Node's `net.Server` and the process exits non-zero. Pick a different port with `-p <port>`, or stop whatever else is bound to it.

## Common errors

| Symptom                                                                                                                                                                             | What it means                                                                                                                                                                                                                                                | Fix                                                                                                                                                           |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Error: listen EADDRINUSE: address already in use 127.0.0.1:4000`                                                                                                                   | Another process holds the port.                                                                                                                                                                                                                              | Stop it, or use `--port <other>`.                                                                                                                             |
| `Could not reach <baseUrl> (fetch failed). Studio will keep running and retry on first /api/credentials hit.`                                                                       | `/v1/auth/cli/config` already succeeded, but the follow-up `/v1/auth/anonymous` hit a transport error. The Studio server starts and will retry.                                                                                                              | Bring connectivity back; the SPA recovers on its next `/api/credentials` poll without restarting `arkor dev`.                                                 |
| `TypeError: fetch failed` (or an equivalent transport error that exits `arkor dev` immediately)                                                                                     | `/v1/auth/cli/config` itself was unreachable, so the deployment mode could not be determined and the CLI fails fast.                                                                                                                                         | Restore connectivity and re-run `arkor dev`.                                                                                                                  |
| ``No credentials on file — bootstrapping an anonymous session. Run `arkor login --oauth` to sign in to your account instead.``                                                      | First `arkor dev` on this machine when the deployment advertises OAuth. The CLI bootstraps anonymous so Studio can start immediately; the message is informational, not an error.                                                                            | Nothing required. To upgrade to a real account, run `arkor login --oauth` separately (it overwrites `~/.arkor/credentials.json`) and refresh Studio.          |
| `No credentials on file — requesting an anonymous token.`                                                                                                                           | Same as above on anon-only deployments (no Auth0 advertised in `/v1/auth/cli/config`). The CLI omits the `arkor login --oauth` hint because that command would fail there.                                                                                   | Nothing required.                                                                                                                                             |
| ``Anonymous id: <id> — Arkor Cloud uses this id to recognise this client across sessions. Keep `<home>/.arkor/credentials.json` to stay signed in as the same anonymous identity.`` | Informational follow-up after the anonymous bootstrap completes: surfaces the cloud-side identifier and where it lives (the path is the resolved `credentialsPath()`, typically `~/.arkor/credentials.json` on Linux and macOS).                             | Nothing required. Back up the credentials file if you want to keep using the same anonymous identity from another machine.                                    |
| ``Anonymous sessions aren't guaranteed to persist — sign in with `arkor login --oauth` to tie future work to your Arkor Cloud account.``                                            | Persistence nudge fired alongside the success message when the deployment is known to support OAuth. Anonymous work has no SLA on the cloud-api side, so the CLI surfaces the upgrade path before you invest real work. Suppressed on anon-only deployments. | Optional: run `arkor login --oauth` to tie future work to your account. Existing anonymous work stays under its current id; there is no migration path today. |
| ``Failed to bootstrap an anonymous session (HTTP <status>). This deployment may require sign-in — run `arkor login --oauth` and try again.``                                        | `/v1/auth/anonymous` rejected the request with a 4xx, so anonymous bootstrap cannot proceed.                                                                                                                                                                 | Run `arkor login --oauth` to complete the browser flow, then re-run `arkor dev`.                                                                              |
| `Could not write ~/.arkor/studio-token (...). The Studio at http://localhost:<port> is unaffected, but the Vite SPA dev workflow will see 403s on /api/*.`                          | `$HOME` is read-only or umask blocks `0600`. The bundled Studio still works; only the standalone Vite dev workflow is affected.                                                                                                                              | Run from a writable home, or only use the bundled Studio served by `arkor dev`.                                                                               |
| HTTP 403 with `{ "error": "Studio API is loopback-only" }` (in browser devtools)                                                                                                    | The `Host` header is something other than `127.0.0.1` / `localhost`.                                                                                                                                                                                         | Reach Studio via `http://localhost:<port>` or `http://127.0.0.1:<port>`. Reverse proxies or `0.0.0.0`-bound shells will be rejected by design.                |
| HTTP 403 with `{ "error": "Missing or invalid studio token" }` (in browser devtools)                                                                                                | The CSRF token in the page does not match the current launch. Usually a stale tab from a previous `arkor dev`.                                                                                                                                               | Reload the tab. Token rotates on every launch.                                                                                                                |

## Examples

Default port:

<CodeGroup>
  ```bash pnpm theme={null}
  pnpm dev
  ```

  ```bash npm theme={null}
  npm run dev
  ```

  ```bash yarn theme={null}
  yarn dev
  ```

  ```bash bun theme={null}
  bun dev
  ```
</CodeGroup>

Custom port plus auto-open:

<CodeGroup>
  ```bash pnpm theme={null}
  pnpm dev --port 5000 --open
  ```

  ```bash npm theme={null}
  npm run dev -- --port 5000 --open
  ```

  ```bash yarn theme={null}
  yarn dev --port 5000 --open
  ```

  ```bash bun theme={null}
  bun dev --port 5000 --open
  ```
</CodeGroup>
