SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

如何在远程 VPS 上用 tmux 运行 Claude Code

在始终在线的 Linux VPS 中通过 tmux 运行 Claude Code,避免 SSH 断开导致 agent 退出。本文涵盖安装、非 root 加固、会话恢复,以及 SIGHUP、swap 和 TUI 无法重新附加等问题。

问题在于笔记本盖子,而不是 CLI

Claude Code 在笔记本上运行正常,但合上盖子后情况就不同了:SSH 会话断开,shell 收到 SIGHUP,正在运行测试的 agent 也会在 3 分钟后随之退出。应在一台不会进入睡眠状态的机器上运行 CLI,并将其放在终端复用器中,使其中的进程不属于 SSH 会话的子进程。这就是全部要点。关键在于 tmux,而不是安装过程。

本页介绍如何管理一台可长期运行 agent 的服务器。如果没有可以保持开机状态的 Linux 服务器,本文内容就不适用。这是唯一不可回避的前提。

tmux 实际做什么

通过 SSH 登录时,sshd 会派生一个 shell,并为其分配伪终端;你从该 shell 启动的所有进程都是它的子进程。连接断开后,内核会销毁 pty,shell 会收到 SIGHUP,并依次向其子进程发送挂断信号。以前台运行的长时间任务会退出。

tmux 反转了进程归属关系。你输入的 tmux 命令是一个轻量客户端,它通过 Unix 套接字与脱离终端运行的 tmux server 通信。会话中的 shell 是该 server 的子进程,而不是 sshd 的子进程。SSH 连接断开后,客户端会退出,但 server、会话和正在执行任务的代理仍会继续运行。重新连接后,执行 tmux attach,即可回到同一个 shell,并保留相同的滚动缓冲区。nohup 同样可以在挂断后继续运行,但无法重新进入后台运行的 TUI。你无法重新附加到它。Claude Code 是交互式程序;tmux(或 screen)才是合适的工具。

配置服务器规格

CLI 本身只是一个原生二进制文件,不会占满服务器。真正消耗服务器资源的是代理代表您运行的内容:构建、完整测试套件、tsc、语言服务器或 Docker 中的数据库。应根据工具链而不是 CLI 规划服务器规格。即使您计划完全不使用 swap,也应配置 swap;这样可以将导致进程被 OOM 强制终止的问题变成速度较慢的构建:

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

还要监控磁盘:代码仓库、node_modules 和 Docker 镜像会很快累积。如果工具链还会运行容器之外的完整虚拟机,例如 KVM 虚拟机或本地 Kubernetes 节点,请在确定方案前确认其提供 CPU 虚拟化扩展。因为在 VPS 上运行嵌套虚拟化需要由服务提供商启用,您无法在虚拟机内部自行开启。

先创建非 root 用户

创建专用用户及其主目录,并配置公钥:

sudo adduser --disabled-password --gecos "" agent
sudo install -d -m 700 -o agent -g agent /home/agent/.ssh
sudo cp ~/.ssh/authorized_keys /home/agent/.ssh/authorized_keys
sudo chown agent:agent /home/agent/.ssh/authorized_keys
sudo chmod 600 /home/agent/.ssh/authorized_keys

继续操作前,先从第二个终端测试登录。此时仍保留密码认证作为备用方式。如果返回 公钥认证被拒绝,通常是 .ssh 目录的所有者或权限模式不正确,而不是密钥本身有问题。

特意不将 agent 加入 sudo 组。如果需要系统软件包,就安装该软件包。仅这一项措施,就能消除大多数因误执行 shell 命令而破坏主机的可能性。

长期运行服务器的 SSH 安全配置

