Saltar a contenido

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/claude apuntaba a un binario roto (reportado). El usuario lo reparó a mano con npm uninstall -g + npm install -g @anthropic-ai/claude-code, que dejó la versión 2.1.302 (verificado).
  • scm-portal y cloud-practice mostraban solo el prompt del shell, con la línea claude --remote-control --continue ya ejecutada y sin ningún proceso claude vivo (verificado). health.log decía "lanzado" porque tmux send-keys funcionó, aunque claude murió 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

  1. Nunca actualizar Claude Code en caliente dentro de tmux-host. Eso incluye npm install -g o npm 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.
  2. Actualizar Claude Code tiene un solo camino:
    1. Subir CLAUDE_CODE_VERSION en scripts/tmux-host/Dockerfile, en una rama, mostrar el diff y mergear con "Confirmo".
    2. Construir: docker build --network host --no-cache -t tmux-host:<fecha>-<motivo> scripts/tmux-host. El --network host evita los DNS: transient error de apk en la red del build, vistos este mismo día. --no-cache evita reusar una capa vieja de npm, como el downgrade a 2.1.278 del 2026-09-28.
    3. Si el build falla en el RUN test -x ..., no se hace el swap. La imagen nueva está rota y la vieja sigue corriendo.
    4. Hacer el swap con el helper docker:cli, con confirmación explícita, porque reinicia las 3 sesiones.
    5. Verificar con la tabla "Verificación post-swap" de arriba.
  3. Revisar health.log tras cualquier reinicio: grep CLAUDE: /DATA/AppData/k3d-lab-v3/tmux-host/logs/health.log | tail. Un CLAUDE: ERROR también llega por Telegram.
  4. Si una sesión queda como un shell vacío (ves el prompt # en vez del ❯ de Claude), primero mirá health.log y claude --version. Si el binario está sano, relanzá en esa sesión claude --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.