Incidente: Claude Code roto en tmux-host por el auto-update en caliente (2026-10-17)¶
| Ventana del incidente | Del 2026-10-17 17:37Z, cuando se reinició tmux-host, a las 18:18Z, cuando el swap a la imagen corregida quedó verificado. El fallo se gestó en los días previos, sin que nadie lo notara. |
| Servicio afectado | Contenedor tmux-host: las sesiones scm-portal (client-initiatives) y cloud-practice de Claude Code y su Remote Control (ver Sesión tmux persistente) |
| Impacto | Tras el reinicio, healthcheck.sh recreó las sesiones y lanzó claude, pero el binario estaba roto: claude terminaba al instante y las sesiones quedaban como un shell vacío. Las sesiones remotas no aparecían en claude.ai |
| Estado | Resuelto y verificado. Imagen tmux-host:2026-10-17-claude-fija en producción |
Fuentes de la evidencia
Los datos marcados (verificado) se confirmaron contra el sistema real:
/root/.npm/_logs/, docker inspect, health.log, tmux capture-pane,
ps y un build de prueba de la imagen. Los marcados (reportado)
vienen del relato del usuario.
Síntoma¶
/usr/local/bin/claudeapuntaba a un binario roto (reportado). El usuario lo reparó a mano connpm uninstall -g+npm install -g @anthropic-ai/claude-code, que dejó la versión 2.1.302 (verificado).scm-portalycloud-practicemostraban solo el prompt del shell, con la líneaclaude --remote-control --continueya ejecutada y sin ningún procesoclaudevivo (verificado).health.logdecía "lanzado" porquetmux send-keysfuncionó, aunqueclaudemurió enseguida.
Hipótesis descartada: "se instala en cada arranque"¶
La primera sospecha fue que entrypoint.sh reinstalaba Claude Code en cada
arranque del contenedor, y que un corte de red en ese momento dejaba la
instalación a medias. Falso (verificado): entrypoint.sh solo lanza
healthcheck.sh supervisado y tail -f /dev/null. Claude Code se instala
una sola vez, en el RUN npm install -g del Dockerfile, al construir
la imagen.
Causa raíz: el auto-updater de Claude Code¶
Claude Code trae un auto-updater activo por defecto ("autoUpdates": true
en /root/.claude.json). Cuando encuentra una versión nueva, ejecuta
npm install -g dentro del contenedor ya corriendo, sobre la misma
instalación global (/usr/local/lib/node_modules/@anthropic-ai/claude-code)
que usan las 3 sesiones a la vez.
Evidencia en /root/.npm/_logs/ (verificado):
| Hora (UTC) | Qué pasó |
|---|---|
| 2026-10-14 01:36:01–03 | Tres npm view @anthropic-ai/claude-code@latest en el mismo segundo, uno por sesión |
| 2026-10-14 01:36:02 | Una sesión corrió npm install --global @anthropic-ai/[email protected]. npm movió el paquete a .claude-code-1devilah y el symlink a .claude-X3ObDFPy mientras las otras sesiones seguían usando esos archivos |
| 2026-10-17 17:55 | Reinstalación manual del usuario (uninstall + install) a 2.1.302 |
Dos actualizaciones que se pisan, o un corte del contenedor a mitad de
una, dejan el symlink apuntando a un binario inexistente o incompleto.
Nadie se entera hasta que una sesión intenta lanzar claude, en este
caso tras el reinicio de las 17:37.
npm conserva solo los últimos 10 logs, así que el de la corrida exacta que rompió el symlink ya no existe. El mecanismo sí quedó demostrado.
Agravante: versiones que no coinciden con la imagen¶
Todo lo que instala el auto-updater queda en la capa escribible del contenedor, no en la imagen (verificado):
| Dónde | Versión |
|---|---|
Imagen tmux-host:2026-09-28-scm |
2.1.283 |
| Contenedor tras el auto-update del 14/10 | 2.1.296 |
| Contenedor tras la reinstalación manual | 2.1.302 |
Un docker restart conserva esa capa. Un recreate o swap la descarta y
vuelve en silencio a la versión de la imagen, que puede ser más vieja.
Solución aplicada¶
1. Imagen con versión fija y sin auto-update (scripts/tmux-host/Dockerfile, commit 63ce961):
ARG CLAUDE_CODE_VERSION=2.1.302
ENV DISABLE_AUTOUPDATER=1
RUN apk add ... && npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" @google/clasp ...
# El build falla si el symlink no apunta a un ejecutable real
# o si el binario no reporta la versión pedida.
RUN test -x "$(readlink -f /usr/local/bin/claude)" \
&& claude --version | grep -q "^${CLAUDE_CODE_VERSION} "
Prueba negativa del chequeo, sobre la imagen construida (verificado):
| Caso | Resultado |
|---|---|
| Instalación sana | pasa |
| Binario borrado (symlink roto) | falla |
| Versión distinta a la pedida | falla |
2. Detección en vivo (scripts/tmux-host/healthcheck.sh): en cada
ciclo de 30 minutos corre check_claude_install. Si claude --version
falla, deja CLAUDE: ERROR en health.log con el destino del symlink y
avisa por Telegram.
3. Swap a tmux-host:2026-10-17-claude-fija, con el helper
docker:cli y el script swap-claude-fija.sh (ver el gotcha del swap en
Sesión tmux persistente).
Verificación post-swap (2026-10-17 18:18Z)¶
| Chequeo | Resultado |
|---|---|
docker ps |
tmux-host:2026-10-17-claude-fija, mismos 5 volúmenes que antes |
claude --version |
2.1.302 (Claude Code) |
echo $DISABLE_AUTOUPDATER |
1 |
health.log |
CLAUDE: ok (2.1.302 (Claude Code)) y las 3 sesiones relanzadas |
| Procesos | 3 procesos claude --remote-control --continue vivos |
/root/.claude.json |
JSON válido |
Gotcha secundario: aviso de .claude.json corrupto al relanzar¶
Al relanzar cloud-practice a mano mientras otra sesión arrancaba,
Claude mostró:
Claude configuration file at /root/.claude.json is corrupted: JSON Parse error: Unexpected EOF
1. Exit and fix manually
2. Reset with default configuration
El archivo no estaba corrupto: era JSON válido un segundo después
(verificado). Fue una carrera. Las sesiones comparten /root/.claude.json,
y una lo leyó justo mientras otra lo estaba escribiendo.
Nunca elegir la opción 2 (Reset)
"Reset with default configuration" borra el login, la confianza de los folders y la configuración de MCP de las 3 sesiones, porque el archivo es compartido. Elegí 1 (Exit), comprobá que el archivo sea válido y relanzá:
node -e 'JSON.parse(require("fs").readFileSync("/root/.claude.json","utf8"));console.log("ok")'
claude --remote-control --continue
Para evitarlo, relanzá las sesiones de a una, esperando a que cada una muestre su prompt antes de lanzar la siguiente.
Cómo evitar que se repita¶
- Nunca actualizar Claude Code en caliente dentro de
tmux-host. Eso incluyenpm install -gonpm update, y aceptar el aviso "Update installed · Restart to apply". Ese aviso no debería volver a aparecer. Si aparece, algún proceso no heredóDISABLE_AUTOUPDATER=1: revisalo. - Actualizar Claude Code tiene un solo camino:
- Subir
CLAUDE_CODE_VERSIONenscripts/tmux-host/Dockerfile, en una rama, mostrar el diff y mergear con "Confirmo". - Construir:
docker build --network host --no-cache -t tmux-host:<fecha>-<motivo> scripts/tmux-host. El--network hostevita losDNS: transient errordeapken la red del build, vistos este mismo día.--no-cacheevita reusar una capa vieja de npm, como el downgrade a 2.1.278 del 2026-09-28. - Si el build falla en el
RUN test -x ..., no se hace el swap. La imagen nueva está rota y la vieja sigue corriendo. - Hacer el swap con el helper
docker:cli, con confirmación explícita, porque reinicia las 3 sesiones. - Verificar con la tabla "Verificación post-swap" de arriba.
- Subir
- Revisar
health.logtras cualquier reinicio:grep CLAUDE: /DATA/AppData/k3d-lab-v3/tmux-host/logs/health.log | tail. UnCLAUDE: ERRORtambién llega por Telegram. - Si una sesión queda como un shell vacío (ves el prompt
#en vez del❯de Claude), primero miráhealth.logyclaude --version. Si el binario está sano, relanzá en esa sesiónclaude --remote-control --continue. Si está roto, no lo reinstales en caliente: reconstruí la imagen y hacé el swap.
Lección general¶
Una herramienta que se actualiza sola es estado mutable fuera del control de la imagen. En un contenedor que debe ser reproducible, toda dependencia tiene que fijarse en la imagen y verificarse en el build. El auto-update de un componente compartido por varios procesos concurrentes no es una comodidad: es una condición de carrera esperando a ocurrir.