SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor

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.

Dimensionamiento del servidor

La CLI es un único binario nativo; no es lo que llena el equipo. Lo que ocupa el equipo es todo lo que el agente ejecuta en su nombre: una compilación, un conjunto completo de pruebas, tsc, un servidor de lenguaje o una base de datos en Docker. Dimensione el sistema para la cadena de herramientas, no para la CLI. Añada espacio de intercambio aunque no piense utilizarlo nunca: convierte una terminación forzada por falta de memoria (OOM) 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/fstab

Vigile también el disco: los repositorios, node_modules y las imágenes de Docker se acumulan rápidamente. Si la cadena de herramientas incluye máquinas virtuales completas además de contenedores, por ejemplo un invitado KVM o un nodo local de Kubernetes, compruebe que el plan exponga las extensiones de virtualización de la CPU antes de contratarlo, ya que ejecutar virtualización anidada en un VPS es algo que debe habilitar el proveedor, no algo que pueda activar desde el 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_keys

Pruebe 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 no

No incluya el intercambio de claves en ese archivo salvo que tenga un motivo específico: una línea KexAlgorithms copiada de una guía antigua de endurecimiento suele fijar únicamente algoritmos clásicos, por lo que un cliente ssh reciente termina mostrando la advertencia de que la conexión no utiliza un intercambio de claves poscuántico.

Valide 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 ssh

Hay 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 enable

Instale 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 = 1h

Por ú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

La ruta de npm necesita Node.js 22 o posterior. (Ahora el paquete incorpora un binario nativo que no se ejecuta en Node, pero npm sigue comprobando la versión del motor y la propia cadena de herramientas del agente normalmente también necesita una versión actual de Node). El paquete de la distribución suele estar desactualizado; NodeSource es la ruta habitual en Ubuntu y Debian, y proporciona un repositorio firmado (sin apt-key, que está obsoleto en Ubuntu 24.04 y ya no existe en Debian 13):

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

Ahora viene el punto que suele hacerse mal: instale la CLI como 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 --version

La exportación va en ~/.bashrc y debe estar por encima de la protección "If not running interactively, don't do anything" situada cerca del principio del archivo. Un panel predeterminado de tmux ejecuta un shell de inicio de sesión: tmux deja default-command vacío, lo que le indica que inicie default-shell como shell de inicio de sesión, y bash de inicio de sesión lee ~/.profile, que en Ubuntu obtiene el contenido de ~/.bashrc. Los shells que no son de inicio de sesión, por ejemplo, si configura default-command manualmente, leen ~/.bashrc directamente. Por tanto, ~/.bashrc cubre ambos casos, mientras que ~/.profile por sí solo no cubre el segundo. Hay una excepción: bash omite ~/.profile si existe ~/.bash_profile o ~/.bash_login, así que, si ha creado uno, haga que también obtenga ~/.bashrc. 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, pero la documentación de Anthropic marca el script de instalación nativo como la ruta recomendada, y los repositorios apt, dnf y apk firmados son otra opción. Consulte la documentación de instalación de Anthropic antes de pegar comandos; los métodos de instalación cambian.

Ejecute claude dentro de un repositorio para iniciarlo. La primera ejecución le guía por la autenticación; un equipo sin interfaz gráfica no tiene navegador, por lo que el proceso le proporciona una URL para abrirla en su propio equipo y un código para devolverlo al terminal. (Una clave de API en el entorno es la otra opción). En ambos casos, esa credencial queda ahora en el servidor, lo que nos lleva a la parte que se suele omitir.

El alcance de un incidente

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 hacer push en cualquier ubicación donde ese usuario pueda hacerlo. Esto no es una crítica de la herramienta, sino su definición. Por eso, la cuenta con la que se ejecuta importa más que cualquier ajuste individual.

  • Usuario dedicado y sin privilegios. Sin el grupo sudo ni un directorio personal compartido con su propia cuenta.
  • Sin credenciales de producción en el equipo. No debe haber ningún ~/.aws/credentials que contenga claves de producción, ningún .env copiado desde producción ni ninguna contraseña de base de datos con acceso de escritura a recursos importantes. Proporcione al agente una credencial de staging o de sólo lectura.
  • Tokens con alcance limitado. 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 el indicador --dangerously-skip-permissions, que omite casi todas sus solicitudes 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 mal interpretada y un git push --force. Además, las solicitudes que omitiría no son completamente binarias. Con el modo automático como modo inicial predeterminado (desde Claude Code v2.1.283), 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 indicador y cómo aislar un agente que se ejecuta con él, desde el sandbox integrado hasta una VPS desechable.

Deploy key frente a reenvío del agente SSH

Puede resultar tentador ssh -A para que git use la clave de su portátil. Debe entender qué concede esto: el reenvío del agente expone el socket de su agente SSH local a los procesos que se ejecutan como 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, regístrela como una deploy key específica del repositorio (con acceso de escritura sólo si el agente necesita hacer push) y configure una identidad de git para que los commits realizados desde el equipo sean identificables:

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 claude

tmux 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. El hábito que evita este problema es ejecutar tmux new -A -s <project> como primer comando después de cada inicio de sesión.

El panel se reduce a un cuadro diminuto. La opción window-size determina qué cliente conectado establece el tamaño. Desde tmux 3.1 (Ubuntu 24.04 incluye la versión 3.4), el valor predeterminado es latest, es decir, el cliente con actividad más reciente. Sin embargo, una configuración que contenga window-size smallest usa el tamaño del cliente conectado más pequeño. Por eso, un cliente antiguo que siga conectado desde otra máquina puede reducir el área de visualización. Desconecte los demás clientes al conectarse: tmux attach -d -t claude.

Una compilación muestra Killed. Sólo aparece una palabra, sin traza de la pila. Confírmelo con sudo dmesg -T | grep -i -E 'out of memory|killed process': el kernel OOM killer seleccionó el proceso más grande. En Node también puede aparecer FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. Soluciones, en este orden: añada swap (como se indicó 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 de la VPS. El OOM killer también puede seleccionar el servidor de tmux en lugar de la compilación y cerrar la sesión. Si systemd-oomd está activo, puede terminar un segmento de usuario completo con el mismo resultado.

npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. Se trata de 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 ver 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: un panel de tmux predeterminado es un shell de inicio de sesión que llega a ~/.bashrc mediante ~/.profile de Ubuntu, y cualquier shell que no sea de inicio de sesión lee ~/.bashrc directamente. Por tanto, un solo archivo cubre ambos casos.

Colores distorsionados después de conectarse. Se trata de una discrepancia de TERM. La línea default-terminal anterior es la solución.

Las sesiones desaparecen después de reiniciar. No es un error: el servidor de tmux es un proceso y un 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.