Sshboard

io.github.meta-taroPublished by GitHub account meta-tarov0.1.39Updated Nov 6, 2026

An AI agent reads your server over the one SSH session you are already watching.

Overview

AI-generated overview

Desktop SSH app with a built-in MCP server that lets an AI agent read your servers over the same SSH session you are watching.

What you can do
  • List connections, browse directories, stat and read files, search, download, and check disk usage
  • Inspect processes, service status, runtime versions, logs, and listening network ports
  • Run read-only commands only by id from a person-written allowlist, with refusals logged
  • Run or propose health checks built from fixed criteria, with results shown on screen
  • Run state-changing operations only from a person-written list, each needing on-screen approval and an hourly ceiling
When to use it
Useful when you maintain a VPS or shared host and want an agent to look at configs, logs and services instead of you pasting state back and forth. Suited to incident triage and read-only investigation on live servers, with writes kept behind explicit human-written allowlists.
Requirements
A desktop app for macOS or Windows (Linux is not a distribution target); the MCP server runs inside the app, not as a separate package. Building from source needs Rust 1.98.0, Node 20 or newer and pnpm; Docker only for the local test server. The agent connects over stdio via the app's proxy or over HTTP on port 22022 with an Authorization Bearer token. SSH keys are best kept in ssh-agent.
Before you install
Alpha software, and installers are not code-signed, so SmartScreen or Gatekeeper may block them. The HTTP registration method leaves the bearer token in plain text in the agent's config file, and whoever holds it can read the servers you are connected to. The agent's terminal is shared with yours, so your input is locked out while it holds it. Allowlists ship empty, and the product cannot verify that a command a person wrote is truly read-only. With become set to ask, a sudo password is typed…

Installation

In SourceWeft

  1. Open Sshboard in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.

Other MCP clients

Follow the launch instructions in the repository.

README

sshboard

English / 日本語

Your AI agent and you, looking at the same server, on the same screen, over one SSH connection.

You keep your own SFTP client and your own terminal. The agent works beside you — inside a fence you drew — and every single thing it does scrolls past in your window while it happens.

[The connection list, with a connection being edited]