对于全天连接公网、保存 agent 和源代码的机器,继续启用密码认证不值得承担相应风险。请将其关闭。在 Ubuntu 24.04 和 Debian 13 中,/etc/ssh/sshd_config包含/etc/ssh/sshd_config.d/*.conf,因此应添加文件,而不是直接编辑主配置文件:

# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no

除非有明确原因,否则不要在该文件中配置密钥交换算法:从旧版加固指南复制的 KexAlgorithms 行通常只会固定使用经典算法,因此新版 ssh 客户端最终会输出连接未使用后量子密钥交换的警告。

验证配置并重新加载服务。测试时保持当前会话打开,并从第二个终端建立一个新会话:

sudo sshd -t && sudo systemctl restart ssh

Ubuntu 24.04 还有一点需要注意:sshd 由 socket 激活。认证设置会在systemctl restart ssh时生效,但修改监听Port还需要执行systemctl daemon-reload并重启ssh.socket。

然后配置防火墙。启用防火墙之前先允许 SSH,否则会把自己锁在服务器外:

sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable

安装fail2ban前,应明确它的作用:关闭密码认证后,暴力破解本身已经无法成功;它可以将失败的尝试排除在日志之外。

# /etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1h

最后,使用sudo apt install unattended-upgrades和sudo dpkg-reconfigure -plow unattended-upgrades自动安装补丁。请注意这与 tmux 的交互:启用Unattended-Upgrade::Automatic-Reboot后,内核更新会重启服务器,并终止所有会话。将其关闭,然后在没有任务运行到一半时,按照自己的计划重启。发行版升级也需要同样谨慎:将服务器从 Ubuntu 24.04 升级到 26.04会重启 sshd 和内核,因此应安排在没有 tmux 会话运行重要任务的维护窗口内。

在 Ubuntu 上安装 Node.js 和 Claude Code

npm 方式需要 Node.js 22 或更高版本。(该软件包现在会引入一个无法在 Node 上运行的原生二进制文件,但 npm 仍会检查 engine 版本;而且代理自身的工具链通常也需要较新的 Node 版本。)发行版软件包通常版本滞后;在 Ubuntu 和 Debian 上,NodeSource 是常用方式,并提供已签名的软件源(不使用已弃用的 apt-key;该组件已在 Ubuntu 24.04 中弃用,并已从 Debian 13 中移除):

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --version

接下来是容易出错的部分:始终以您的 agent 用户安装 CLI,绝不要使用 sudo npm -g。 由 root 所有的全局 prefix 会在之后产生权限错误,并在 npm 缓存中留下由 root 所有的文件。先将 npm 的 prefix 指向用户主目录:

mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
npm install -g @anthropic-ai/claude-code
claude --version

将该 export 写入 ~/.bashrc,并放在文件顶部 “If not running interactively, don't do anything” 判断之前。默认的 tmux 窗格会运行登录 shell:tmux 会让 default-command 保持为空,这表示以登录 shell 启动 default-shell;登录 bash 会读取 ~/.profile,而 Ubuntu 上的该文件会加载 ~/.bashrc。非登录 shell(例如您自行设置 default-command 时)会直接读取 ~/.bashrc。因此,~/.bashrc 可以覆盖这两种情况,而单独使用 ~/.profile 会遗漏第二种情况。需要注意:如果 ~/.bash_profile 或 ~/.bash_login 存在,bash 会跳过 ~/.profile;因此,如果您创建了其中一个文件,也要让它加载 ~/.bashrc。使用 nvm 等版本管理器安装每个用户独立的 Node 也能达到相同效果;无论采用哪种方式,目标都是让 npm install -g 不再需要 sudo。npm 仍然可以正常使用,但 Anthropic 的文档将原生安装脚本标记为推荐方式;签名的 apt、dnf 和 apk 软件源也是其他选择。粘贴命令前请查看 Anthropic 的安装文档,因为安装方式会变化。

在仓库目录中运行 claude 即可启动它。首次运行时会引导您完成身份验证;无头服务器没有浏览器,因此该流程会提供一个 URL,供您在自己的计算机上打开,并提供一个代码,让您带回终端。(另一种方式是在环境中设置 API key。)无论采用哪种方式,该凭据现在都保存在服务器上。接下来就是容易被忽略的部分。

爆炸半径讨论

拥有 shell 访问权限的代理就是一个 shell。它可以读取运行该代理的用户能够读取的任何内容,也可以向该用户能够推送的任何位置推送。这不是对该工具的批评,而是 shell 的定义。因此,代理运行所使用的账户比任何单独的设置都更重要。

  • 使用专用的非特权用户。 不加入 sudo 组,不与您自己的账户共享主目录。
  • 服务器上不保存生产环境凭据。 不要让 ~/.aws/credentials 存放生产环境密钥,不要从生产环境复制 .env,也不要保存对重要资源具有写权限的数据库密码。为代理提供预发布环境凭据或只读凭据。
  • 限制令牌权限。 使用仅限于一个仓库的细粒度 GitHub 令牌;如果只需要读取权限,则使用部署密钥。

Claude Code 提供 --dangerously-skip-permissions 标志,可跳过几乎所有权限提示。在笔记本电脑或一次性项目中是否使用它由您决定。但在保存令牌的服务器上,它会移除误读指令与 git push --force 之间的最后一道防线。您跳过的权限提示也不是非黑即白的;由于 自动模式现在是默认启动模式(从 Claude Code v2.1.283 开始),有必要了解不应持续监控的服务器应固定使用哪种权限模式。该标志实际改变的内容,以及如何约束使用该标志运行的代理,涵盖从内置沙箱到一次性 VPS 的做法,请参阅在服务器上安全运行 Claude Code。

部署密钥与 SSH 代理转发

将 ssh -A 以便 git 使用笔记本电脑上的密钥,这种做法很容易让人心动。请明确它授予的权限:代理转发会将本地 SSH 代理的套接字暴露给服务器上以该用户身份运行的进程。只要您保持连接,任何以 agent 身份运行的进程(包括该代理)都可以请求使用您的密钥为其能够访问的任何主机签名。这远不只是“让 git 拉取这个仓库”。

改为在服务器上生成密钥,将其注册为单个仓库的部署密钥(仅当代理需要推送时才授予写权限),并设置 git 身份,以便识别从服务器提交的提交记录:

ssh-keygen -t ed25519 -C "agent deploy key" -f ~/.ssh/id_ed25519_repo
cat ~/.ssh/id_ed25519_repo.pub   # paste into the repo's Deploy Keys
git config --global user.name "Agent (build box)"
git config --global user.email "agent@example.com"

tmux 工作流

安装 tmux(sudo apt install tmux),然后创建一个最小配置(~/.tmux.conf):

set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"

日常使用只需掌握 4 个命令:

tmux new -A -s claude     # attach to session "claude", creating it if absent
# ...run `claude` inside it, work normally...
# Ctrl-b then d           -> detach; everything keeps running
tmux ls                   # list sessions
tmux attach -t claude     # reattach, from this machine or any other
tmux kill-session -t claude

tmux new -A -s claude 是需要记住的命令。如果会话已存在,它会连接到该会话;如果会话不存在,它会创建会话。因此,无论是开始工作还是连接中断后恢复,都可以使用同一个命令。可以为它设置别名。在会话中,Ctrl-b c 可打开窗口,Ctrl-b n 和 Ctrl-b p 可在窗口之间切换,Ctrl-b [ 可进入复制模式以向上滚动查看内容(q 退出复制模式)。

有一点需要了解:不要随意终止会话。代理在每轮交互中都会重新发送完整对话。因此,在让会话持续运行一周之前,请先阅读长期运行的 Claude Code 会话会将令牌消耗在哪里。

故障模式

“我的会话不见了。” tmux ls 输出 no server running on /tmp/tmux-1000/default。这几乎总是因为进程从未运行在 tmux 中:您通过 SSH 登录后直接运行了 claude,断开连接后进程随之终止。没有可恢复的内容。避免这种问题的习惯是:每次登录后,tmux new -A -s <project> 都必须是第一个命令。

窗格缩小成一个很小的方框。 window-size 选项决定由哪个已连接客户端设置大小。从 tmux 3.1 开始(Ubuntu 24.04 提供 3.4),默认值为 latest,即最近有活动的客户端;但如果配置中包含 window-size smallest,大小就会按已连接客户端中最小的尺寸设置。因此,另一台机器上仍保持连接的旧客户端可能会压缩显示区域。连接时强制断开其他客户端:tmux attach -d -t claude。

构建过程输出 Killed。 只有一个词,没有堆栈跟踪。使用 sudo dmesg -T | grep -i -E 'out of memory|killed process' 确认,这是内核 OOM killer 选择了占用内存最多的进程。从 Node 运行时也可能看到 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory。按以下顺序处理:添加 swap(见上文);限制测试和编译器的并行度;使用 NODE_OPTIONS=--max-old-space-size=... 增大 Node 堆;或升级 VPS 配置。OOM killer 也可能选择终止 tmux server,而不是构建进程,从而同时结束您的会话;如果正在运行 systemd-oomd,它还可能终止整个用户 slice,产生相同结果。

npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'。 这是将全局软件包安装到 root 所有的前缀目录导致的。使用上文的 ~/.npm-global 前缀。如果您之前运行过 sudo npm,还可能看到 Your cache folder contains root-owned files;使用 sudo chown -R $(id -u):$(id -g) ~/.npm 修复。

claude: command not found,但只有时有时无。 您的 PATH 导出语句位于 ~/.bashrc 中“如果不是交互式 shell,则不执行任何操作”检查之后,因此非交互式 shell 会跳过它。将该导出语句移到检查之前,并保留在 ~/.bashrc 中:默认 tmux 窗格是一个登录 shell,会通过 Ubuntu 的 ~/.profile 读取 ~/.bashrc;任何非登录 shell 则会直接读取 ~/.bashrc,因此同一个文件可以覆盖两种情况。

连接后颜色显示异常。 这是 TERM 不匹配导致的;使用上面的 default-terminal 行即可修复。

重启后会话消失。 这不是错误:tmux server 是一个进程,重启会终止它。检查 uptime。

随着规模扩大,哪些环节会出问题

更多项目。 每个代码仓库对应一个 tmux 会话,并以仓库名命名;tmux ls 此时就是你的仪表盘。如果不遵守命名规范,你最终会得到 0、1、2 这样的会话。当多个会话同时运行时,它们不必相互隔离,因为 同一台机器上的一个会话可以将消息传递给另一个会话,这在处理大型重构任务时很有用:你可以让另一个会话去运行测试。端口管理也会出现同样的问题,当六个仓库都需要 :3000 端口时,就不要再手动分配了,应让 Docker Compose 下的 Traefik 反向代理 根据主机名进行流量分发。

更多人员。 tmux 套接字是按用户划分的,因此同一台机器上的两名开发人员各自拥有独立的 tmux 服务,无法看到对方的会话。通过共享套接字来共享同一个会话,意味着每个人都以同一个 Unix 用户身份在同一个 shell 中输入命令,这会带来审计和权限方面的问题。使用独立用户是枯燥但正确的做法。

无人值守的任务。 tmux 适用于你需要附加(attach)上去的交互式会话。按计划运行且无需人工监控的任务,应放入 systemd 单元和服务定时器中,这样可以免费获得日志记录、重启策略和开机自启功能。如果为了运行定时任务而使用 tmux,说明该任务应该被配置为服务。

最后一点:将代理启动的开发服务器绑定到 127.0.0.1,而不是 0.0.0.0,并通过 SSH 隧道 (ssh -L 3000:127.0.0.1:3000 agent@your-server) 访问它们,而不是在 ufw 中开放端口。当你需要转发六个端口,或者手机和笔记本都需要访问同一个预览环境时,请在它们前面部署一个 自托管的 WireGuard VPN:开发服务器绑定到私有接口,而 ufw 继续拒绝来自公网接口的所有连接。只有当你不再在防火墙上打洞时,防火墙才能发挥作用。

Claude Code 并不是唯一的选择:在 VPS 上运行编码 AI 代理 时,Aider 和 Goose 也是值得考虑的方案。

FAQ

Claude Code 在 SSH 连接断开后还会继续运行吗?

只有在 tmux 中启动时才会。直接从 SSH shell 启动的进程是该 shell 的子进程,连接断开时会随 pty 一起退出。在 tmux 中,shell 属于已分离的 tmux 服务器,因此 agent 会在任务中途继续工作,而 tmux attach 会将您带回同一个回滚缓冲区。每次登录后都将 tmux new -A -s <project> 作为第一个命令执行,问题即可解决。

应该使用 sudo npm install -g 安装 CLI 吗?

不应该。由 root 拥有的全局 prefix 会导致后续安装出现 EACCES 错误,并在 npm 缓存中留下由 root 拥有的文件。将 npm 的 prefix 设置为 ~/.npm-global(或使用 nvm 等版本管理器),以非特权的 agent 用户执行安装,并从 ~/.bashrc 将 ~/.npm-global/bin 导出到 PATH,位置应在交互式检查之前。如果您已经执行过一次 sudo npm,请使用 sudo chown -R $(id -u):$(id -g) ~/.npm 修复缓存。

在运行 agent 的主机上使用 ssh -A agent 转发安全吗?

它授予的权限远超任务所需。转发会将本地 SSH agent 的套接字暴露给以该用户身份运行的所有进程。因此,只要您保持连接,主机上的任何进程都可以请求您的密钥为其能够访问的任意主机签名。请在服务器上生成 ed25519 密钥,并将其注册为每个仓库独立的 deploy key。只有 agent 确实需要推送时,才授予写权限。

为什么我的构建只输出 Killed?

没有堆栈跟踪的单个词通常表示内核 OOM killer 终止了进程。使用 sudo dmesg -T | grep -i -E 'out of memory|killed process' 确认;从 Node 中可能会看到 JavaScript heap out of memory。请按顺序处理:添加 swapfile,限制测试和编译器的并行度,提高 NODE_OPTIONS=--max-old-space-size=...,最后升级 VPS 规格。请注意,OOM killer 可能终止 tmux 服务器而不是构建进程,导致整个会话随之断开。

应该使用 tmux 还是 systemd 服务?

tmux 适合需要连接、查看输出并输入命令的交互式会话,这正是 agent 会话的使用方式。按计划运行且无人监控的任务应使用 systemd 单元和计时器;日志记录、重启策略和重启后继续运行等功能可直接获得。如果您需要使用 tmux 运行类似 cron 的任务,那么该任务更适合配置为服务。