如何在远程 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 是一个 Node 进程,不会占满整台机器。真正消耗资源的是代理代表您运行的任务,例如构建、完整测试套件、tsc、语言服务器或 Docker 中的数据库。应根据工具链而不是 CLI 规划服务器规格。即使计划永远不使用 swap,也应配置它。这样可以将直接触发的 OOM kill 变成缓慢的构建:
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验证配置并重新加载服务。测试时保持当前会话打开,并从第二个终端建立一个新会话:
sudo sshd -t && sudo systemctl restart sshUbuntu 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
Claude Code 是一个 Node CLI,因此需要当前版本的 Node。Ubuntu 和 Debian 的发行版软件包通常滞后;在 Ubuntu 和 Debian 上,通常使用 NodeSource。它提供带签名的软件源(不需要 apt-key,该工具已经移除):
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,而不是 ~/.profile;并且必须放在文件顶部 “If not running interactively, don't do anything” 保护条件的上方:tmux 可能启动非登录 shell,这类 shell 会读取 ~/.bashrc,并跳过 ~/.profile;~/.profile 只对登录 shell 生效。使用 nvm 等版本管理器安装每用户 Node 也能达到相同效果;无论采用哪种方式,目标都是让 npm install -g 不再需要 sudo。npm 仍然可以正常使用,也可以使用 Anthropic 的原生安装脚本,这是当前文档中的默认方式。粘贴命令前,请查看 Anthropic 的安装文档,因为安装方式会发生变化。
在仓库中运行 claude 以启动它。首次运行时,程序会引导您完成身份验证;无头服务器没有浏览器,因此流程会提供一个 URL,供您在自己的计算机上打开,并提供一个代码,供您带回终端。(另一种方式是在环境中设置 API key。)无论采用哪种方式,该凭据现在都会保存在服务器上。下面进入通常会被跳过的部分。
爆炸半径的讨论
拥有 shell 访问权限的代理就是一个 shell。它可以读取运行该代理的用户能够读取的任何内容,也可以向该用户能够推送的位置推送内容。这不是对工具的批评,而是 shell 的定义。因此,代理运行所使用的账户比任何单独的设置都更重要。
- 专用的非特权用户。 不加入
sudo组,也不与您自己的账户共享主目录。 - 服务器上不得存放生产凭据。 不要让
~/.aws/credentials保存生产密钥,不要从生产环境复制.env,也不要配置对重要数据具有写入权限的数据库密码。为代理提供预发布环境凭据或只读凭据。 - 限定作用域的令牌。 使用仅限一个仓库的细粒度 GitHub 令牌;如果只需要读取权限,则使用部署密钥。
Claude Code 提供了一个可完全跳过权限提示的标志。在笔记本电脑或一次性项目中,是否使用它由您决定。在存放令牌的服务器上,它会移除误读指令与 git push --force 之间最后一道防线。您跳过的权限提示也不是非黑即白的选项;随着自动模式成为新的默认模式,了解未受您监控的服务器应固定使用哪种权限模式很有必要。该标志实际会改变什么,以及如何限制使用该标志运行的代理,相关内容涵盖从内置沙箱到一次性 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 claudetmux 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>。
窗格缩成一个很小的框。 tmux 会将会话大小调整为所有已连接客户端中的最小尺寸,因此另一台机器上仍处于连接状态的旧客户端会压缩显示区域。连接时强制断开其他客户端: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 拥有的 prefix 中。使用上文的 ~/.npm-global prefix。如果您之前运行过 sudo npm,还可能看到 Your cache folder contains root-owned files;使用 sudo chown -R $(id -u):$(id -g) ~/.npm 修复。
claude: command not found,但仅偶尔发生。 您的 PATH export 位于 ~/.bashrc 中“如果不是交互式运行,则不执行任何操作”这一判断之后,因此非交互式 shell 会跳过它。将 export 移到该判断之前,并将其保留在 ~/.bashrc 中,而不是 ~/.profile:tmux 可能启动非登录 shell;这类 shell 会读取 ~/.bashrc,不会读取 ~/.profile。
连接后颜色显示异常。 这是 TERM 不匹配导致的;上面的 default-terminal 行可以修复此问题。
重启后会话消失。 这不是错误:tmux server 是一个进程,重启会终止它。检查 uptime。
扩展后会出现什么问题
项目增多。 每个仓库使用一个 tmux 会话,并以仓库名命名;这样 tmux ls 就是你的控制面板。如果不遵守命名规范,最终会得到 0、1、2 这样的会话。多个会话同时运行后,它们不必彼此隔离,因为一个会话可以向同一台主机上的另一个会话传递消息。例如,执行长时间重构的 agent 可以让另一个 agent 运行测试。端口也会以同样的方式不断增加:当 6 个仓库都需要 :3000 时,就不要再手动分配端口,而应让Traefik 反向代理在 Docker Compose 下按主机名路由多个应用。
用户增多。 tmux 套接字按用户隔离,因此同一台主机上的两个开发者各自拥有自己的 tmux server,也看不到对方的会话。通过共享套接字共享一个会话,意味着所有人都以同一个 Unix 用户身份在同一个 shell 中输入命令,并承担由此产生的审计和权限后果。使用不同的用户才是简单且正确的方案。
无人值守的任务。 tmux 用于连接到交互式会话。按计划运行且无人监看的任务应放入 systemd unit 和 timer 中,这样可以直接获得日志记录、重启策略和重启后自动恢复。使用 tmux 运行类似 cron 的任务,通常说明该任务应作为服务运行。
最后需要注意:将 agent 启动的开发服务器绑定到 127.0.0.1,而不是 0.0.0.0,并通过 SSH 隧道(ssh -L 3000:127.0.0.1:3000 agent@your-server)访问它们,不要在 ufw 中开放端口。当需要转发六七个端口,或者手机和笔记本都需要访问同一个预览环境时,应改为在它们前面部署VPS 上自托管的 WireGuard VPN:开发服务器绑定到私有接口,ufw 继续拒绝来自公网接口的所有连接。只有停止不断在防火墙上开例外,防火墙才有作用。
Claude Code 不是唯一选择:在 VPS 上运行编码 AI agent时,也可以考虑 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 的任务,那么该任务更适合配置为服务。