Ejecutar Claude Code en un VPS remoto con tmux
Mantén sesiones de Claude Code activas tras perder SSH: ejecútalas en un VPS Linux dentro de tmux y evita que SIGHUP detenga el agente.
El problema es la tapa del portátil, no la CLI
Claude Code funciona correctamente en el portátil hasta que lo cierra: la sesión SSH termina, el shell recibe un SIGHUP y el agente se detiene tres minutos después de iniciar una prueba. Ejecute la CLI en una máquina que nunca entre en suspensión, dentro de un multiplexor de terminal cuyos procesos no sean hijos de la sesión SSH. Ese es todo el truco. La parte esencial es tmux, no la instalación.
Esta página trata sobre cómo administrar un equipo en el que deja agentes en ejecución. Si no tiene un servidor Linux que pueda dejar encendido, nada de esto se aplica. Ese es el único requisito previo real.
Qué hace realmente tmux
Cuando inicia una sesión SSH, sshd bifurca un shell y le asigna una pseudo-terminal; todo lo que inicia desde ese shell es un proceso hijo suyo. Si se pierde la conexión, el kernel elimina la pty, el shell recibe SIGHUP y, a su vez, envía la señal de cierre a sus procesos hijos. Los procesos de larga duración que están en primer plano terminan.
tmux invierte la relación de propiedad. El comando tmux que escribe es un cliente ligero que se comunica mediante un socket Unix con un servidor tmux que se ejecuta separado de su terminal. Los shells dentro de una sesión son hijos de ese servidor, no de sshd. Si se cierra la conexión SSH, el cliente termina, pero el servidor, la sesión y el agente que está ejecutando una tarea continúan activos. Vuelva a conectarse, ejecute tmux attach y regresará al mismo shell con el mismo historial desplazable. nohup también sobrevive a una desconexión, pero no permite volver a entrar: no puede volver a conectarse a una TUI que se ejecuta en segundo plano. Claude Code es interactivo; tmux (o screen) es la herramienta adecuada.
Ajuste de recursos del servidor
La CLI es un proceso de Node; no es lo que llena el servidor. Lo que ocupa el servidor es lo que el agente ejecuta en su nombre: una compilación, una suite de pruebas completa, tsc, un servidor de lenguaje o una base de datos en Docker. Dimensione el servidor para la cadena de herramientas, no para la CLI. Añada swap aunque no prevea utilizarla nunca: convierte un cierre forzado por falta de memoria en una compilación lenta:
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/fstabVigile también el disco: los repositorios, node_modules y las imágenes de Docker se acumulan rápidamente. Si la cadena de herramientas llega más allá de los contenedores y utiliza máquinas virtuales completas, un invitado de KVM o un nodo local de Kubernetes, compruebe antes de contratar el plan que expone las extensiones de virtualización de la CPU, ya que ejecutar virtualización anidada en un VPS es algo que habilita el proveedor, no algo que pueda activar desde dentro del invitado.
Primero, un usuario que no sea root
Cree un usuario dedicado con su propio directorio de inicio y coloque allí su clave pública:
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_keysPruebe el inicio de sesión desde un segundo terminal antes de continuar, mientras la autenticación mediante contraseña todavía está disponible como alternativa. Si aparece Permiso denegado (publickey), el problema suele estar en el propietario o los permisos de ese directorio .ssh, no en la clave.
De forma intencionada, agent no pertenece al grupo sudo. Si necesita un paquete del sistema, instálelo. Esta decisión elimina la mayoría de las formas en que un comando de shell ejecutado por error puede dañar el host.
Higiene de SSH para un servidor que permanece encendido
La autenticación mediante contraseña en una máquina expuesta todo el día a Internet pública, que contiene un agente y el código fuente, introduce un riesgo innecesario. Desactívela. En Ubuntu 24.04 y Debian 13, /etc/ssh/sshd_config incluye /etc/ssh/sshd_config.d/*.conf, así que cree un archivo independiente en lugar de editar la configuración principal:
# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin noValide la configuración y vuelva a cargar el servicio. Mantenga abierta la sesión actual mientras prueba una sesión nueva desde un segundo terminal:
sudo sshd -t && sudo systemctl restart sshHay un detalle específico de Ubuntu 24.04: sshd se activa mediante sockets. La configuración de autenticación se aplica en systemctl restart ssh, pero un cambio en el Port de escucha también requiere systemctl daemon-reload y reiniciar ssh.socket.
Después, configure el firewall. Permita SSH antes de activarlo para no bloquearse el acceso:
sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enableInstale fail2ban teniendo claro qué aporta: cuando la autenticación mediante contraseña está desactivada, un ataque de fuerza bruta no puede tener éxito de todos modos. Esta medida mantiene los intentos fallidos fuera del journal.
# /etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1hPor último, aplique las actualizaciones automáticamente con sudo apt install unattended-upgrades y sudo dpkg-reconfigure -plow unattended-upgrades. Tenga en cuenta la interacción con tmux: active Unattended-Upgrade::Automatic-Reboot y una actualización del kernel reiniciará el servidor, lo que cerrará todas las sesiones. Déjelo desactivado y reinicie según su propio horario, cuando no haya ninguna tarea en ejecución. La misma precaución se aplica a una actualización de versión: mover el servidor de Ubuntu 24.04 a 26.04 reinicia sshd y el kernel, por lo que debe programarse en una ventana en la que ninguna sesión de tmux mantenga trabajo importante.
Instalar Node.js y Claude Code en Ubuntu
Claude Code es una CLI de Node, por lo que necesita una versión actual de Node. El paquete de la distribución suele estar desactualizado. En Ubuntu y Debian, NodeSource es la opción habitual y proporciona un repositorio firmado (sin apt-key, esa herramienta ya no existe):
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --versionAhora viene el punto que suele causar problemas: instale la CLI como el usuario agent, nunca con sudo npm -g. Un prefijo global propiedad de root provoca errores de permisos más adelante y deja archivos propiedad de root en la caché de npm. Configure primero el prefijo de npm en el directorio personal del usuario:
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 --versionLa exportación va en ~/.bashrc, no en ~/.profile, y debe estar antes de la guarda «If not running interactively, don't do anything», cerca del principio del archivo: tmux puede iniciar shells que no sean de inicio de sesión, que leen ~/.bashrc y omiten ~/.profile; ~/.profile sólo se ejecuta en shells de inicio de sesión. Un Node por usuario mediante un gestor de versiones como nvm consigue lo mismo. En ambos casos, el objetivo es que npm install -g nunca necesite sudo. npm sigue funcionando correctamente. También puede usar el script de instalación nativo de Anthropic, que es el método predeterminado documentado actualmente. Consulte la documentación de instalación de Anthropic antes de pegar comandos, porque los métodos de instalación cambian.
Ejecute claude dentro de un repositorio para iniciarlo. La primera ejecución le guía por el proceso de autenticación. Un servidor sin interfaz gráfica no tiene navegador, por lo que el proceso proporciona una URL para abrirla en su propio equipo y un código que debe introducir de nuevo en el terminal. Una clave de API definida en el entorno es la otra opción. En ambos casos, esa credencial queda almacenada en el servidor. Esto nos lleva a la parte que suele omitirse.
El alcance del impacto
Un agente con acceso al shell tiene acceso al shell. Puede leer todo lo que pueda leer el usuario con el que se ejecuta y enviar cambios a cualquier destino al que ese usuario pueda enviarlos. Esto no es una crítica de la herramienta. Es su definición. Por eso, la cuenta con la que se ejecuta importa más que cualquier ajuste individual.
- Usuario dedicado sin privilegios. Sin el grupo
sudoni un directorio personal compartido con su propia cuenta. - Sin credenciales de producción en el equipo. No use
~/.aws/credentialspara almacenar claves de producción, no copie.envdesde producción y no use una contraseña de base de datos con acceso de escritura a nada importante. Proporcione al agente una credencial de staging o de sólo lectura. - Tokens con permisos limitados. Un token de GitHub con permisos específicos limitado a un repositorio; una deploy key cuando sea suficiente el acceso de lectura.
Claude Code incluye un flag que omite por completo sus avisos de permisos. En un portátil o en un proyecto desechable, la decisión es suya. En un servidor que contiene tokens, elimina la última barrera entre una instrucción interpretada incorrectamente y un git push --force. Además, los avisos que se omiten no funcionan como un todo o nada. Con el modo automático como nuevo valor predeterminado, conviene saber qué modo de permisos debe fijarse en un servidor que no está supervisando. En ejecutar Claude Code de forma segura en un servidor se explica qué cambia realmente el flag y cómo contener un agente que se ejecuta con él, desde el sandbox integrado hasta un VPS desechable.
Deploy key frente a SSH agent forwarding
Es tentador ssh -A para que git pueda usar la clave de su portátil. Debe entender qué permisos concede esta opción: el agent forwarding expone el socket de su agente SSH local a los procesos que se ejecutan con ese usuario en el equipo. Cualquier proceso que se ejecute como agent, incluido el agente, puede pedir a su clave que firme para cualquier host al que pueda acceder, mientras usted mantenga la conexión. Esto es mucho más que «permitir que git haga pull de este repositorio».
Genere una clave en el servidor. Después, regístrela como deploy key para un solo repositorio. Conceda acceso de escritura sólo si el agente necesita hacer push. Configure también una identidad de git para que los commits realizados desde el equipo sean reconocibles:
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"El flujo de trabajo con tmux
Instálelo (sudo apt install tmux) y cree una configuración mínima de ~/.tmux.conf:
set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"Cuatro comandos cubren el uso diario:
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 es el comando que debe memorizar. Se conecta si la sesión existe y la crea si no existe. Así, un solo comando sirve tanto para iniciar la jornada como para reanudar el trabajo después de una desconexión. Cree un alias. Dentro de una sesión, Ctrl-b c abre una ventana, Ctrl-b n y Ctrl-b p permiten recorrerlas, y Ctrl-b [ entra en el modo de copia para desplazarse hacia atrás (q permite salir).
Debe tener en cuenta un aspecto de las sesiones que nunca termina: el agente vuelve a enviar toda la conversación en cada turno. Por eso, consulte en qué consume sus tokens una sesión de Claude Code de larga duración antes de dejar una sesión en ejecución durante una semana.
Modos de fallo
«Mi sesión ha desaparecido». tmux ls muestra no server running on /tmp/tmux-1000/default. Casi siempre significa que el proceso nunca se ejecutó dentro de tmux: se conectó por SSH, ejecutó claude directamente y la desconexión terminó el proceso. No hay nada que recuperar. Para evitarlo, ejecute tmux new -A -s <project> como primer comando después de cada inicio de sesión.
El panel se reduce a un cuadro diminuto. tmux ajusta el tamaño de la sesión al cliente conectado más pequeño. Por eso, un cliente antiguo que siga conectado desde otra máquina reduce el área de visualización. Fuerce la desconexión de los demás clientes al conectarse: tmux attach -d -t claude.
Una compilación muestra Killed. Sólo aparece una palabra y no hay traza de pila. Confírmelo con sudo dmesg -T | grep -i -E 'out of memory|killed process': el kernel OOM killer ha seleccionado el proceso más grande. Desde Node también puede aparecer FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. Aplique estas correcciones, en este orden: añada swap (arriba), limite el paralelismo de las pruebas y del compilador, aumente el heap de Node con NODE_OPTIONS=--max-old-space-size=... o amplíe el tamaño del VPS. El OOM killer también puede seleccionar el servidor tmux en lugar de la compilación y cerrar la sesión; si systemd-oomd está en ejecución, puede terminar todo un slice de usuario con el mismo resultado.
npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. Se ha realizado una instalación global en un prefijo propiedad de root. Use el prefijo ~/.npm-global indicado arriba. Si ya ejecutó sudo npm en algún momento, también puede aparecer Your cache folder contains root-owned files. Repárelo con sudo chown -R $(id -u):$(id -g) ~/.npm.
claude: command not found, pero sólo a veces. La exportación PATH está en ~/.bashrc, debajo de la protección «If not running interactively, don't do anything», por lo que los shells no interactivos la omiten. Mueva la exportación por encima de esa protección y manténgala en ~/.bashrc, no en ~/.profile: tmux puede iniciar shells que no sean de inicio de sesión. Estos leen ~/.bashrc y nunca procesan ~/.profile.
Colores distorsionados después de conectarse. Se trata de una discrepancia de TERM. La línea default-terminal indicada arriba lo corrige.
Las sesiones desaparecen después de reiniciar. No es un error: el servidor tmux es un proceso y el reinicio lo termina. Compruebe uptime.
Qué se rompe al crecer
Más proyectos. Use una sesión de tmux por repositorio y asígnele el nombre del repositorio; tmux ls se convierte en su panel de control. Si no mantiene una disciplina de nombres, acabará con las sesiones 0, 1 y 2. Cuando hay varias sesiones activas, no tienen por qué funcionar de forma aislada, porque una sesión puede enviar un mensaje a otra en el mismo equipo, lo que resulta útil cuando el agente que ejecuta una refactorización larga quiere que otro ejecute las pruebas. Los puertos se multiplican de la misma forma. Si seis repositorios necesitan :3000, es el momento de dejar de asignarlos manualmente y permitir que un proxy inverso de Traefik encamine varias aplicaciones bajo Docker Compose según el nombre de host.
Más personas. Los sockets de tmux son específicos de cada usuario. Por tanto, dos desarrolladores en el mismo equipo tienen cada uno su propio servidor de tmux y no pueden ver las sesiones del otro. Compartir una sesión mediante un socket compartido significa que todos escriben en el mismo shell como el mismo usuario de Unix, con las consecuencias correspondientes para la auditoría y los permisos. Usar usuarios separados es la opción sencilla y correcta.
Trabajo desatendido. tmux está pensado para sesiones interactivas a las que se conecta. Los trabajos programados que se ejecutan sin supervisión deben configurarse en una unidad y un temporizador de systemd. Así obtienen registro, una política de reinicio y supervivencia al arranque sin configuración adicional. Recurrir a tmux para ejecutar un trabajo con características de cron indica que ese trabajo debería ser un servicio.
Una última observación: vincule los servidores de desarrollo que inicia el agente a 127.0.0.1, no a 0.0.0.0, y acceda a ellos mediante un túnel SSH (ssh -L 3000:127.0.0.1:3000 agent@your-server) en lugar de abrir puertos en ufw. Cuando reenvíe media docena de puertos, o un teléfono y un portátil necesiten acceder a la misma vista previa, coloque delante una VPN WireGuard autogestionada en el VPS: los servidores de desarrollo se vinculan a una interfaz privada y ufw sigue denegando todas las conexiones procedentes de la interfaz pública. El firewall sólo resulta útil si deja de abrirle agujeros.
Claude Code no es la única opción: ejecutar un agente de IA para programación en un VPS también permite comparar Aider y Goose.
FAQ
¿Claude Code sigue ejecutándose si se interrumpe la conexión SSH?
Sólo si lo inició dentro de tmux. Un proceso iniciado directamente desde el shell SSH es hijo de ese shell y muere con el pty cuando se interrumpe la conexión. Dentro de tmux, el shell pertenece al servidor tmux separado, por lo que el agente sigue trabajando en la tarea actual y tmux attach le devuelve a la misma salida desplazable. Haga que tmux new -A -s <project> sea el primer comando después de cada inicio de sesión y el problema desaparecerá.
¿Debo instalar la CLI con sudo npm install -g?
No. Un prefijo global propiedad de root provoca errores EACCES en instalaciones posteriores y deja archivos propiedad de root en la caché de npm. Establezca el prefijo de npm en ~/.npm-global (o use un gestor de versiones como nvm), instale como el usuario sin privilegios agent y exporte ~/.npm-global/bin a PATH desde ~/.bashrc, antes de la condición para sesiones interactivas. Si ya ejecutó sudo npm una vez, repare la caché con sudo chown -R $(id -u):$(id -g) ~/.npm.
¿Es seguro ssh -A el reenvío del agente en un equipo que ejecuta un agente?
Concede mucho más acceso del que necesita la tarea. El reenvío expone el socket del agente SSH local a todos los procesos que se ejecutan con ese usuario. Por tanto, cualquier proceso del equipo puede pedir a su clave que firme para cualquier host al que pueda conectarse mientras usted mantenga la sesión. Genere una clave ed25519 en el servidor y regístrela como clave de despliegue para cada repositorio, con acceso de escritura sólo si el agente realmente tiene que hacer push.
¿Por qué mi compilación sólo muestra Killed?
Una sola palabra sin traza de pila indica que intervino el terminador OOM del kernel. Confírmelo con sudo dmesg -T | grep -i -E 'out of memory|killed process'; desde Node puede aparecer JavaScript heap out of memory en su lugar. Aplique las correcciones en este orden: añada un archivo de intercambio, limite el paralelismo de las pruebas y del compilador, aumente NODE_OPTIONS=--max-old-space-size=... y amplíe el VPS. Tenga en cuenta que el terminador OOM puede elegir el servidor tmux en lugar de la compilación y cerrar toda la sesión.
¿tmux o un servicio de systemd?
tmux es adecuado para sesiones interactivas a las que se conecta, que supervisa y en las que escribe. Eso es exactamente una sesión de agente. El trabajo programado que nadie supervisa debe ejecutarse en una unidad y un temporizador de systemd, que proporcionan registro, una política de reinicio y continuidad tras el arranque. Si está usando tmux para ejecutar un trabajo con características de cron, ese trabajo debería ser un servicio.