Sshboard

io.github.meta-taro由 GitHub 账号 meta-taro 发布v0.1.39更新于 Nov 6, 2026

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

概览

AI 生成的概览

一款桌面 SSH 应用,内置 MCP 服务器,让 AI 代理在你正注视的同一条 SSH 会话上读取服务器。

可以做什么
  • 列出连接、浏览目录、查看文件属性与读取文件、搜索、下载,并查看磁盘占用
  • 查看进程、服务状态、运行时版本、日志以及正在监听的网络端口
  • 仅按人工写入白名单中的 id 执行只读命令,被拒绝的调用会记录在案
  • 运行或提议由固定判定条件组成的健康检查,结果会显示在界面上
  • 仅按人工写入的清单执行变更操作,每次都需要屏幕前的人批准,并受每小时上限约束
适用场景
适合维护 VPS 或共享主机的场景:希望代理自己查看配置、日志和服务,而不是由你反复粘贴服务器状态。适用于线上服务器的故障排查和只读调查,写操作则被限制在人工明确写好的白名单之后。
运行要求
需要 macOS 或 Windows 桌面应用(Linux 不是发布目标);MCP 服务器内置于应用中,不是独立软件包。从源码构建需要 Rust 1.98.0、Node 20 或更高版本以及 pnpm;Docker 仅用于本地测试服务器。代理可通过应用的代理以 stdio 连接,或通过 22022 端口的 HTTP 并携带 Authorization Bearer 令牌连接。SSH 密钥建议放在 ssh-agent 中。
安装前请注意
仍处于 alpha 阶段,安装包未做代码签名,可能被 SmartScreen 或 Gatekeeper 拦截。使用 HTTP 注册方式会把 bearer 令牌以明文留在代理的配置文件中,持有该令牌的人可以读取你已连接的服务器。代理与你共用终端,它占用时你的输入会被锁定。白名单默认是空的,且产品无法验证人工写入的命令是否真的只读。当 become 设为 ask 时,sudo 密码需在屏幕上当场输入,且不会被保存。

安装

在 SourceWeft 中

  1. 打开 控制台中的 Sshboard,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

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

来源:README.md,提交 d235d8b

工具

0
SourceWeft 尚未在沙箱中启动过该软件包,因此其工具未知。

版本历史

12
  1. v0.1.39最新Nov 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

评分

已满足适用检查项的 50%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分

    请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。

  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分