Terminal Hole
2026.06.25Expose your local terminal to a public IP for remote control from a browser.
把本地终端通过公网中继暴露给浏览器,让你离开电脑后仍能用手机操作本地的命令行工具(如 Claude Code、vim、tmux 等)。
工作原理
手机浏览器 公网 VPS 本地电脑
┌──────────┐ ┌──────────┐ ┌──────────┐
│ xterm.js│ WSS │ │ WSS out │ │
│ Web UI │◄───────►│ Relay │◄───────────│ Agent │
└──────────┘ └──────────┘ └────┬─────┘
│
┌───────┴──────┐
│ tmux 会话们 │
└──────────────┘
- Agent(本地):管理 tmux 会话,提供 Web 终端,主动 WSS 连出到 Relay
- Relay(公网 VPS):HTTPS 入口,认证浏览器,反向代理到 Agent
- 浏览器:xterm.js + 原生 JS,多 tab 切换不同 tmux 会话
关键设计:tmux 是终端的"持久化层",会话独立于任何网络连接。浏览器关了、网络抖了,会话不丢;只有 Agent 进程死了会话才消失。
前置要求
- Go 1.25+
- tmux(macOS:
brew install tmux,Linux:apt/dnf install tmux) - 一台有公网 IP/域名的 VPS(用于跑 Relay)
- 一个指向 VPS 的域名(用于 ACME 自动签证书)
安装
一键安装(Agent + Relay 通用,推荐)
curl -fsSL https://raw.githubusercontent.com/iannil/terminal-hole/main/scripts/install.sh | sh
装特定版本:
curl -fsSL https://raw.githubusercontent.com/iannil/terminal-hole/main/scripts/install.sh | sh -s -- --version v0.1.0
装到自定义路径:
curl -fsSL https://raw.githubusercontent.com/iannil/terminal-hole/main/scripts/install.sh | sh -s -- --install-dir /opt/bin
行为:
- root 跑装到
/usr/local/bin,普通用户装到~/.local/bin(并提示加 PATH) - SHA256 校验
checksums.txt - 检测 tmux 缺失时打印安装命令(Relay 不需要 tmux,可忽略)
Docker(仅 Relay,推荐用于 VPS)
# 首次初始化配置(交互式,写入 ./config/config.yaml)
docker run --rm -it \
-v "$PWD/config:/etc/terminal-hole" \
ghcr.io/iannil/terminal-hole-relay:latest init --mode relay
# 启动 Relay
docker run -d --name terminal-hole-relay \
-p 443:443 -p 80:80 \
-v "$PWD/config:/etc/terminal-hole" \
-v "$PWD/state:/var/lib/terminal-hole" \
ghcr.io/iannil/terminal-hole-relay:latest
镜像 tag:v0.1.0(精确版本)、0.1(小版本滚动)、latest(最新)。
注意:
- 必须把 DNS A/AAAA 记录指向 VPS 后再起容器(ACME 验签)
-v state卷保留 ACME 证书缓存和 signer secret;丢了 → 浏览器 cookie 全失效,要重新登录
从源码构建(开发/贡献者)
git clone https://github.com/iannil/terminal-hole.git
cd terminal-hole
go build -o terminal-hole ./cmd/terminal-hole
需要 Go 1.25+。跑测试还需本机装 tmux。
部署
1. Relay(在 VPS 上)
# 交互式初始化(生成 /etc/terminal-hole/config.yaml)
sudo ./terminal-hole init --mode relay
# > 域名? tp.example.com
# > 管理员账号? alice
# > 管理员密码?
# 启动(前台;生产可用 systemd 包装)
sudo ./terminal-hole relay
Relay 会:
- 在
:443上跑 HTTPS,用 Let's Encrypt 自动签证书 - 在
:80上跑 HTTP→HTTPS 重定向(可在 config 用http_listen: "-"禁用) - 接受 Agent 的 WSS 隧道连接(
wss://tp.example.com/agent-tunnel) - 持久化 HMAC signer secret 到
signer.key,重启后浏览器 cookie 不失效
DNS:先把域名 A/AAAA 记录指向 VPS,再启动 Relay(ACME 签证书时要验签)。
2. Agent(在本地电脑上)
# 交互式初始化(生成 ~/.config/terminal-hole/config.yaml)
./terminal-hole init
# > 选择密码存储方式:
# 1) 明文存 config(0600 权限)
# 2) password_file(自己创建文件,config 引用路径)
# 3) TH_AGENT_PASSWORD 环境变量(config 不存密码)
# > 中继地址? https://tp.example.com
# > 账号? alice
# > [根据选择] 输入密码 / 文件路径
# 启动
./terminal-hole agent
Agent 启动后会:
- 在
0.0.0.0:7777起本地 Web 终端(局域网设备也能访问,需账号密码) - 主动 WSS 连出到 Relay,注册自己
- 指数退避重连(1s → 2s → 4s → ... → 封顶 30s)
日常使用
- 本地启动 Agent,让它跑着(不要登出系统,否则 Agent 进程会被杀)
- 锁屏离开
- 手机浏览器打开
https://tp.example.com,输入账号密码登录 - 看到 sidebar 会话清单(首次为空),点 "+" 新建终端
- 在终端里跑
claude、vim、htop等命令,跟本地操作一致 - 关浏览器/tab 不杀会话;下次登录点回原会话继续
手机键盘:界面底部有特殊键面板(Esc / Tab / Ctrl / Alt / 方向键 / / / - / Enter / Backspace)。Ctrl/Alt 是修饰键,点一下高亮,下次输入组合生效。仅在触摸设备显示。
多端共享:tmux 原生支持多客户端 attach,手机和电脑可同时看同一个会话。
Agent 掉线自动恢复:浏览器订阅 /api/agent-status SSE,Agent 重连后自动刷新会话清单并重 attach 当前会话。
配置参考
Agent (~/.config/terminal-hole/config.yaml)
agent:
relay:
url: https://tp.example.com
username: alice
password: "secret" # 或 password_file / TH_AGENT_PASSWORD
local:
bind: 0.0.0.0:7777 # 默认值;改 127.0.0.1 可限制只 loopback 访问
shell: /bin/zsh # 默认从 $SHELL 推断
tmux_prefix: th- # Agent 管理的 tmux 会话前缀
Relay (/etc/terminal-hole/config.yaml)
relay:
domain: tp.example.com
listen: ":443"
http_listen: ":80" # 设为 "-" 禁用 HTTP 重定向
acme_cache_dir: /var/lib/terminal-hole/certs
signer_secret_file: /var/lib/terminal-hole/signer.key
allowed_origins: # WS origin allowlist;空则走同源策略
- https://tp.example.com
# 自带证书模式(与 domain/ACME 二选一):
# cert_file: /path/fullchain.pem
# key_file: /path/privkey.pem
accounts:
- username: alice
password_hash: "$2a$10$..." # bcrypt,init 自动生成
配置优先级:CLI flags > 环境变量(TH_* 前缀)> config 文件 > 默认值
安全
- 认证:username + password(bcrypt 哈希),浏览器登录用 HMAC 签名 Cookie
- TLS:Relay 强制 HTTPS(ACME 自动签或自带证书),不支持纯 HTTP 部署
- 限流:
/api/login5 次/IP/分钟,/agent-tunnel10 次/IP/分钟,超限 429 - 会话 ID 校验:仅允许字母数字+下划线+连字符,最长 256 字符
- 错误消息:上游错误细节不暴露给客户端,仅记录到服务端日志
- WS origin allowlist:可配置
relay.allowed_origins做纵深防御 CSRF - 稳定 signer secret:Relay 重启后浏览器 cookie 不失效
- 常量时间认证:登录失败时不区分"用户名错"和"密码错",防用户名枚举
已知限制(v1):
- 单用户单 Agent(不支持多机/多用户)
- Agent 密码可明文存 config(建议用
password_file或TH_AGENT_PASSWORD替代) - 无审计日志、无文件传输、无 WebAuthn
- Agent 前台运行,不自动重启(设计选择,简化使用模型)
子命令
terminal-hole init [--mode agent|relay] [--config PATH] # 交互式生成 config
terminal-hole agent [--config PATH] [--bind ADDR] # 跑 Agent
terminal-hole relay [--config PATH] # 跑 Relay
terminal-hole version # 打印版本
terminal-hole help # 帮助
状态与路线图
- Plan 1:本地 MVP(Agent + 本地浏览器终端 + 多 tab + 手机键盘)
- Plan 2:远程访问(Relay + WSS+yamux 隧道 + ACME)
- Plan 3:体验打磨(稳定 signer、限流、SSE 状态推送、origin allowlist、ServeMux 模式)
- Plan 4:分发(GoReleaser、install.sh、Docker、GitHub Actions)
详见 docs/design.md(完整设计)和 docs/superpowers/plans/(实现计划)。
开发
# 跑测试(需要本机装 tmux)
tmux kill-server 2>/dev/null; sleep 1
go test ./... -race
# 构建
go build -o terminal-hole ./cmd/terminal-hole
# 跑 vet
go vet ./...
项目结构:
cmd/terminal-hole/ 入口
internal/agent/ Agent(管理 tmux、本地 HTTP、隧道客户端)
internal/relay/ Relay(HTTPS、登录、限流、SSE、代理)
internal/tunnel/ yamux 多路复用隧道
internal/tmux/ tmux wrapper
internal/pty/ creack/pty 封装
internal/auth/ bcrypt + HMAC cookie + 中间件
internal/config/ AgentConfig YAML
internal/protocol/ WS 控制消息
internal/web/ 嵌入式静态资源 + 登录端点
internal/version/ ldflags 版本信息
internal/web/assets/ 前端源码(HTML/CSS/JS + vendor xterm.js)