⚠️ This is alpha. It is in use on production servers (investigating a failed certificate renewal, restarting pm2, and so on). Installers are published for Windows (x64 / ARM64) and macOS. Passing tests does not mean it works as a tool — we shipped a build whose terminal displayed not one byte for three days (Issue #10).

⬇ Download · Direction · Every decision, with its reasoning


When you would reach for this

"Renew the certificates on three hosts, today"

You have a terminal open per host and you are copying the same command into each, watching for the one that fails. sshboard opens one console per connection — or two on the same host, side by side — and the agent can read all of them while you work. Not "the agent runs it for you": the agent watches what you are doing and tells you which pane went wrong.

[One shared console; the tabs along the top are the open connections]

"Mail stopped arriving and I don't know where to look"

You ask the agent. It reads /etc/postfix/main.cf, checks the service, tails the log — over the same SSH session you are already on, and you see each step as it happens. It cannot run an arbitrary command to do it: there is no run_command(cmd) tool.

[Human] $ cd /var/www[AI]    $ df -hFilesystem      Size  Used Avail Use% Mounted on/dev/vda1        50G   38G   10G  80% /[AI]    read /etc/postfix/main.cf  (4.1 KB)

"I want help, but not an agent loose on my server"

The agent may write only inside directories you listed, one connection at a time. The list starts empty, so it cannot write a single byte until you say where. Delete, rename, move, chmod, restart, package installs, sudo — none of them exist for the agent. You are not restricted anywhere.

[The two-pane file browser; the agent's writable directory is shown at the top]


Try it in five minutes

Install the app, then point your agent at it. There is nothing else to run — the MCP server lives inside the app.

sh
# 1. a throwaway SSH server on your own machine (needs Docker)sh tools/test-server/up.shssh-add tools/test-server/.key
# 2. the apppnpm installpnpm --filter desktop tauri dev

Register 127.0.0.1:2222, user probe, connect, then press Copy the MCP command in the window's title bar and paste it into your agent:

sh
claude mcp add --scope user --transport http sshboard http://127.0.0.1:22022/mcp \  --header "Authorization: Bearer <the token the button copied>"

Prebuilt installers are on the releases page (Windows x64 / ARM64, macOS Apple Silicon). They are not code-signed, so SmartScreen and Gatekeeper will both stop you the first time.


What you see while the agent works

Every operation, yours and the agent's, lands in one band. You do not have to ask what it did.

[The activity band: one line per operation, with [AI] marking the agent's]

When something fails, the same log the agent reads over MCP is on your screen — which stage it reached, why it stopped, and what to do next. It carries no hostnames, no usernames and no secrets.

Why this exists

There are services still running on ordinary shared hosting and VPSes. Maintain one of those with an AI agent and this happens every single time:

you : I want to fix this service's configAI  : I can't see the server, so please tell me its stateyou : (open a terminal, look, paste)AI  : now show me that config file tooyou : (open SFTP, download, paste)

This tool exists to delete that round trip. The agent does not need to operate your server. Letting it see is enough.

How you actually use it

This is not a tool you drive by clicking buttons. You talk to an AI agent in your terminal, and the agent calls sshboard's built-in MCP server.

you → the AI agent in your terminal: "mail isn't arriving. look into it"              │              │  MCP (inside the app — no extra process)              ▼        sshboard  ── one SSH ──▶  server              │     (sftp / exec)              ▼   an [AI] line scrolls past in sshboard's window   ── you are just watching. what it read and what it typed is visible as it happens
[Human] $ cd /var/www[AI]    $ df -hFilesystem      Size  Used Avail Use% Mounted on/dev/vda1        50G   38G   10G  80% /[AI]    read /etc/postfix/main.cf  (4.1 KB)

You are not restricted. The two-pane file browser and the terminal tab work like any other SFTP client and terminal. Only the agent's side is fenced.

What the agent may call (Phase 1 — reading)

list_connections / list_directory / stat / read_file / search / download / disk_usage / process_list / service_status / runtime_versions / read_log / network_listen / run_readonly (allowlist) / list_checks / run_check / propose_check (health checks)

There is no run_command(cmd). One such tool does everything, and once you have it the only thing standing between an agent and a destructive command is a promise not to use it. A promise is a runbook, not a gate.

With run_readonly the agent may pass only an id that a person wrote into readonly.toml. There is no argument slot. The list ships empty — until you write it, nothing runs at all. Refusals show up in the window's activity band and are recorded in readonly-refused.log. Adding entries is a person's job.

The one success criterion

The agent stops saying "please tell me the state of the server."

Not downloads, not stars. Whether the round trip disappeared during real maintenance.

It will not modernise your setup

  • This is not a tool for migrating your server somewhere else
  • Working with the setup you already have is the point
  • These services will still be here in ten years. Nothing exists today that brings AI development to them

Shape

   ┌──────────┬──────────┐        ┌──────────┐   │  files   │ terminal │        │   MCP    │   │ two-pane │   tab    │        │          │   └────┬─────┴────┬─────┘        └────┬─────┘        └──────────┴───────┬───────────┘                           ▼              ┌────────────────────────┐              │  Operation Engine      │  ← exactly one implementation lives here              └───────────┬────────────┘                          ▼                    one SSH connection                      (sftp / exec)

We do not keep two SFTP implementations, and we never open an SSH session you cannot see. Being invisible is the single biggest danger here.

Whoever touched what flows into the same band. Different surfaces, one record.

[Human] $ cd /var/www[AI]    $ df -hFilesystem      Size  Used Avail Use% Mounted on/dev/vda1        50G   38G   10G  80% /[AI]    read /etc/postfix/main.cf  (4.1 KB)

Phase 1 is read-only

No writes at all. Every intended use — incident triage, mail-config investigation, writing up a proposal — is reading. That makes the risk close to zero, which means you can point it at a live production server on day one.

There is no run_command(cmd)

Put one arbitrary-command tool in place and it can do everything. That is exactly why it does not exist.

  • Hand it over and the only remaining defence against a destructive command is a promise
  • A promise is a runbook, not a gate
  • With an allowlist, dangerous commands are not expressible
What readonly.toml looks like (a person writes it)
toml
version = 1
[[command]]id = "uptime"          # the only value the agent may passrun = "uptime"         # what actually runs. **exactly what a person wrote**description = "how long it has been up"

The product ships with zero entries. Nobody has the list of what real maintenance needs, and filling it in by guesswork gets it wrong. Refused calls pile up in readonly-refused.log — read that, and add only what was genuinely needed.

What this cannot verify: whether the run you wrote is truly read-only. The product cannot know. No machine can tell uptime from rm -rf / by inspection. What this prevents is exactly one thing: the agent composing a command string.

The human side (the GUI) is not restricted. Use it as an ordinary SFTP client and terminal. Only the agent's side is fenced.

Health checks (checks.toml)

Group the things you look at in the same order every time — disk, memory, a service, a listening port — into one check. Write it once; a person runs it from the menu (Checks → Run) and an agent runs the same check with run_check.

  • A step only names an id from readonly.toml. There is no field for a command line
  • Criteria come in fixed shapes only: text present (contains) / absent (absent), a number below / above a limit
  • Each step comes back good, bad or unknown. Something that could not be measured is never counted as good
  • The run does not stop at the first bad step, and the previous value sits beside the new one (until the app quits)
  • Every step shows on the band before it runs. Checks the agent runs appear in the Checks dialog too
  • The list ships empty. A person writes it; the agent cannot
  • The agent can propose a check with propose_check. Every step and criterion appears on screen, and only the person can save it (saving runs nothing)

A template lives in docs/checks.example.toml.

State-changing operations (operations.toml)

The list ships empty. Until you write this file, the agent can run nothing.

This is a separate file from the read allowlist (readonly.toml), so that write operations never live in a file called readonly. Telling them apart by name is the point. Put it next to connections.toml.

toml
version = 1
[[operation]]id = "restart-httpd"run = "sudo systemctl restart httpd"description = "restart Apache"max_per_hour = 3
fieldwhat to write
idthe only thing the agent may pass. Short, readable
runwhat actually gets typed. The agent cannot compose it
descriptionshown on the approval dialog. Words you will understand in six months
max_per_hourhourly ceiling. 0 is rejected (there has to be a place it stops)

Gates it passes through before running

  1. It is in the list a person wrote. Unknown ids are refused, and the refusal is recorded
  2. It is under the hourly ceiling
  3. A person approves it on screen. The agent's first call is always refused, and the window shows what would actually run
  4. An approval is spent once. One approval, one run
  5. An approval expires after five minutes. People press allow and the agent never calls back (the conversation moves on). We do not leave approvals lying around — with become = "ask", the password is dropped here too

A person at the screen does not pass 3–5. They are looking at it. The one exception is the become = "ask" password — that is not approval, it is a credential, and the product cannot invent it.

Refused even if you write it down

No setting enables these. They are rejected even when present in operations.toml.

  • The cage itself — sudoers / visudo / authorized_keys
  • Keys — .ssh / id_rsa / id_ed25519 / id_ecdsa
  • Where passwords live — /etc/shadow / /etc/gshadow
  • sshboard's own config — connections.toml / readonly.toml / operations.toml / checks.toml
  • Users and roles — useradd / usermod / userdel / groupadd
  • Destruction — mkfs / fdisk / dd / rm -rf

The agent cannot widen its own cage. That is what this list is for.

Operations that need root (become)

operations.toml decides which commands may run. How privilege is obtained is written per connection (connections.toml).

toml
[[connections]]id = "..."become = "sudoers"   # scope is cut on the server side (recommended)# become = "ask"     # ask the person when it is needed. **never stored**# omit it entirely   # do not elevate
becomewhat sshboard typespassword
omittedexactly what you wrote in runnone
sudoerssudo -n …none (sudoers.d holds the scope)
asksudo -S -p '' …typed by a person, on screen, in the moment

Only a run that starts with sudo is touched. Everything else is left byte for byte. Choosing ask does not make non-sudo operations ask for a password.

sudoers (recommended)

<user> ALL=(root) NOPASSWD: /usr/bin/systemctl restart httpd, /usr/bin/systemctl reload httpd
sh
# always through visudo. get it wrong and nobody can sudo any moresudo visudo -f /etc/sudoers.d/sshboardsudo chmod 0440 /etc/sudoers.d/sshboard

This is a gate the OS enforces, not a promise the app makes. The scope is written in sudoers, so a person can audit it afterwards, and sshboard gains no secret.

Check that it works:

sh
sudo -n systemctl reload httpd    # passes with no password prompt → goodsudo -n systemctl restart nginx   # not listed → should be refused

If the second one succeeds, your scope is too wide.

ask (when you cannot touch sudoers)

Not being able to touch the server is a real situation — rented, no rights, too many machines. On a server where sudo -n answers "a password is required", the sudoers route was never open. That is what ask is for.

A password field appears on the approval dialog and a person types it there.

  • Never stored. Not on disk, not in the OS credential store
  • Never on the command line. It goes in over stdin, so it does not show up in ps
  • Never on screen and never in the log. What appears is $ sudo -S -p '' … and no more
  • One approval, one run. Dropped the instant it is used
  • Refuse, and it is dropped there and then

Reading a log only root can read works the same way:

toml
[[operation]]id = "read-letsencrypt-log"run = "sudo tail -n 200 /var/log/letsencrypt/letsencrypt.log"description = "find out why the certificate renewal failed"max_per_hour = 6

This does not apply to the read allowlist (readonly.toml). If it did, every read would ask for a password. Reads that need root belong in operations.toml.

Becoming root first with su is not recommended — the scope disappears. On servers that only have su -, use the console: the person types su -, presses [Hand to the AI], the agent carries on in the same shell, and hands it back with console_release without closing it. The person can always take it back.

What the product cannot guarantee

Whether an operation really does only what it says, the product cannot verify. What runs is a string a person wrote. As with readonly.toml, that is the part a person has to read and confirm. Which is why sudoers makes the OS enforce it.

There is no way to deliver an approval to someone who is away from the screen yet (.claude/decisions.md D47 — undecided). Today only a person at the screen can answer. With nobody there, the operation does not run.

What this will not do

not doingwhy
Let the agent write outside the fenceThe person sets where it may write, per connection. Empty by default = not one byte
Give the agent arbitrary commandsAn allowlist prevents it structurally
Give the agent sudoRaising privileges happens only inside operations the person wrote in operations.toml; the password is typed by the person for that one run and never stored
Build our own key storeDelegate to the OS credential store and ssh-agent. What you do not hold, you need not guard
Embed an AI chat UIThe agent lives outside the app
Generate reportsIf it can read, the agent writes the proposal. Nothing to add
Push you to migrateWorking with your existing setup is the point

Running it

This is still alpha. Before pointing it at a production server, run it once against the local test server.

Requirements

OSmacOS / Windows (Linux is not a distribution target)
Rustrust-toolchain.toml pins 1.98.0. rustup sorts it out
Node20 or newer
pnpmcorepack enable (npm / yarn are not used)
Dockeronly if you want the local test server

Keeping your key in ssh-agent is recommended. Then sshboard never receives a passphrase at all.

Start

sh
pnpm installpnpm --filter desktop tauri dev

Tests

sh
cargo test --workspace         # Rustpnpm --filter desktop test     # frontendpnpm --filter desktop check    # type check

Local test server

It does not touch your servers at all. It creates a throwaway key and brings up one machine in Docker.

sh
sh tools/test-server/up.sh        # upsh tools/test-server/up.sh down   # down

Connecting an agent (MCP)

Recommended — the token is never written anywhere
sh
# macOSclaude mcp add --scope user sshboard -- /Applications/sshboard.app/Contents/MacOS/sshboard --mcp-stdio-proxy
# Windowsclaude mcp add --scope user sshboard -- "%LOCALAPPDATA%\\sshboard\\sshboard.exe" --mcp-stdio-proxy

--scope user registers it for every folder. Without it, Claude Code keeps it only for the folder you typed the command in — open Claude Code anywhere else and sshboard does not even appear in /mcp.

This way the token is left nowhere (Issue #2). Started with --mcp-stdio-proxy, sshboard runs as a relay with no window. It reads the token itself and passes it to the running app.

  • The token appears neither in ~/.claude.json nor in your shell history
  • Move the port (SSHBOARD_MCP_PORT) and you do not have to re-register
  • If the app is not running you get: "sshboard is not running. Start the app and try again."

Only the app opens SSH connections. The relay holds no engine and no band. It adds not one invisible SSH session (prohibition 3).

Straight over HTTP

The "Copy the MCP registration command" button at the top right of the window copies a single line, token included.

sh
claude mcp add --scope user --transport http sshboard http://127.0.0.1:22022/mcp \  --header "Authorization: Bearer <token>"

--transport http is required. The path is /mcp and the token goes in Authorization: Bearer.

The port is fixed at 22022 (D33), and the token is reused, so you register once. Restarting keeps the same URL.

⚠️ claude mcp add leaves the token in ~/.claude.json in plain text. It is a loopback handle rather than a key, but whoever holds it can read the servers you are connected to. Remember claude mcp remove sshboard when you are done.

.mcp.json works too (also plain text on disk):

json
{  "mcpServers": {    "sshboard": {      "type": "http",      "url": "http://127.0.0.1:22022/mcp",      "headers": { "Authorization": "Bearer <copy it from the MCP button>" }    }  }}

If the port is taken, it does not quietly move somewhere else. The window says "failed to start (port 22022)". Set SSHBOARD_MCP_PORT to move it.

Wake the agent when the person answers (Claude Code channels)

When the person answers a passphrase, host key, console permission or operation approval on screen, sshboard pushes that fact to the agent's session. Even if the agent ended its turn instead of waiting, it wakes up and carries on — nobody has to tell it "I pressed it".

This uses Claude Code's channels (a research preview). Start Claude Code with:

bash
claude --dangerously-load-development-channels server:sshboard

Started without it, Claude Code silently drops the notice (await_answer still works as before). The notice says only which kind of question was answered — never where the connection points. In Team / Enterprise organisations an admin has to enable channels first.

Updates

When a new version exists, the window tells you at startup (D34).

It never replaces itself quietly. Downloading, installing and restarting are all things a person presses. This tool handles SSH keys, so it does not rewrite itself without being asked.

The update itself is signed with minisign, and Tauri always verifies it. That is separate from code signing (D12 — whether the OS shows a warning); what is guaranteed here is that the update came from us.

Things to know during alpha

  • It is not code-signed. Whether Windows stops you depends on how you fetched it (Issue #3, measured):
    • Downloaded in a browser, double-clicked → SmartScreen stops you
    • gh run download / CI / a script → it does not. No Mark of the Web, so SmartScreen's reputation check has nothing to trigger on
    • macOS Gatekeeper is untested
  • The agent may write only under directories a person listed for that connection. The list is empty by default — not one byte
  • The run_readonly allowlist is empty by default too. Nothing runs until a person writes readonly.toml. The purpose-built tools (disk_usage and friends) work without it
  • The terminal is shared between you and the agent. While the agent holds it your input is locked out, and [Stop] always works
  • Installers are attached to each Release (D32). Windows gets .msi and an NSIS -setup.exe, macOS gets a .dmg. They are unsigned, so Windows shows SmartScreen and macOS shows Gatekeeper. If a warning stopped you and you did not know what to do, please open an issue — that is what decides whether we buy a certificate (D12)

Stack

ShellTauri 2 (Windows / macOS)
CoreRust
UISvelteKit + xterm.js (we do not write our own ANSI parser)
SSHrussh + russh-sftp (both tried against 9 real machines before deciding — D6)
MCPInside the app (not a separate binary, not a separate build)
CredentialsOS credential store + ssh-agent

Licence

MIT

Development rules

This repository follows a baseline set of rules for AI-agent development. See .claude/rules/product-baseline.md and CLAUDE.md.

  • The AI commits, a human pushes (no pushing without human review)
  • Tests are never deferred and never deleted
  • This is a public repository. No server hostnames, IPs, usernames, personal names or personal email addresses in code, docs, commit history or screenshots

Source: README.md at commit d235d8b

Tools

0
SourceWeft has not started this package in its sandbox yet, so its tools are not known.

Version history

12
  1. v0.1.39LatestNov 6, 2026
  2. v0.1.37Nov 5, 2026
  3. v0.1.36Nov 5, 2026
  4. v0.1.35Nov 3, 2026
  5. v0.1.34Oct 29, 2026
  6. v0.1.33Oct 26, 2026
  7. v0.1.32Oct 26, 2026
  8. v0.1.30Oct 26, 2026
  9. v0.1.29Oct 25, 2026
  10. v0.1.27Oct 24, 2026
  11. v0.1.25Oct 23, 2026
  12. v0.1.24Oct 13, 2026

Score

50% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 days25 pts

    Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.

  • Auth declared10 pts

    Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.

  • Tools listed15 pts

    Answer tools/list without a credential, or ship a package that starts in a sandbox without one.

  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts