Content
# Quarterdeck
**Drive remote development from Claude Code.** Claude Code runs locally in VS Code, but your
code lives on remote Linux servers — including repositories too large (hundreds of TB) to clone.
Quarterdeck gives the agent a set of `remote_*` tools so it connects over SSH, searches, reads,
and edits code *on the server*, runs commands and tests there, and reports back in your local
chat. A read-only mount lets you browse the same tree in the VS Code explorer.
It is a thin, self-hosted control deck: a server registry, a pinned MCP server, a few mount
scripts, and a rule set the agent follows. You bring your own servers and your own Claude Code.
---
## Requirements
- **Claude Code** in VS Code, with an active Anthropic plan.
- **One or more remote Linux servers** you can reach over SSH, each with `bash`, `python3`, and
`git`. `ripgrep` (`rg`) is nice to have (fast search); without it search falls back to `grep`.
- **Passwordless SSH** to those servers via `~/.ssh/config` (`ssh <host> true` must succeed with
no prompt — the agent cannot type passwords).
- Local tools: `tmux` ≥ 3.0, `uv` (provides `uvx`), and `rclone` (only for the file-tree mounts).
## Quickstart
```bash
# 1. Install local dependencies (macOS)
brew install tmux rclone
curl -LsSf https://astral.sh/uv/install.sh | sh # if you don't have uv yet
# 2. Clone and open in VS Code
git clone https://github.com/avanturist322/quarterdeck.git
code quarterdeck
```
Opening the folder, the Claude Code extension picks up [.mcp.json](.mcp.json) and asks to approve
the project MCP server `remote-ssh` — approve it. The agent now has the `remote_*` tools.
```bash
# 3. Check prerequisites
tests/smoke.sh # expect "0 fail"
```
**Add a server.** Put an alias in `~/.ssh/config` (key-based, passwordless), then add one line to
[remotes.conf](remotes.conf) — `name ssh-host path-to-repo`:
```
# Shared account; has rg, git, python3.
example-gpu-box my-gpu-box /home/you/code/awesome-project
```
The *name* is how you refer to the server in chat; the *ssh-host* is the alias from
`~/.ssh/config`; the *path* is the repo directory on the server. You can also just tell the agent
"add a server example-gpu-box, host my-gpu-box, repo /home/you/code/awesome-project" and it will
append the line and verify the connection.
**Work.** Nothing to run by hand — just talk to the agent, naming the server:
> Let's work on example-gpu-box. Find where `load_config` is defined and rename it to
> `read_config`, updating all call sites.
The agent reads the host and path from `remotes.conf`, runs `remote_connect` (always with
`agent_forwarding=false`), checks `git status` (did anyone leave uncommitted work on a shared
account?), searches with `remote_grep` (**on the server** — instant at any repo size), reads,
edits, runs tests, shows you the diff. The rules it follows are in [CLAUDE.md](CLAUDE.md).
**Browse the tree.** `bin/rmount example-gpu-box` makes the server's repo appear as
`mounts/example-gpu-box/` in the explorer — read-only, lazy (opening a file fetches only its
bytes), so a 200 TB repo browses like a local folder. `bin/rumount example-gpu-box` removes it.
**Verify a server end to end:** `python3 tests/acceptance.py example-gpu-box` (~1 min, expect
`0 fail`). Details in [tests/ACCEPTANCE.md](tests/ACCEPTANCE.md).
## Project folders: working on several repos in parallel
Each remote repository gets a folder [projects/](projects/)`<name>/` (the name matches the one in
`remotes.conf` and the mount). Inside, three files with distinct roles:
| File | Who writes it | What's in it |
|---|---|---|
| `PROJECT.md` | you | the passport: server, goal, context, constraints |
| `TASKS.md` | you (agent ticks off done) | tasks: in progress / backlog / done |
| `JOURNAL.md` | the agent | session log: what was done, loose ends (jobs, logs) |
The loop: you write a task in `TASKS.md` → say "let's work on `<name>`" → the agent reads
`PROJECT.md` and `TASKS.md`, connects, and does it → on completion it ticks the task off and
appends to `JOURNAL.md`. A new chat session loses no context — the agent restores it from these
files. See [projects/example-gpu-box/](projects/example-gpu-box/) for a filled-in example, and
[projects/_template/](projects/_template/) to start a new one (copy it to `projects/<name>/`,
fill in `PROJECT.md`, add a line to `remotes.conf` — or ask the agent to "set up project ...").
## Tracking progress
**Watch the agent live.** Every connection is a tmux session on the laptop. In the VS Code
integrated terminal:
```bash
tmux ls # active sessions
tmux attach -t remote-ssh-mcp/<host> # watch the agent's commands live
# detach: Ctrl-b d (do not close the session!)
```
Every command the agent runs on the server is visible — transparency instead of magic.
**Long-running jobs (training, etc.).** Say "start training and keep an eye on the log". The agent
launches it under `nohup` with a log in `~/.quarterdeck-jobs/<job>-<date>.log` on the server — the
chat is not blocked and the job survives a dropped connection. Ask "how's the training?" and the
agent tails the log and `pgrep`s the process. You can close the laptop; in a later session, say
"check the training on example-gpu-box" and the agent finds the log in `~/.quarterdeck-jobs/`.
## How it works
```
laptop (macOS) server (Linux)
┌─────────────────────────────────────┐ ┌──────────────────────────┐
│ VS Code + Claude Code extension │ │ (often a shared account) │
│ │ │ │ │
│ ├─ MCP server remote-ssh (uvx) │── ssh ───▶│ repository of any size │
│ │ └─ tmux session per host │ (tmux) │ (nothing is copied │
│ │ remote_run/read/write/… │ │ in full) │
│ │ │ │ │
│ └─ bin/rmount (rclone nfsmount) │── sftp ──▶│ same file tree, visible │
│ mounts/<name>/ — read-only │ (lazy) │ in the explorer │
└─────────────────────────────────────┘ └──────────────────────────┘
```
Two independent channels:
- **The work channel — the MCP server [remote-ssh-mcp](https://github.com/avanturist322/remote-ssh-mcp)**
(started automatically from [.mcp.json](.mcp.json)). One persistent tmux+SSH session per server:
shell state (cwd, venv, variables) lives between commands, as if you were sitting in SSH
yourself. The agent's tools: `remote_connect`, `remote_run`, `remote_read`, `remote_write`,
`remote_edit`, `remote_grep`, `remote_glob`, `remote_status`, `remote_disconnect`.
- **The eyes channel — a read-only mount** via `rclone nfsmount` ([bin/rmount](bin/rmount)): the
repo tree appears in the VS Code explorer as `mounts/<name>/`. No macFUSE, no admin rights.
Listings and bytes load on demand; nothing is written to disk (cache ≤ 1 GB).
### Huge repositories: what matters
- **Nothing is copied in full** — a hard rule for the agent. Search (`remote_grep`, `remote_glob`)
runs on the server; only the matched lines come back over the wire.
- `remote_read` returns at most 1 MB per call, and files larger than ~300 KB are read **in 256 KB
chunks** (`offset`/`limit`) — a single call with a large limit hangs for ~2 minutes due to
tmux-channel limits. This is covered by a test (1.5 MB → 6 chunks → byte for byte).
- The mount is lazy: listing a directory is one sftp request; opening a file fetches only its
bytes. The rclone cache is capped at 1 GB and self-evicts.
## Safety on a shared account
Several people often work under one user on these servers. Hence the rules (baked into
[CLAUDE.md](CLAUDE.md); the agent follows them itself):
- **`agent_forwarding=false`, always.** With ssh-agent forwarding (`ssh -A`) the agent socket is
reachable by every process of that user on the server — a colleague under the same account could
sign with your key. Checked by the acceptance test.
- **Git hygiene.** Before and after edits — `git status`/`git diff`: someone else's uncommitted
work is visible immediately, and the agent reports it and leaves it alone. `remote_edit` is a
read-replace-write: if the file changed underneath and the old string is gone, the edit fails
and the other person's change survives (covered by a "conflict with a colleague's edit" test).
- **Destructive actions** (`rm -rf`, `kill`, `git reset/checkout/clean/push`, switching branches,
installing packages) happen only after explicit confirmation.
- **Transparency:** every agent command is visible in the tmux session (`tmux attach`) — including
to colleagues on the same account.
## Troubleshooting
| Symptom | Cause and fix |
|---|---|
| `Address already in use` in ssh output | A `LocalForward` in `~/.ssh/config` collides with a second connection. Not an error — the scripts use `ClearAllForwardings=yes`; if the MCP server is noisy, add a forward-free alias. |
| `remote_connect` times out (~45s) | The plugin never saw the prompt: a custom PS1 didn't match the detector (`[$#>❯»]\s*$`) or the MOTD is very slow. Simplify PS1 for non-interactive sessions in the server's `~/.bashrc`. |
| Session "hangs" (commands don't return) | The shell is probably waiting for more input. `tmux attach -t remote-ssh-mcp/<host>` to look; if unfixable, `tmux kill-session -t remote-ssh-mcp/<host>` and `remote_connect` again. |
| `REMOTE HOST IDENTIFICATION HAS CHANGED` | The server's host key changed (reinstall / new IP). Confirm it's expected, then `ssh-keygen -R <host>`. |
| `remote_run` returns exit 255 | That's ssh's own code: the connection dropped or never opened. Check `ssh <host> true`, the network, the VPN. |
| `remote_read` "hangs" ~2 minutes then fails | You requested too large a chunk. Files > ~300 KB — only in chunks `limit=262144` (see above). The per-call limit is 1 MB. |
| `rmount` says "mount did not appear" | Check `ssh <host> true`. If OK, look at the log: `pkill -f "rclone nfsmount.*<name>"` and run it by hand without `--daemon`, with `-vv`. On stripped-down sshd (e.g. jupyter) the `shell_type=unix,disable_hashcheck=true` options baked into rmount handle it. |
| Mount won't release | `bin/rumount <name>` escalates on its own: `umount` → force → stop the rclone process. Close any files open from the mount and retry. |
## Pinning the MCP server
In [.mcp.json](.mcp.json) remote-ssh-mcp is pinned to a specific commit — protection against an
upstream swap: bumping the version is a deliberate edit of that file. To update: pick a new
commit/tag in the [repository](https://github.com/avanturist322/remote-ssh-mcp), replace the hash
after `@`, restart the session, and run `python3 tests/acceptance.py` against your servers.
> This project pins a fork at [avanturist322/remote-ssh-mcp](https://github.com/avanturist322/remote-ssh-mcp)
> for longevity. The original is [Square596/remote-ssh-mcp](https://github.com/Square596/remote-ssh-mcp) — credit to the upstream author.
## Repository layout
```
quarterdeck/
├── CLAUDE.md # agent rules + pointer to the registry (always in context)
├── remotes.conf # server registry: name, ssh-host, repo path
├── projects/<name>/ # passport, tasks and journal per project (_template — the template)
├── .mcp.json # the remote-ssh MCP server (uvx, commit-pinned)
├── bin/rmount, bin/rumount # read-only mount of the server's file tree
├── bin/lmount # live mount of a LOCAL repo (hybrid projects)
├── mounts/<name>/ # mount points (gitignored)
├── tests/smoke.sh # prerequisites and connectivity
├── tests/acceptance.py # full e2e over MCP JSON-RPC (30 checks)
├── tests/ACCEPTANCE.md # acceptance: automated + manual checklist
├── .vscode/settings.json # mounts/ excluded from VS Code indexing
└── .claude/settings.json # permissions for remote_* and bin/*
```
## License
[MIT](LICENSE) © 2026 Egor Cherepanov
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available