cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / docs / terminal / ssh.md
ssh.md 15.4K rendered
1---2title: "Serving over SSH"3sidebar_position: 34---56# Serving over SSH78`012 serve` runs an SSH server that gives every session its own 012, in9the client's own terminal, on the files of one directory. It's for10reaching your sheets from another machine or a tablet with an SSH app,11without installing anything there, and for working on one workbook12together: sessions that open the same file share it, each with its own13cursor, and every change reaches the others as it's made14([Sharing a workbook](#sharing-a-workbook)).1516```sh17012 serve ~/sheets # serve ~/sheets on 127.0.0.1:231218ssh -p 2312 localhost # from this machine, on a new sheet19ssh -t -p 2312 localhost budget.012 # opening budget.012 from ~/sheets20ssh -t -p 2312 localhost @standup # joining the named room standup21```2223A file name after the host opens that file, as `012 budget.012` does24locally: a `.012` sheet, a new sheet to be saved under that name when25there's no such file, or a file to import (`data.csv`). A name starting26with `@` joins a named room instead. It must be one word (quote a name27with spaces twice: `ssh -t host "'my book.012'"`) and needs `-t`, since28ssh only asks for a terminal on its own when there's no command.2930```31012 serve [--listen addr] [--authorized-keys file] [--host-key file]32 [--idle-timeout 30m] [--max-sessions 8] [--share edit|view|off]33 [--log file] [--otlp url] [dir]34```3536| Flag | Default | What |37|---|---|---|38| `dir` | `.` | The served directory. Sessions open, save, import and download only inside it |39| `--listen` | `127.0.0.1:2312` | Address to listen on. Loopback only unless you change it |40| `--authorized-keys` | `~/.ssh/authorized_keys` | Public keys allowed in, in OpenSSH's format |41| `--host-key` | `<config dir>/012/ssh_host_ed25519_key` | The server's key, generated on first run |42| `--idle-timeout` | `30m` | End a session after this long without input, keeping its unsaved changes; `0` never does |43| `--max-sessions` | `8` | Sessions at once; more are turned away with a message |44| `--share` | `edit` | Sessions opening the same file share it: `edit`, everyone edits; `view`, one writes and the others follow; `off`, each session gets a copy of its own |45| `--log`, `--otlp` | off | Telemetry, as for the app ([Observability](../contributing/observability.md)) |4647The config directory is `os.UserConfigDir()`: `~/Library/Application48Support` on macOS, `~/.config` on Linux. On start, 012 prints the served49directory, the address, the host key's fingerprint and the command to50connect.5152## Setup53541. Put the public keys that may connect in an authorized_keys file. Your55 own `~/.ssh/authorized_keys` works, or keep a separate one for 012:5657 ```sh58 cat ~/.ssh/id_ed25519.pub > ~/.config/012/authorized_keys59 chmod 600 ~/.config/012/authorized_keys60 012 serve --authorized-keys ~/.config/012/authorized_keys ~/sheets61 ```62632. Connect with `ssh -p 2312 host`. The first time, check the host key64 fingerprint ssh shows against the one 012 printed, or add the key to65 `known_hosts` yourself from `ssh_host_ed25519_key.pub` next to it.66673. To reach it from another machine, prefer keeping 012 on loopback and68 tunnelling through the machine's own sshd69 (`ssh -L 2312:127.0.0.1:2312 host`, then `ssh -p 2312 localhost`), or70 a private network such as Tailscale with `--listen 100.x.y.z:2312`.71 Listening beyond loopback prints a warning.7273Stop the server with Ctrl+C. Running sessions end with it, keeping74their unsaved changes (see [Unsaved work](#unsaved-work)).7576## Security model7778The server exposes the files of one directory to whoever holds one of79the authorized keys. Everything else is closed.8081- **Authentication.** Public keys only, checked against the82 authorized_keys file, which is read again on every login so edits83 apply at once. No passwords, no keyboard-interactive, no certificates.84 The file must not be writable by group or others, as with sshd's85 `StrictModes`. Keys carrying options 012 can't honor (`command=`,86 `from=`, `no-pty`, `restrict` without `pty`, `expiry-time=`,87 `cert-authority` and so on) are skipped, so a key restricted to, say,88 a backup command never gets a spreadsheet. The user name is logged but89 not checked.90- **Host key.** An ed25519 key generated on first run, written `0600`91 in a `0700` directory. 012 refuses to start if the key is readable by92 others.93- **What a session can do.** Run 012, nothing else; no command is94 executed, unless the server's config turns on notebooks' nushell95 cells (see [Notebooks](#notebooks)). A shell request starts 012 on a new sheet. An exec request96 is accepted only with a terminal (`ssh -t`) and only when it is one97 word, which is taken as a file name and never run: it resolves inside98 the served directory like a name typed in File > Open, and anything99 that doesn't (see Files below), or holds control characters, ends the100 session with the reason before 012 starts. Other exec requests101 (`ssh host cat /etc/passwd`, `ssh -t host sh -c id`, any exec without102 a terminal) are refused, as are subsystems (`sftp`, `scp`), local and103 remote port forwarding and X11; agent forwarding requests are104 accepted by the SSH library but never used. A shell without a105 terminal is told to use `ssh -t` and closed.106- **Files.** Names typed in File > Open, Save as, Import and Download,107 and a file name given on the ssh command line, resolve inside the108 served directory. Refused, with "outside the served directory":109 absolute paths, `..` that climbs out, hidden files and directories110 (`.env`, `.git`, `.ssh`, `.012-recovery`), and symbolic links that111 lead outside or nowhere. Links that stay inside work. Error messages112 name files relative to the served directory, not where it is on the113 server. Files are written with the server's user and permissions;114 recovery files are the one thing 012 writes that no one named, always115 in `.012-recovery/` (see [Unsaved work](#unsaved-work)).116- **JEV.** On only when the server process's own environment has117 `TYPESAFE_API_KEY`; sessions never read `.env` files. Every session118 has its own answer cache, and all of them spend the server's key.119- **Logs.** Logins, rejected keys, failed handshakes, sessions turned120 away and session ends go to stderr and to telemetry as `ssh.*` events121 with the user name, the key's SHA256 fingerprint and the remote122 address, never file contents or cells.123124| Event | When | Attributes |125|---|---|---|126| `ssh.session` | a session starts | `user`, `key`, `remote`, `term`, `width`, `height`, `file` (a file was named on the command line; the name isn't logged) |127| `ssh.session_end` | it ends | `user`, `key`, `remote`, `duration`, `idle` (closed for idling), `stopped` (the server stopped), `recovered` (unsaved work was kept) |128| `ssh.rejected` | a key not in authorized_keys | `user`, `key`, `remote` |129| `ssh.full` | a session turned away at `--max-sessions` | `user`, `key`, `remote` |130| `ssh.failed` | a handshake or login fails | `remote`, `error` |131| `ssh.auth_error` | authorized_keys can't be read or parsed | `error` |132133The `frames` summaries and the `ssh_sessions` gauge cover every session134together: telemetry is per process.135136What it doesn't protect against: someone who can already write to the137served directory on the server (a symbolic link swapped in between the138check and the open), and anything a holder of an authorized key does139with the files they're given.140141## Sharing a workbook142143Sessions that open the same file (by any name that leads to it) are in144one room, and so are those joining the same named room (`@standup`,145which starts as a new sheet until someone saves it under a name). A146room is one workbook the server holds: every session sees every change147as it's made, whoever made it, while keeping its own cursor, scroll,148selection, sheet shown, theme and modes. A session on a new sheet or an149imported file has a workbook of its own until it's saved and opened150again.151152![Ann's screen with bob and cy in the budget: bob's pointer on D4 in his color, cy's on A8 in hers, their initials on those rows' headers, the cells they just changed marked, and their names on the status line](../media/share-presence-dark.png#gh-dark-mode-only)153![Ann's screen with bob and cy in the budget: bob's pointer on D4 in his color, cy's on A8 in hers, their initials on those rows' headers, the cells they just changed marked, and their names on the status line](../media/share-presence-light.png#gh-light-mode-only)154155**Who is where.** Each other person in the room has a color: their156pointer's cell is drawn in it, double-underlined, their initial is on157that row's header, and their name is on the status line. A cell someone158else changed in the last 30 seconds has a `▘` in its corner, and the159context line on it says who changed it and how; on someone's pointer it160says who is there, and whether they're typing. **File > Who's here**161lists everyone with the cell they're on, and picking one goes there.162163**Editing together.** Everyone edits (with `--share edit`). The server164takes each session's keys and clicks in turn, so every change is made165whole, in one order, through the one path every change takes in 012:166two entries in one cell leave the later one, and the one who typed167over the other's change is told, as is the one typing while someone168else changed the cell.169170![Ann above and bob below, in tmux panes, in one budget: each change arrives in the other's pane, ann lists who's here, and bob's undo takes back only his own change](../media/share.gif)171172**Undo is yours.** Ctrl+Z takes back your own latest change, even when173others changed other cells since: theirs stay. When someone changed the174same cells after you (the same cells, widths, formats, names or rules,175or rows and columns inserted or deleted on that sheet, or any change to176the list of sheets), undo refuses and says who: take back yours by177hand, or ask them. Redo is yours on the same terms. Formulas that read178a cell you undid recalculate as usual.179180**One writer.** With `--share view`, the first in a room writes and181the others follow: they move, scroll, select, copy and look around as182they like, but can't change the workbook, and the status line says who183writes. The writer hands writing over with **File > Hand over writing**;184when the writer leaves, whoever has been there longest writes.185186**Saving is the room's.** Any session saves the one workbook to its187file, and the others' `modified` clears with it. Quitting while others188are in the room asks nothing: they still have the workbook. The last189one to leave is asked about unsaved changes as in a local 012, and the190last session to end without quitting keeps them (see191[Unsaved work](#unsaved-work)).192193**What runs is the room's.** Files linked to the workbook are followed194once for everyone, and notebook cells run once: any session may run or195stop them, everyone sees them running and their outputs, and they go on196when the one who started them leaves, until the last one does.197198**Agents.** With sharing on, the server also listens for agents run by199its own user on its machine: `012 mcp --attach budget.012` joins the200room of a file someone has open, where the agent shows with a ◆ and its201changes wait as suggestions for anyone in the room to accept202([Live mode](../agents/live.md#012-serve)). The room closes when its203last person leaves, whatever agents are still in it.204205## Each session206207Besides the shared workbook, every session is a separate 012 with208nothing shared but the process: its own cursor and selection, undo of209its own changes, clipboard, theme, JEV cache and terminal state. What210it learns about the terminal comes from the client: the window size and211its changes, the colors (from the `TERM` the client sends), the light or212dark background, kitty or sixel graphics, and the clipboard, which213Ctrl+C sets on the client's machine through OSC 52.214215[Macros](../sheets/macros.md) can be recorded, run, renamed and deleted in a216session, but not edited as scripts: that would start an editor on the217server. A file's macros ask for trust once per session, since the218session isn't the server's own computer, and scripts have no file,219network or clock access in any case. A macro running in a shared220workbook is one change: the others' keys wait until it ends.221222## Notebooks223224A session opens [notebooks](../nushell/notebooks.md) and shows their225cells and saved outputs, but runs none of their cells: a cell is a226program on the server, with the server's user and every file it can227reach, well beyond the served directory. `serve-shell = on` in the228server's config lets sessions run them, as the user 012 serve runs as,229following the `shell` option as the local app does; a file's cells still230ask once per session before they run. Only turn it on when everyone holding an231authorized key may run programs on the server. The same setting decides232whether nu is started to [highlight and check](../nushell/notebooks.md#writing-a-cell)233the cell being written; without it, 012's own highlighting answers.234235## Unsaved work236237A session that ends without the user quitting, because it was idle for238`--idle-timeout`, because the server is stopping (Ctrl+C, SIGTERM), or239because 012 [stopped on an internal error](../files/saving.md#if-012-crashes)240(whose report goes to the server's log), keeps its unsaved changes: the whole workbook is written to241`.012-recovery/<name>-<time>.012` in the served directory, and the242client's terminal is told where before the connection closes:243244```245012: closed after 30m0s without input246012: unsaved changes kept in .012-recovery/budget-20260927-140203.012; open budget.012 again to restore them247```248249The next session opening `budget.012` (from File > Open or250`ssh -t host budget.012`) asks on the context line: Enter restores them251as unsaved changes to `budget.012`, D deletes them, Esc leaves them for252next time. Saving a restored workbook removes its recovery file, and253saving over a file that changed on disk still asks first. A sheet never254saved is kept as `(untitled)-<time>.012` and offered when a session255starts on a new sheet.256257In a shared workbook, only the last session in the room keeps them:258one that ends while others remain leaves the workbook to them, and says259so when it idled out. Restoring kept changes opens the room again on260them, once nobody else is in it.261262It stays small: nothing is kept for a workbook without changes or with263no cells, and only the newest three files per name are kept. The264directory is created `0700` and its files `0600`, and it's hidden, so no265name typed in a session reaches it; only 012 itself reads and writes266it. A session whose client just goes away (a dropped connection)267keeps nothing, and neither does quitting, which asks about unsaved268changes as the local app does.269270## Files changed elsewhere271272Saving over a file that changed on disk since it was opened or last273saved (another program wrote it, or a session with `--share off`) asks274first on the context line: Enter overwrites it, S saves under another275name, Esc cancels. The same check protects the local app from other276programs writing the file.277278## Limits279280- An idle session is closed without saving over its file; unsaved281 changes go to a recovery file instead.282- A dropped connection loses unsaved changes, unless others share the283 workbook.284- Sessions in one room take turns: a long change (a sort of a million285 rows, a macro) makes the others wait for it.286- Each session holds its screen's buffers, about a megabyte, plus the287 sheets it opens; sessions sharing a file hold one workbook between288 them. A change reaches the others' screens within a frame. Fifty289 sessions typing at once get their frames as fast as ten do: see290 [Bounds of support](../contributing/limits.md#serving-over-ssh) for291 the measurements.