# Running it safely

> What Shelley protects (not much), what it doesn't, and how to run it without handing your machine to the network.

Shelley is a single-user agent with no authentication and no sandbox. That's
deliberate: it's meant to sit behind something that does those jobs. This page
is about what that means in practice, and what to put in front of it.

## What you're running

- **It runs as you.** The agent's commands and edits, and the terminal in the
  UI, run as the user who started Shelley, with that user's permissions and
  environment. If you have passwordless `sudo`, so does the agent.
- **No authentication.** Anyone who can load the UI can read every
  conversation, start new ones, and open a terminal. That last one doesn't even
  need a model.
- **It listens on every interface.** `shelley serve` listens on port 9000 on
  all addresses, IPv4 and IPv6. `--port` changes the port, not the address;
  to bind to one address, use socket activation (below).
- **No sandbox.** No container, no command allowlist, no approval prompts. A
  check in the shell tool refuses a few obvious mistakes, like `rm -rf ~` or
  `git add -A`; its [source](https://github.com/boldsoftware/shelley/blob/main/claudetool/bashkit/bashkit.go)
  is explicit that it is not security. The browser tool runs headless Chrome
  with Chrome's own sandbox turned off.
- **It installs missing tools.** If the agent runs a command that isn't
  installed, Shelley asks a model whether it's a legitimate tool and, if so,
  installs it with the package manager it finds (`sudo apt install -y`,
  `brew install`, and so on).
- **Your keys are within reach.** The `*_API_KEY` variables are inherited by
  every command the agent runs. Custom-model API keys are stored in the
  database in plain text, and the UI's API returns them.
- **One user.** Everyone who uses an instance shares one OS account, one
  database, and one set of keys.

The agent also acts on what it reads: web pages, files, command output, issue
text. Instructions planted in any of those can steer it. Assume anything the
agent can reach, someone who controls its inputs can reach too.

## What it does protect against

- **Cross-site requests.** On the TCP port, Shelley rejects cross-origin
  state-changing requests from browsers (Go's `http.CrossOriginProtection`),
  and the terminal's WebSocket refuses foreign origins. A web page you visit
  can't drive your Shelley from its own origin.
- **The Unix socket, from other accounts.** It's created mode `0600`, so other
  users on the machine can't connect to it.

That's the list. Listening only on loopback keeps the network out, but it
doesn't make the port private: other software on the machine, and in some cases
a web page in your browser (DNS rebinding is the usual trick), can still reach
it. Treat the port as unauthenticated wherever it listens.

## `-require-header`

```sh
shelley --db ~/.shelley/shelley.db serve -require-header X-Forwarded-User
```

With this, requests to `/api/` and the MCP routes get a `403` unless they carry
the named header. It's for running behind an authenticating proxy that sets
the header: it stops requests that bypassed the proxy from using the API.

It is not authentication on its own:

- Only presence is checked, not the value. Anyone who can reach the port can
  send the header.
- It doesn't cover the UI's static files or the handful of endpoints outside
  `/api/` (version check, settings, upgrade, exit, feature flags, most debug
  pages).
- Requests on the Unix socket skip it.

So use it only when the proxy is the one thing that can reach Shelley's port,
and make sure the proxy sets the header itself rather than passing along one
from the client. This is how exe.dev runs Shelley: socket-activated on
`127.0.0.1:9999`, with `-require-header X-Exedev-Userid` and exe.dev's
authenticating proxy in front.

## The Unix socket

By default, `shelley serve` also listens on a Unix socket at
`~/.config/shelley/shelley.sock` (under `$XDG_CONFIG_HOME` if that's set).
`shelley client` and `shelley mcp` talk to the server through it, including when
the agent runs them.

- The socket is mode `0600`, so only your user can connect. Anything running
  as your user can, and it gets the full API: requests on the socket skip the
  cross-origin check and `-require-header`.
- If another Shelley is already using the path, the next one takes
  `shelley-2.sock`, then `shelley-3.sock`, and so on.
- `-socket none` turns it off; `shelley client` and `shelley mcp` then need an
  explicit `-url`. `-socket <path>` moves it.

## Files on disk

- The database holds every conversation, plus custom-model API keys, MCP
  server headers, and MCP OAuth tokens. It's created with your umask, which
  usually means anyone on the machine can read it. Keep it in a private
  directory: `chmod 700 ~/.shelley`.
- Uploads, screenshots, recordings, and browser downloads go in `/tmp/shelley-*`
  directories, readable by other users on a shared machine.

See [where files live](/docs/install#where-files-live) for the full list.

## Recommendations

Roughly from most to least isolated. They combine.

1. **Give it a machine of its own.** A VM or spare box with nothing on it
   you'd mind losing, and only the credentials it needs. Then everything above
   matters less.
2. **Keep the port off the network.** On a laptop, at minimum, firewall inbound
   connections to Shelley's port. Better, have it listen only on loopback with
   [socket activation](/docs/install#running-it-as-a-service) and reach it from
   elsewhere through an SSH tunnel:

   ```sh
   ssh -L 9000:localhost:9000 your-server
   ```

   Then open `http://localhost:9000` on your own machine.
3. **Use a VPN.** Tailscale, WireGuard, or similar limits who can reach the
   machine at all. Since Shelley listens on every interface, also firewall the
   other interfaces, or bind it to loopback and forward from the VPN side
   (`tailscale serve` can do that).
4. **Put an authenticating proxy in front.** Caddy, nginx with oauth2-proxy,
   Cloudflare Access, whatever you already trust, in front of a loopback-only
   Shelley, with `-require-header` set to a header the proxy adds. The proxy
   has to pass WebSockets (the terminal) and unbuffered streaming responses
   (conversations update over Server-Sent Events).
5. **Or use exe.dev.** Shelley comes running on every
   [exe.dev](/docs/exe-dev) VM: a machine of its own,
   already behind auth, with models included.

If you'd rather Shelley not contact GitHub for updates, set
`SHELLEY_SKIP_VERSION_CHECK=true`; see [Upgrading](/docs/install#upgrading).
