Skip to content

Server

gridshell-server is the PTY + Sheets bridge + MCP relay a self-hosted GridShell backend runs. It exposes three plain WebSocket endpoints:

Endpoint Purpose
/terminal xterm.js ↔ PTY - session reattach, idle-kill, output buffering.
/host The Sheets sidebar/dialog connects here to open/close/kill shells.
/mcp gridshell-mcp connects here to relay tool calls.

terminal-server is shipped open source, as a reference implementation - it's not a maintained SaaS backend. If your organization needs to run it inside its own trusted runtime with custom controls, you're expected to adapt it rather than depend on it unmodified; see "Writing your own server" below.

Running it

gridshell-server [--port N] [--host HOST] [--idle-hours H] [--buffer-mb M]
                  [--wss --cert-path FILE --key-path FILE] [--auth-token TOKEN]
                  [--no-auth-token] [--regenerate-token] [--copy-token] [--allow-root]
                  [--allow-origin HOST_SUFFIX[,HOST_SUFFIX...]] [--max-message-mb M]
Flag Env var Default Meaning
--port PORT 3000 Port to listen on.
--host HOST localhost Bind address. Not auto-detected from --wss or anything else, without this, the server can never be reached from outside the machine, no matter what else is configured. Use 0.0.0.0 (or a specific interface) for standalone direct exposure with no reverse proxy in front; a reverse-proxy deployment doesn't need this at all, since the proxy is what's actually reachable. A non-loopback bind refuses to start only if --no-auth-token is also passed, otherwise the auto-generated (or explicit) token below still applies. "Loopback" is an exact match against localhost/127.0.0.1/::1/[::1] - a value like 127.0.0.2 is treated as non-loopback and will demand a token too.
--idle-hours IDLE_HOURS 12 Idle time before a disconnected shell session is killed. A client's own ?idleHours= query param can only lower this, never raise it - it's a ceiling, not just a default.
--buffer-mb BUFFER_MB 8 Max size of the per-session output replay buffer (used for reattach). A client's own ?bufferMb= query param can only lower this, never raise it, same as ?idleHours= above, except at exactly 0: unlike ?idleHours=0 (an explicit zero grace period, honored as given), ?bufferMb=0 is treated as "no override" and falls back to the server default, since a zero-size buffer isn't a meaningful value.
--wss - off Serve wss:// (TLS) directly, using --cert-path/--key-path. Requires a token (auto-generated by default) and a real --cert-path/--key-path pair - refuses to start otherwise.
--cert-path / --key-path CERT_PATH / KEY_PATH - TLS certificate/key files, required with --wss.
--auth-token AUTH_TOKEN auto-generated Shared secret required on every /terminal, /host, /mcp connection. If not set explicitly, one is generated on first run and persisted to ~/.gridshell/token (or $GRIDSHELL_TOKEN_PATH), reused on every future launch. Never printed to the console - see "Security & deployment" below for how it's actually delivered.
--no-auth-token - off Disable the token entirely. Not recommended unless access is restricted at a different layer (see "Security & deployment" below) - refused together with --wss or a non-loopback --host.
--regenerate-token - off Force a fresh auto-generated token this launch, overwriting the persisted one. Not valid together with --auth-token or --no-auth-token - there's nothing to regenerate in either case.
--copy-token - - Resolve the current token (persisted/auto-generated, or an explicit --auth-token/AUTH_TOKEN) and copy it to the clipboard, then exit without starting the server. This way it can be retrieved again later.
--allow-root - off Allow starting as root/Administrator, refused by default, see "Shell privileges" below.
--allow-origin ALLOW_ORIGIN *.googleusercontent.com Comma-separated list of extra allowed Origin header suffixes (see "Security & deployment" below). Google's own Apps Script origin is always allowed; add your own only if you're fronting the server with something other than the stock Sheets sidebar.
--max-message-mb MAX_MESSAGE_MB 64 Max size of a single WebSocket message on any endpoint. Raise it if you work with very large ranges; the underlying library's own default (1 MiB) is well inside a normal Sheets range. gridshell-mcp/SheetsClient read the same $MAX_MESSAGE_MB - set it wherever they run too, not just on the server, or they'll reject an oversized response the server was happy to send.

