shelley

Getting started

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

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 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 and reach it from elsewhere through an SSH tunnel:

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