Every day

Your own client

Connecting Claude Code, Codex, your terminal and your editor to a Pupitre server over SSH, with the private key the app generated.

Once the machine is set up, Pupitre is not where you have to work. The server is an ordinary Ubuntu box reached over SSH as dev, and anything that speaks SSH attaches to it — the Claude desktop app, the ChatGPT desktop app and its Codex, your terminal, your editor.

This page is the practical part: where the key is, how to declare the host once, and what to fill in for each client.

What the server offers a client

Nothing special, on purpose:

  • an SSH endpoint on port 22 — or 443 as well, if you asked core.hardening for it;
  • the dev user, whose authorized_keys holds the public half of every device on your account allowed on that server;
  • your projects under the directory you chose, owned by dev;
  • the runtimes activated in non-interactive shells too, so a client that runs a command without a login shell finds the same node, python and claude you do;
  • tmux, systemd, the dev command, and the databases on 127.0.0.1.

That last point about non-interactive shells is what decides whether a remote client is pleasant or maddening. The runtime modules write their activation into .zshenv, which zsh reads for every shell, for exactly this reason.

Where the app keeps the key

Pupitre generates an ed25519 key per server and keeps it in its own data folder, in mode 0600. It never rewrites your ~/.ssh/config: when you ask, it adds one line there, and only that one, described below.

SystemFolder
macOS~/Library/Application Support/@pupitre/desktop
Windows%APPDATA%\@pupitre\desktop
Linux~/.config/@pupitre/desktop

Inside it:

ssh/config        one Host block per server, passed to ssh with -F
ssh/known_hosts   the host keys the app pinned
keys/device       this computer's key, the one a server granted by your organisation opens with
keys/srv-xxxxxxx  the key of a server you added yourself, mode 0600

Each key has its .pub beside it. Open ssh/config to find out which server is which: each block carries its address, its port, its user and the path of its key — which is keys/device for a granted server, and keys/srv-… for one you added.

Host pupitre-srv-m9k2p1
  HostName 203.0.113.10
  Port 22
  User dev
  IdentityFile /Users/you/.pupitre/desktop/keys/srv-m9k2p1
  IdentitiesOnly yes

The path goes through ~/.pupitre/desktop, a link the app keeps pointed at its data folder. The folder itself has a space in its name on macOS, and often on Windows; OpenSSH reads a quoted path, but JetBrains Gateway parses the same file with its own code and splits IdentityFile and UserKnownHostsFile on the space, quotes or not. The link is what makes the app’s block readable by both.

Point clients at that file rather than copying it somewhere else. Deleting the server in the app takes its key with it, and a copy you made elsewhere would be an orphan that still opens your machine.

Declare the host once

Everything below needs your own ~/.ssh/config to know the server: ssh, the editors and the coding agents read that file only, not the one the app keeps in its folder. The app does it for you. In Settings › SSH, tick Let ssh, my editors and my coding agents reach my servers by name: one line is added at the top of ~/.ssh/config, and nothing else in it moves.

Include "/Users/you/Library/Application Support/@pupitre/desktop/ssh/config"

Every server of the app then answers to its SSH name — the word chosen beside its name when you add it, atelier by default for a server called “Atelier”, changed from the server’s entry in Settings › Servers, the SSH setting shows it — with the app’s key, IdentitiesOnly and the pinned host key. A server whose SSH name is already a Host of your file keeps its identifier alone, pupitre-srv-…. Unticking the box takes the line back. Then check it:

ssh atelier "dev status"

A machine that answers with its projects is a machine every client on this page can now reach. If it asks for a password instead, the key is not the one dev accepts — reveal the public key on the server screen and compare it with ~/.ssh/authorized_keys on the machine.

If you would rather write the entry yourself, copy the values from the app’s own block, link included: a path with a space in it needs quotes for OpenSSH and breaks JetBrains Gateway regardless.

Host atelier
  HostName 203.0.113.10
  Port 22
  User dev
  IdentityFile /Users/you/.pupitre/desktop/keys/srv-m9k2p1
  IdentitiesOnly yes

Claude Code

Two ways, and they end up in the same place: ai.claude has already installed Claude Code for dev, with the machine context and the Pupitre skills.

From the Claude desktop app. Open the environment dropdown before starting a session and choose Add SSH connection. It asks for four things:

FieldWhat to put
NameWhatever you call the machine
SSH Hostpupitre, or dev@203.0.113.10
SSH Port22, or the one you chose
Identity FileThe path of the app’s key, from the table above

The Identity File field is why the app’s key works here with no further ceremony: give the path and nothing else has to move. Leave the field empty if the host already resolves through your ~/.ssh/config.

From a terminal. Open a session, keep it in tmux so it survives the network, and start the tool where the project is:

ssh atelier
tmux new -A -s claude
cd ~/projects/your-project && claude

Either way you sign in once, from whichever session you are in: the tool prints a URL, you approve it in your browser. It is your subscription; Pupitre resells nothing and stores no token for you.

Codex

ai.codex installs Codex through mise, and the shims are on the path of every shell — including the non-interactive one a remote client opens. That is the requirement the app has of the machine: it starts Codex on the remote host through your login shell.

From the ChatGPT desktop app. Codex there adds a remote project from an SSH host: it reads the concrete host aliases of your ~/.ssh/config and resolves them with OpenSSH, so the server’s name, declared above, is what it offers you. There is no field for a key: the IdentityFile line of the app’s block is what points at its key.

From a terminal. Same shape as Claude Code:

ssh atelier
tmux new -A -s codex
cd ~/projects/your-project && codex

Claude Code and Codex live on the same machine without argument. They see the same projects, the same runtimes and the same databases, because there is only one machine.

Your terminal

ssh atelier is enough, and the agent binary is also linked as dev, so the operations the app performs exist in a bare terminal:

dev status                    what runs, on what port, with what memory
dev up my-project             start it in its tmux session
dev restart all
dev logs my-project -f
dev sync my-project           git pull, then dependencies
dev attach my-project         prints the tmux line to paste in your own terminal
dev branch                    the branch each project sits on
dev branch my-project main    switch that project to another branch
dev db url                    the connection string, for a client on your laptop
dev doctor                    a short diagnosis of the machine

The agent reads its enrolment and its licence from files only root can open, so the command speaks to sudo -n pupitred serve, which sudo runs without a password. dev db dump, dev db import and dev backup now go through a privileged session, for which sudo asks for the sudo password of dev on the terminal. Typing sudo dev … yourself changes nothing.

dev db also takes shell, dump and import, and the engine goes without saying when the machine holds one database — dev db url postgres when it holds two.

Add --json to any of them and you get the agent’s own answer rather than a rendering of it, which is what makes these commands usable by a script, or by an agent working in a terminal.

Your editor

VS Code, Cursor and Windsurf connect over Remote SSH, Zed through zed://ssh, JetBrains IDEs through Gateway. They read the same ~/.ssh/config entry, and the editor.* modules preinstall the remote backends so the first connection is immediate rather than a ten-minute download. See remote editors.

When it does not open

  • It asks for a password. The key is not in dev’s authorized_keys. Copy the public half from the server screen in the app.
  • Permissions 0644 for ... are too open. The private key has to be readable by you alone: chmod 600 on macOS and Linux. On Windows, keeping it in %APPDATA% is enough — a copy dropped in a shared folder is not.
  • Nothing answers on port 22. If your network filters it, core.hardening can put SSH on 443 as well; change Port in your entry.
  • The host key changed. Reinstalling a machine changes it. The app asks you about it on its side; in your own ~/.ssh/known_hosts, remove the old line.
  • Gateway says Could not verify host key without asking you. The app’s block points at its own known_hosts with strict checking, and Gateway could not read the path. Check that ~/.pupitre holds a link named after the app’s folder and that nothing else took its place; the app puts it back at its next start.
  • codex: command not found from a remote client. The client opened a shell that did not read .zshenv. Check with ssh atelier 'which codex'.

Revocation still works

A device removed in the console loses its key on the server at the agent’s next check-in — whatever was using that key — unless it was the server’s last key. That is the reason to point your clients at the app’s key rather than at a copy of it.

What stays in the app

Pupitre remains the place for the things a chat client has no business doing: inspecting the machine, installing and configuring services, hardening, the live install report, project registration, secrets, the service status the agent reports, updates and alerts.

Its terminal is there for the day you want it — the first sign-in, a log you want to watch, a command you would rather run where the install happened. It is not the price of admission.