An unrecognized flag, a value-taking flag with no value, or a non-numeric value for a numeric flag, exits immediately with a clear error rather than being silently ignored or surfacing as a raw traceback. Run with --help for the full flag list.

Security & deployment

Access to gridshell-server is access to a real, interactive shell, so a token guards every connection by default - including a purely local one. It's required on /terminal, /host, and /mcp alike, checked as the first message on the connection rather than in the URL (so it never ends up in a reverse proxy's plaintext access log), and compared byte-for-byte, so non-ASCII values work fine. An Origin-header check adds a second, smaller layer on top - it catches a browser tab on the same machine trying to connect directly, but not a plain script or process, which sends no Origin at all; the token is what's actually doing the work.

Getting the token. Left unset, gridshell-server generates one on first run, persists it to ~/.gridshell/token (or $GRIDSHELL_TOKEN_PATH) so it survives restarts, and copies it to your clipboard rather than printing it - paste it once into the Sidebar's Auth token field, it's saved per Google account and carries over to every other spreadsheet. Run --copy-token any time to copy the current value again, or --regenerate-token to rotate it. No clipboard available (a headless box, an SSH session)? The server starts normally regardless and prints the file path instead - read it from there. Only an actual failure to persist the token refuses to start. Prefer to manage it yourself instead? Pass --auth-token/$AUTH_TOKEN - the natural choice for a deployment where the token already lives in your own secrets management.

Three ways to run it: - Local, trusted use (the default) - nothing further to configure. - Behind a reverse proxy that terminates TLS (nginx, Caddy, Cloudflare, ...) - pass --auth-token or keep the default; gridshell-server itself doesn't need a certificate. - Standalone, directly exposed - add --wss --cert-path FILE --key-path FILE. Works with a loopback bind too, if you want to encrypt local traffic as well; see below.

--wss and a non-loopback --host both refuse to start without a token configured, so there's no way to end up exposed with nothing guarding the connection. The token check itself doesn't depend on --wss. TLS termination and authentication are separate questions, and gating one on the other would leave the reverse-proxy shape (the common production case) silently unchecked.

Each open document's shell also carries its own per-spawn secrets. Naming a session over /mcp, /host, or /terminal isn't enough on its own, the matching key has to come with it, so knowing or guessing another document's session id doesn't reach it. This only applies to attaching to an already-running shell (evicting its live dialog and replaying its scrollback); spawning a brand-new one has no prior key to check against yet. gridshell-mcp/SheetsClient handle this automatically from inside the shell they're targeting; see MCP and Python Client for the explicit-session case. The Sheets add-on's own sidebar handles its /host and /terminal keys the same way, transparently.

gridshell-server is a single-operator tool - one person running their own server for their own use, not a shared or multi-tenant backend.

Encrypting a local server too

--wss isn't only for remote deployments, it works with a loopback bind as well, encrypting the connection itself against anything else on the same machine that might be watching local traffic. This is offered as an available option, not the default, since it trades a bit of setup friction for it: a self-signed certificate works, but the browser will show a certificate warning the first time it connects, which you'd need to accept.

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout key.pem -out cert.pem -subj "/CN=localhost"
gridshell-server --wss --host 127.0.0.1 --cert-path cert.pem --key-path key.pem

For a setup with no browser warning at all, use a tool like mkcert to install a locally-trusted certificate authority instead of a plain self-signed one.

Hardening a remote deployment

The above covers what gridshell-server itself does; a remote (shape 2/3) deployment benefits from standard operator-side hardening too, on top of it:

  • A VPN or mesh network (Tailscale, WireGuard) is the best answer for most people who want remote access without becoming a hardened public-service operator - it keeps the server off the raw public internet entirely, reachable only from your own enrolled devices, while still letting you connect from anywhere.
  • OS or cloud firewall IP allow-listing, if your client IPs are stable restrict inbound connections to just those.
  • fail2ban (or similar) against the server's own log line on a rejected token ("[auth] rejected connection from <ip> ...") - works today provided you run the server with its output going to a real log file, not just a terminal that vanishes on restart.
  • A lightweight reverse proxy purely for rate-limiting, even if you don't need one for TLS - gridshell-server has no rate-limiting of its own.

None of this is shipped code - it's the standard playbook for exposing any self-hosted service, applied here.

Shell privileges

The spawned shell inherits the full environment of the gridshell-server process, including any secrets you have set as environment variables (cloud credentials, API keys), worth being mindful of before starting it, especially if a less-trusted agent will run inside the resulting shell. If you start gridshell-server from inside an already-running AI CLI session, a shell it spawns inherits that session's own environment markers too. Launching the same CLI tool inside one of those shells may then behave as a nested/child session (ordinary process-environment inheritance, not a GridShell bug). Start the server from a plain terminal instead if you want a clean environment for what it spawns.

gridshell-server refuses to start as root/Administrator by default (--allow-root overrides) - a child process can never exceed its parent's privilege level, so this bounds what a spawned shell can ever do "for free." For real isolation beyond that (a less-trusted agent, or a deployment where you don't fully trust what's connecting), run gridshell-server itself inside a container/VM or under a dedicated, restricted OS account - the same pattern already common for agentic coding tools generally. GridShell doesn't build this containment itself: the tool's core value is a shell that acts as you, with your normal toolchain and credentials, which is in real tension with running it as a stripped-down service account by default. Reusing mature container/VM isolation is lower-risk than us reimplementing a weaker version of it.

Stopping the server

Ctrl+C (or a plain kill on Linux/Mac) cleanly terminates every shell currently running before the process exits. A hard crash or a forceful kill (Task Manager "End Task", taskkill /F, kill -9) does not, as no code runs in that case, so a shell that was active at that moment can keep running as an orphaned process, invisible to a freshly started server (a reattach for its session id just spawns a new shell instead of finding it). If this happens, find and end the orphaned process manually (Task Manager / ps + kill) - there's no in-app way to recover it.

Platform support

Testing coverage is real but uneven across platforms and deployment shapes - not "all three, fully," so here's the honest breakdown. The automated test suite is designed to run against a fake PTY process on any platform (FakePtyProcess in tests/test_conformance.py). There's no CI configured in this repo yet, so that's currently exercised manually rather than continuously, on whichever platform is running it. It verifies session bookkeeping - attach/reattach/idle-kill/buffer-replay, and the auth-frame/token handling - independent of the real shell backend; it does not exercise pywinpty, ptyprocess, or --wss/TLS on any platform. The platform and deployment coverage below comes from manual testing, not CI.

  • Windows: the primary development platform, tested extensively at the PTY level and through the local/localhost shape; not yet exercised in a real remote/wss deployment. The shell itself is hardcoded to powershell.exe here with no $COMSPEC/pwsh lookup or override, unlike the POSIX side's $SHELL handling below.
  • Linux: the platform tested through a real remote/wss deployment: a live VPS, TLS behind a reverse proxy with a real certificate, correct rejection of a wrong or non-ASCII token, idle-kill, and the isolation guarantee described in Quick Start - no way for the agent to reach a different open spreadsheet from within one shell - confirmed live against two spreadsheets connected at once.
  • macOS: tested at the PTY level (spawn/resize/kill) on real hardware, not yet through the full remote/wss deployment flow. One platform-specific thing to know: the server falls back to /bin/bash when $SHELL is unset, which is common for a service/launchd-started process - macOS's own default shell is zsh, and its bundled /bin/bash is the old 3.2 release, so a server started as a service can land you in an unfamiliar shell rather than your normal one. Set $SHELL explicitly if you hit this.

If you hit a platform-specific issue, especially in a combination not covered above, please report it.

Writing your own server

server.py is meant to be read, not just run. The wire protocol it speaks (/terminal, /host, /mcp) is plain JSON over WebSocket, and there's no reason a Sheets-integrated backend has to be this implementation specifically. If your environment has requirements this reference implementation doesn't meet (a different PTY sandboxing model, integration with existing infrastructure, additional access controls), fork it rather than working around it.