---
title: "Un vigía de contexto · que te avise antes de que el chat se ponga caro, no después"
description: "Claude Code manda la conversación completa en cada mensaje. Mientras el chat pesa 30 mil tokens no se nota; a 400 mil, una pregunta de una línea paga la relectura de todo el historial, y si vuelves al día siguiente el cache caducó y se regraba entero a tarifa de escritura. Medí dos semanas de mis tr"
url: https://www.davidiriza.com/lab/vigia-de-contexto
author: David Iriza
level: Avanzado
category: Claude Code
tags: ["claude code", "hooks", "tokens", "contexto", "costos"]
tools: ["Claude Code", "Node.js", "context-mode"]
published: 2026-08-28
---
# Un vigía de contexto: que te avise antes de que el chat se ponga caro, no después

Hay dos momentos en que una conversación con Claude Code se pone cara y ninguno de los dos avisa: cuando el contexto cruza cierto tamaño y cada mensaje nuevo relee todo lo anterior, y cuando retomas un chat después de una pausa y el cache ya caducó. El indicador de la terminal muestra el porcentaje de ventana, no lo que cuesta. Este vigía se mete justo antes de cada mensaje, mide el contexto real con los mismos números que Claude Code guarda en disco, y te lo dice una sola vez por escalón. Lo que hagas con el aviso (/clear, /compact o seguir) es tuyo; el vigía solo quita la excusa de 'no me di cuenta'.

**Ficha:** Requiere saber programar · Herramientas: Claude Code, Node.js, context-mode · Te llevas: 6 comandos · Lectura: 28 min · Verificado 28-ago-2026

> Para agentes: esta guía describe pasos ejecutables. Sigue los bloques de comando y código en orden; pregunta al usuario solo lo que no puedas inferir (rutas, nombres de proyecto). Repositorio y contexto del sitio: https://www.davidiriza.com/llms.txt

## De un vistazo

1. Qué es y por qué es un hook
2. Lo que medí antes de escribirlo
3. Cómo se mide el contexto real
4. Los dos eventos y los dos canales de salida
5. Instalar en cinco minutos
6. El script completo
7. Decisiones de diseño y sus porqués
8. Los medidores nativos y por qué no bastan
9. Las palancas que bajan el contexto
10. La rutina de un día con el vigía puesto
11. Cuando el problema es lo que entra, no cuánto llevas
12. Qué hacer con el aviso

## 01 · Qué es y por qué es un hook

_el punto de partida_

El vigía es un archivo ctx-guard.mjs que Claude Code ejecuta en dos momentos: cuando arranca una sesión retomada y cada vez que envías un mensaje. En ambos casos recibe por stdin un JSON con la ruta del transcript de la conversación, lo abre por el final, encuentra la última respuesta del modelo y suma sus tokens de entrada. Con ese número decide si te dice algo.

- **Modo prompt** — Corre en UserPromptSubmit, antes de que el mensaje llegue al modelo. Si el contexto cruzó 200k o 400k, o si la conversación lleva más de 8 horas viva y pesa más de 100k, muestra un aviso. Recuerda qué escalón ya avisó para no repetirse.
- **Modo resume** — Corre en SessionStart cuando la sesión viene de --resume, --continue o /resume. Calcula cuánto lleva pausada la conversación y, si el cache ya caducó, estima cuánto cuesta regrabarlo antes de que escribas la primera palabra.

Lo intenté primero como instrucción en CLAUDE.md ('avísame cuando el contexto sea grande') y no sirve: el modelo no sabe cuánto contexto lleva, y aunque lo supiera, la instrucción vive dentro del mismo contexto que quieres vigilar. Un hook corre fuera del modelo, con acceso al disco, y su salida puede ir solo a tu pantalla. Esa es la diferencia entre un recordatorio y un medidor.

> El vigía no cierra nada. Claude no puede ejecutar /clear: es un comando del CLI, no una herramienta del modelo. Lo único que puede hacer un hook aquí es medir y avisar. El hábito de cerrar sigue siendo tuyo.

## 02 · Lo que medí antes de escribirlo

_las cifras que lo justifican_

Antes del vigía escribí un medidor que recorre todos los transcripts locales de los últimos 14 días y pondera cada petición con los multiplicadores de precio (cache read a 0.1x, cache write a 1.25x, salida a 5x). Sobre 23,804 peticiones en 63 conversaciones salió esto:

- 62.8% del gasto ponderado fue releer cache, 29.5% reescribirlo y 7.6% salida real. Solo el 8% del ciclo fue trabajo producido.
- 14 conversaciones que vivieron más de 48 horas se llevaron el 83.2% del ciclo, con entre 0.4 y 19 horas de trabajo real cada una. Una vivió 318 horas y trabajó 6.7.
- 71% del gasto ocurrió por encima de 400k de contexto. Las 31 conversaciones de menos de 4 horas sumaron el 2.8%.
- 1,060 regrabaciones de cache de más de 50k tokens (pausa larga, cache caducado, retomar) fueron el 25% del ciclo por sí solas.
- Retomar no limpia nada: medí 476k que pasaron a 477k tras 24 horas de pausa, 452k que siguieron en 452k tras 104 horas, 620k a 626k tras 48.
- Lastre de arranque: la mediana de contexto de la primera petición de cada sesión era 96k, con un piso de 35k en la conversación más limpia. La meta razonable es menos de 10k.

El hallazgo que cambió el diseño: yo sí ejecutaba mi comando de cierre de sesión (19 veces en esas dos semanas). El comando guardaba el estado en el repo y yo volvía al día siguiente al mismo chat. Guardar no era el problema; seguir escribiendo en la ventana vieja sí. De ahí que el vigía tenga un modo específico para el momento de retomar.

| Contexto de la conversación | Qué pasa con cada mensaje nuevo |
| --- | --- |
| 35k (limpia) | Referencia. Es el piso que usa el vigía para el 'x veces más caro'. |
| 100k a 200k | Relee 3 a 6 veces el piso a tarifa de cache. Todavía cómodo. |
| 200k a 400k | Cada mensaje relee 6 a 11 veces el piso. Primer aviso (amarillo). |
| 400k o más | 11x o más. Aquí vive el 71% del gasto medido. Aviso rojo. |
| Retomada tras más de 1 hora | El cache caducó: la primera petición regraba todo a 1.25x del precio base antes de responder. |

## 03 · Cómo se mide el contexto real

_de dónde sale el número_

Claude Code guarda cada conversación como un archivo JSONL en ~/.claude/projects/<carpeta-del-proyecto>/<session_id>.jsonl. Cada línea es un evento; las de tipo assistant traen un objeto message.usage con los mismos campos que devuelve la API. El contexto que pagó esa petición es la suma de tres:

**La cuenta que hace el vigía por cada respuesta del modelo**

```javascript
const u = evento.message.usage
const contexto =
  (u.input_tokens || 0) +                 // lo que no estaba en cache
  (u.cache_creation_input_tokens || 0) +  // lo que se escribió al cache (1.25x)
  (u.cache_read_input_tokens || 0)        // lo que se releyó del cache (0.1x)
```

Dos detalles de implementación que importan. Primero: los transcripts de conversaciones largas pesan decenas de megabytes, y este hook corre antes de cada mensaje que escribes. Leerlo completo se sentiría. El vigía abre el archivo, se posiciona 3 MB antes del final y lee solo eso hacia atrás hasta encontrar la primera línea assistant con usage. En mi máquina tarda unos 100 milisegundos sobre un transcript de 59 MB, contando el arranque de Node.

Segundo: se saltan las líneas con isSidechain. Son las peticiones de los subagentes, que tienen su propio contexto y no suman al de tu conversación. Si las cuentas, el vigía te avisa por un subagente que ya terminó.

> Las docs advierten que el transcript se escribe de forma asíncrona y puede ir un turno atrás de la conversación en memoria. Para este uso da igual: un turno de retraso en un umbral de 200k es ruido. Para un hook que necesite la última respuesta exacta, las docs dicen usar el campo last_assistant_message del evento Stop, no el archivo.

## 04 · Los dos eventos y los dos canales de salida

_lo que hay que saber de hooks_

Un hook de tipo command es un ejecutable que Claude Code lanza en un evento del ciclo de vida. Recibe un JSON por stdin y puede responder con un JSON por stdout. Para el vigía importan dos eventos y, sobre todo, entender qué hace cada campo de salida, porque el que elijas decide si tu hook cuesta tokens o no.

| Evento | Cuándo dispara | Qué trae en el stdin |
| --- | --- | --- |
| SessionStart | Al arrancar la sesión. Con matcher 'resume' solo cuando viene de --resume, --continue o /resume. | session_id, transcript_path, cwd, source (startup, resume, clear, compact, fork) |
| UserPromptSubmit | Cada vez que envías un mensaje, antes de que el modelo lo procese. No admite matcher. | session_id, transcript_path, cwd, prompt (tu texto) |

| Campo de salida | A quién le llega | Cuesta tokens |
| --- | --- | --- |
| systemMessage | A ti, como aviso en la terminal. | No. No entra al contexto del modelo. |
| stdout en texto plano (exit 0) | Al modelo, como system reminder invisible en el chat. | Sí. Y en cada mensaje siguiente. |
| hookSpecificOutput.additionalContext | Al modelo, igual que el texto plano pero dentro de JSON. | Sí. |
| decision: 'block' + reason | Bloquea el mensaje, lo borra y te muestra reason. | No, pero pierdes lo que escribiste. |

Aquí está la trampa que me costó una tarde: si el hook imprime texto plano y sale con 0, ese texto se le inyecta al modelo como contexto. Un vigía que avisa 'llevas 400k' en texto plano le está metiendo el aviso al modelo, que a su vez lo relee en cada mensaje. El vigía solo debe escribir un JSON con systemMessage. Ese campo va a tu pantalla y a ningún otro lado.

Descarté bloquear el mensaje con decision: 'block'. Suena tentador ('a 600k no te dejo seguir'), pero las docs son claras: el bloqueo borra el prompt. Perder un mensaje largo que acabas de escribir para que el hook te regañe es peor que el problema que resuelve. El vigía avisa; tú decides.

> El campo suppressOutput existe en las docs pero no hace nada: Claude Code lo acepta y lo ignora, y el stdout de un hook exitoso nunca se muestra en el chat de todos modos. Mi primera versión lo mandaba por si acaso. La versión pública ya no.

## 05 · Instalar en cinco minutos

_manos a la obra_

**1. Descargar el script (terminal)**

```bash
mkdir -p ~/.claude/scripts && curl -fsSL https://raw.githubusercontent.com/davidiriza-lab/ctx-guard/main/ctx-guard.mjs -o ~/.claude/scripts/ctx-guard.mjs
```

> Repo público con licencia MIT: github.com/davidiriza-lab/ctx-guard. Node 18 o superior, sin dependencias. Si prefieres leerlo antes, abajo está completo.

Después registra los dos hooks en ~/.claude/settings.json para que apliquen en todos tus proyectos (o en .claude/settings.json de un repo si lo quieres solo ahí). Si ya tienes una clave hooks, agrega estas entradas dentro de la que existe:

**~/.claude/settings.json — bloque hooks**

```json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "resume",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["/Users/tu-usuario/.claude/scripts/ctx-guard.mjs", "resume"],
            "timeout": 10
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["/Users/tu-usuario/.claude/scripts/ctx-guard.mjs", "prompt"],
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

- args en vez de un string con espacios: con args, Claude Code lanza el ejecutable directo, sin pasar por el shell. Menos sorpresas con comillas y rutas con espacios. Por eso mismo la ruta va absoluta: sin shell no hay quien expanda ~.
- timeout va en segundos. El default de UserPromptSubmit es 30 y bloquea tu mensaje mientras el hook corre; 10 es de sobra para un script que tarda 100 ms, y si algo se cuelga, el mensaje sigue sin el aviso.
- matcher 'resume' en SessionStart: sin él el hook corre también en startup, clear y compact. No rompe nada (el script sale en silencio si no hay contexto), pero es trabajo inútil.
- UserPromptSubmit no admite matcher; si lo pones, se ignora.

**2. Verificar que quedó registrado (dentro de Claude Code)**

```bash
/hooks
```

> Deben aparecer una entrada bajo SessionStart y otra bajo UserPromptSubmit. Los cambios en settings.json se leen al arrancar la sesión.

**3. Probarlo a mano contra un transcript grande (terminal)**

```bash
T=$(ls -S ~/.claude/projects/*/*.jsonl | head -1) && echo "{\"session_id\":\"prueba\",\"transcript_path\":\"$T\",\"hook_event_name\":\"UserPromptSubmit\",\"prompt\":\"hola\"}" | node ~/.claude/scripts/ctx-guard.mjs prompt
```

> Toma tu transcript más pesado y lo pasa al script como si fuera el hook. Si pasa de 200k, imprime el JSON con systemMessage; si no, no imprime nada. Borra ~/.claude/.ctx-guard/prueba.txt para volver a probar.

## 06 · El script completo

_para copiar_

Es el mismo archivo que instala el curl de arriba. Léelo de arriba a abajo una vez: son cuatro partes (entrada, localizar el transcript, medir, decidir qué decir) y no hay nada mágico.

**~/.claude/scripts/ctx-guard.mjs**

```javascript
#!/usr/bin/env node
/**
 * ctx-guard.mjs — vigía de contexto para Claude Code.
 *
 *   node ctx-guard.mjs prompt   → hook UserPromptSubmit: avisa al cruzar umbrales
 *   node ctx-guard.mjs resume   → hook SessionStart (matcher "resume"): avisa al retomar
 *
 * Solo emite "systemMessage": se muestra al usuario y NO entra al contexto del modelo.
 *
 * Variables de entorno:
 *   CTX_GUARD_WARN       primer aviso                 (default 200000)
 *   CTX_GUARD_URGENT     aviso rojo                   (default 400000)
 *   CTX_GUARD_HOURS      horas de vida para avisar    (default 8)
 *   CTX_GUARD_FLOOR      contexto de un chat limpio   (default 35000)
 *   CTX_GUARD_LONG_MULT  recargo por encima de 200k   (default 1)
 *   CTX_GUARD_STATE_DIR  dónde recuerda avisos dados  (default ~/.claude/.ctx-guard)
 */
import { readFileSync, openSync, readSync, fstatSync, closeSync, existsSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
import { homedir } from 'node:os'

const MODO = process.argv[2] || 'prompt'
const num = (v, d) => (Number(v) > 0 ? Number(v) : d)
const WARN = num(process.env.CTX_GUARD_WARN, 200000)
const URGENT = num(process.env.CTX_GUARD_URGENT, 400000)
const HORAS_WARN = num(process.env.CTX_GUARD_HOURS, 8)
const PISO = num(process.env.CTX_GUARD_FLOOR, 35000)
const LONG_MULT = num(process.env.CTX_GUARD_LONG_MULT, 1)
const ESTADO = process.env.CTX_GUARD_STATE_DIR || join(homedir(), '.claude', '.ctx-guard')

const salir = (msg) => {
  if (msg) process.stdout.write(JSON.stringify({ systemMessage: msg }))
  process.exit(0)
}

// ---- entrada del hook (JSON por stdin) --------------------------------------
let payload = {}
try {
  const raw = readFileSync(0, 'utf8')
  if (raw.trim()) payload = JSON.parse(raw)
} catch { /* sin stdin utilizable: salimos en silencio más abajo */ }

// En SessionStart solo nos interesa una conversación retomada.
if (MODO === 'resume' && payload.source && payload.source !== 'resume') salir(null)

// ---- localizar el transcript -------------------------------------------------
function buscarPorSessionId(sid) {
  if (!sid) return null
  const root = join(homedir(), '.claude', 'projects')
  if (!existsSync(root)) return null
  for (const dir of readdirSync(root)) {
    const p = join(root, dir, sid + '.jsonl')
    if (existsSync(p)) return p
  }
  return null
}
const transcript = payload.transcript_path && existsSync(payload.transcript_path)
  ? payload.transcript_path
  : buscarPorSessionId(payload.session_id)
if (!transcript) salir(null)

// ---- leer solo la cola del archivo (los transcripts pesan decenas de MB) -----
function cola(path, bytes) {
  const fd = openSync(path, 'r')
  try {
    const size = fstatSync(fd).size
    const len = Math.min(bytes, size)
    const buf = Buffer.alloc(len)
    readSync(fd, buf, 0, len, size - len)
    return buf.toString('utf8')
  } finally { closeSync(fd) }
}
function cabeza(path, bytes) {
  const fd = openSync(path, 'r')
  try {
    const len = Math.min(bytes, fstatSync(fd).size)
    const buf = Buffer.alloc(len)
    readSync(fd, buf, 0, len, 0)
    return buf.toString('utf8')
  } finally { closeSync(fd) }
}

// Última petición del hilo principal con uso de tokens.
let ctx = 0
let tUltima = 0
for (const linea of cola(transcript, 3000000).split('\n').reverse()) {
  if (!linea || linea[0] !== '{') continue
  let o
  try { o = JSON.parse(linea) } catch { continue }
  if (o.type !== 'assistant' || o.isSidechain) continue
  const u = o.message && o.message.usage
  if (!u) continue
  const c = (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0)
  if (!c) continue
  ctx = c
  tUltima = Date.parse(o.timestamp) || 0
  break
}
if (!ctx) salir(null)

// Antigüedad: primer timestamp del archivo.
let tPrimera = 0
for (const linea of cabeza(transcript, 65536).split('\n')) {
  if (!linea || linea[0] !== '{') continue
  let o
  try { o = JSON.parse(linea) } catch { continue }
  if (o.timestamp) { tPrimera = Date.parse(o.timestamp) || 0; break }
}
const horasVida = tPrimera ? (Date.now() - tPrimera) / 3.6e6 : 0
const horasPausa = tUltima ? (Date.now() - tUltima) / 3.6e6 : 0

const k = (n) => (n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : Math.round(n / 1000) + 'k')
const recargo = ctx > 200000 ? LONG_MULT : 1
// Coste relativo de cada mensaje frente a una conversación limpia.
const factor = ((ctx / PISO) * recargo).toFixed(0)

// ---- modo resume: se retomó una conversación vieja ---------------------------
if (MODO === 'resume') {
  if (ctx < 100000 && horasPausa < 1) salir(null)
  const partes = ['[ctx-guard] Retomaste una conversación de ' + k(ctx) + ' de contexto (' + horasVida.toFixed(0) + 'h de vida).']
  if (horasPausa >= 1) {
    // El cache caducó: la primera petición regraba todo el prefijo a tarifa de escritura (1.25x).
    const regrab = ctx * 1.25 * recargo
    partes.push('El cache caducó hace ' + horasPausa.toFixed(0) + 'h: regrabarlo cuesta ~' + k(regrab) + ' unidades antes de leer tu primera palabra.')
  }
  partes.push('Cada mensaje aquí sale ~' + factor + 'x más caro que en limpio.')
  partes.push('Si es un tema nuevo: /clear. Si retomas un proyecto, reconstruye el contexto desde tu archivo de estado.')
  salir(partes.join(' '))
}

// ---- modo prompt: vigilancia por escalón -------------------------------------
const sid = payload.session_id || 'sin-id'
const escalon = Math.floor(ctx / 100000) // un aviso por cada 100k cruzados
// La antigüedad solo cuesta si la conversación además pesa.
const vieja = horasVida >= HORAS_WARN && ctx >= 100000
const clave = escalon + ':' + (vieja ? 1 : 0)

if (!existsSync(ESTADO)) mkdirSync(ESTADO, { recursive: true })
const archivoEstado = join(ESTADO, sid + '.txt')
let previo = ''
try { previo = readFileSync(archivoEstado, 'utf8').trim() } catch { /* primer aviso */ }
if (previo === clave) salir(null) // ya avisado en este escalón

if (ctx < WARN && !vieja) salir(null)
writeFileSync(archivoEstado, clave)

const avisos = []
if (ctx >= URGENT) {
  avisos.push('[ctx-guard] ROJO: contexto en ' + k(ctx) + '. Cada mensaje cuesta ~' + factor + 'x uno en limpio.')
  avisos.push('Aquí es donde se va la mayor parte del ciclo. /clear si cambiaste de tema, /compact si sigues en la misma tarea.')
} else if (ctx >= WARN) {
  avisos.push('[ctx-guard] AMARILLO: contexto en ' + k(ctx) + '. Cada mensaje sale ~' + factor + 'x uno en limpio.')
  avisos.push('Buen momento para /clear si el tema cambió.')
}
if (vieja) {
  avisos.push('Lleva ' + horasVida.toFixed(0) + 'h viva y sigue cargada. Las conversaciones que se estiran días son las que se llevan el ciclo.')
}
salir(avisos.length ? avisos.join(' ') : null)
```

Así se ve un aviso real, generado contra una de mis conversaciones viejas: '[ctx-guard] ROJO: contexto en 601k. Cada mensaje cuesta ~17x uno en limpio. Aquí es donde se va la mayor parte del ciclo. /clear si cambiaste de tema, /compact si sigues en la misma tarea. Lleva 767h viva y sigue cargada.' Y en modo resume sobre la misma: 'Retomaste una conversación de 601k de contexto. El cache caducó hace 385h: regrabarlo cuesta ~752k unidades antes de leer tu primera palabra.'

## 07 · Decisiones de diseño y sus porqués

_lo que no está en las docs_

- **Un aviso por escalón, no por mensaje** — La primera versión avisaba en cada mensaje por encima de 200k. A los tres mensajes ya no lo leía. Ahora guarda en un archivo por sesión la clave 'escalón:vieja' (por ejemplo 4:1) y solo habla cuando cambia. Cruzas 200k, te lo dice; cruzas 300k, te lo dice; en medio, silencio.
- **Vieja Y cargada, no solo vieja** — El aviso de antigüedad exige contexto de 100k o más. Una conversación de 20k abierta tres días no arrastra nada: relee 20k a tarifa de cache y ya. Avisar ahí era ruido, y el ruido mata al vigía. Lo caro es la combinación de vieja y pesada.
- **El 'x veces más caro' usa tu piso, no cero** — Un mensaje en una conversación limpia ya cuesta lo que pesa tu arranque: CLAUDE.md, memoria, listado de herramientas y MCPs. En mi máquina eran 35k. Comparar contra eso da un número honesto ('17x') en vez de uno inflado. Mide el tuyo y ponlo en CTX_GUARD_FLOOR.
- **El recargo por encima de 200k es configurable** — Mi versión original duplicaba el coste estimado al pasar de 200k, siguiendo la tarifa de contexto largo. La página de precios actual dice que los modelos 4.6 en adelante cobran la ventana completa a tarifa estándar, así que el default público es 1. Si tu proveedor o tu modelo cobran distinto, súbelo.
- **La regrabación se estima a 1.25x** — Al retomar tras una pausa mayor al tiempo de vida del cache, la primera petición escribe todo el prefijo a tarifa de escritura: 1.25x el precio base para el cache de 5 minutos. El vigía usa ese multiplicador para el número que te muestra. En suscripción dentro del plan el cache dura 1 hora; con créditos extra o API key, 5 minutos.
- **Nada de emojis ni colores** — El prefijo [ctx-guard] y las palabras AMARILLO y ROJO son a propósito: el mensaje viaja como texto plano por systemMessage, y en el SDK y en salida stream-json llega como un mensaje informativo. Cualquier terminal lo muestra igual.

Una limitación que conviene saber: el archivo de estado guarda el escalón, no el contexto. Si haces /compact y bajas de 450k a 80k y luego vuelves a subir a 400k, te avisa de nuevo porque la clave cambió en medio. Es el comportamiento que quieres. Lo que no hace es avisarte de que compactar fue caro; para eso están las docs de prompt caching: compactar tras una pausa larga reprocesa todo el historial sin cache.

## 08 · Los medidores nativos y por qué no bastan

_lo que ya trae Claude Code_

Antes de instalar nada conviene saber qué te da Claude Code sin hooks. Hay tres medidores y una línea de estado. Todos son buenos; ninguno te habla en el momento en que importa, que es justo antes de mandar el mensaje.

| Comando | Qué te enseña | Dónde se queda corto |
| --- | --- | --- |
| /context | Una cuadrícula de colores con lo que ocupa la ventana: mensajes, archivos leídos, herramientas MCP, memoria. Es la radiografía del piso. | Lo tienes que pedir. Nadie lo corre a mitad de una racha de 40 mensajes. |
| /usage (o /cost, que es su alias) | Tokens y costo estimado de la sesión, barras del plan, y en suscripción un desglose que marca 'contexto largo' o 'cache misses' cuando pesan 10% o más de tu uso reciente. Tecla d o w para ver 24 horas o 7 días. | Es retrospectivo: te dice que ya gastaste, no que estás a punto. |
| /insights | Un reporte HTML en ~/.claude/usage-data con patrones de trabajo de hasta 200 sesiones: en qué proyectos, dónde se atora, qué probar. | Cuesta tokens generarlo y mira semanas atrás, no el mensaje que vas a mandar. |
| Línea de estado | Puedes configurarla para mostrar el uso de la ventana de forma continua. | Es un porcentaje de la ventana, no un 'x veces más caro', y no sabe si el cache caducó. |

El vigía no compite con esto: se apoya en los mismos números y solo agrega el timing. Mi rutina real es /context una vez al día para entender el piso, /usage al abrir la semana para ver si 'cache misses' aparece en el desglose, y el vigía para el resto, porque es el único que habla sin que se lo pidas.

## 09 · Las palancas que bajan el contexto

_qué mueve el número_

Cuando el vigía suena, tienes más opciones que /clear y /compact. Estas son las que uso, ordenadas de la que más baja el número a la que menos. Todas están en las docs; lo que agrego es cuándo conviene cada una.

- **/clear con nombre** — Lo que más baja: a cero. Si vas a querer volver, /clear seguido de un nombre etiqueta la conversación anterior en el selector de /resume. Es más rápido que /rename antes y /clear después.
- **/compact con instrucción** — Escribe qué debe sobrevivir: '/compact conserva las decisiones, los archivos tocados y lo que falta'. Sin instrucción el resumen decide por ti. Hazlo al cerrar una tarea, no a la mitad; a la mitad se pierden detalles que todavía necesitas. Y recuerda que compactar es en sí una petición grande: lee todo lo que resume.
- **Ventana de autocompactación** — Si dejas que el sistema compacte solo, llega tarde y no eliges qué sobrevive. Puedes fijar el umbral con /autocompact 300k (se guarda en tus settings), con la bandera --autocompact al arrancar, o con CLAUDE_CODE_AUTO_COMPACT_WINDOW en scripts. Yo prefiero que el vigía me avise a 200k y decidir; la autocompactación es la red, no el plan.
- **Subagentes para lo verboso** — Correr tests, leer logs, bajar docs: eso lo hace un subagente en su propio contexto y a tu conversación vuelve solo el resumen. Es la palanca que más subestimé. Un log de 10 mil líneas leído en el hilo principal pesa en cada mensaje que sigue; en un subagente pesa una vez y se va.
- **Hooks que filtran antes de que Claude lea** — El mismo mecanismo del vigía sirve para recortar: un PreToolUse sobre Bash puede reescribir 'npm test' para que solo devuelva las fallas. Las docs traen el ejemplo completo. Miles de tokens por corrida se vuelven cientos.
- **CLAUDE.md corto, skills largas** — Todo lo que está en CLAUDE.md entra en cada sesión aunque no lo uses. Las docs sugieren menos de 200 líneas. Las instrucciones de un flujo específico (revisar PRs, migraciones) van a una skill, que solo carga cuando se invoca. Esto baja el piso, y el piso multiplica todo.

### Modelo y razonamiento: la otra mitad de la cuenta

El vigía mide contexto de entrada. Hay una segunda cuenta que no mide y que también se dispara en sesiones largas: los tokens de pensamiento, que se cobran como salida. Tres ajustes que sí probé:

- /model opusplan (una sola palabra, en minúsculas) usa Opus mientras estás en modo plan y cambia a Sonnet para ejecutar. Pagas el modelo caro solo en la fase corta, la de pensar. Si tu plan sube Opus a 1M de contexto, opusplan lo hereda en la fase de plan; opusplan[1m] lo fuerza en ambas.
- /effort gradúa el razonamiento por sesión: low, medium, high (el default en casi todos los modelos), xhigh, max, ultracode y auto. /effort status te dice en cuál estás. Para trabajo de mantenimiento bajo a medium; para arquitectura subo a xhigh. Con max las docs mismas avisan que tiende a sobrepensar.
- ultrathink es una palabra clave, no un comando: la pones en cualquier parte del mensaje y ese turno razona más hondo sin cambiar el effort de la sesión. Las variantes 'think' o 'think hard' no son palabras clave; Claude Code las pasa como texto normal. Úsala en arranques y decisiones difíciles, no en preguntas de una línea, porque ese turno cuesta más.
- Option+T (Alt+T en Windows y Linux) alterna el pensamiento extendido. En Fable 5 no se puede apagar; ahí la palanca es solo /effort.

> Los turnos de pensamiento no cambian el contexto que mide el vigía, pero sí la factura. Si /usage te marca que la salida pesa más de lo que esperabas, la palanca es effort y modelo, no /clear.

## 10 · La rutina de un día con el vigía puesto

_juntarlo todo_

1. Al abrir: /usage. Si en el desglose aparece 'cache misses' o 'long context', ayer dejaste chats abiertos. Ciérralos hoy antes de empezar.
2. Tarea nueva y compleja: modo plan (Shift+Tab) con /model opusplan. Que el modelo caro piense y el barato construya.
3. Si Claude se pone lento o repite cosas: /context antes de adivinar. Si la conversación domina la cuadrícula, es hora de compactar.
4. Cuando el vigía marque amarillo y la tarea siga: sigue, pero manda lo verboso a subagentes.
5. Tarea cerrada: /compact con instrucción de qué conservar, o /clear con nombre si lo que sigue es otro tema.
6. Decisión difícil: ultrathink una vez, en vez de corregir cinco.
7. Al cerrar el día: estado al repo, /clear. Mañana arrancas en el piso, no en 400k, y el aviso de resume no aparece.

Nada de esto exige disciplina de hierro. Lo que exige es que el número esté enfrente en el momento correcto, y eso es lo único que el vigía hace.

## 11 · Cuando el problema es lo que entra, no cuánto llevas

_otra pieza del rompecabezas_

El vigía ataca el contexto acumulado. Hay otro frente: lo que cada herramienta mete al chat de golpe. Un snapshot de navegador o veinte issues de GitHub pesan decenas de KB cada uno, y ahí hay un plugin que vale la pena conocer: context-mode (mksglu/context-mode). Lo cloné y lo probé antes de escribir esto; lo que sigue es lo que vi, no lo que promete su README.

- **Qué hace** — Registra un servidor MCP con herramientas de sandbox (ctx_execute, ctx_index, ctx_search y otras): en vez de que Claude lea 47 archivos, escribe un script que los recorre y devuelve solo el resultado. Lo que pasa de cierto tamaño se indexa en SQLite con búsqueda de texto completo y se consulta por fragmentos.
- **Qué más hace** — Un hook PreCompact guarda una foto de tareas, decisiones y archivos tocados, y otro en SessionStart la reinyecta al retomar. Es la parte que se traslapa con el sistema de STATE.md de este Lab, resuelta en automático y en base de datos en vez de en un archivo del repo.
- **Cómo se instala** — Dos comandos dentro de Claude Code: /plugin marketplace add mksglu/context-mode y /plugin install context-mode@context-mode. Luego reiniciar o /reload-plugins, y /context-mode:ctx-doctor para verificar. Por fuera, npx context-mode doctor hace el mismo diagnóstico; lo corrí y pasó las pruebas de almacenamiento y runtimes.
- **Lo que hay que saber** — Su hook de SessionStart manda instrucciones por additionalContext, es decir, sí entra al modelo (por diseño: necesita que Claude prefiera sus herramientas). Es el canal opuesto al del vigía, y cuesta tokens en cada sesión. La licencia es Elastic 2.0, no MIT. El '98% de reducción' es de su benchmark, no lo medí yo.

No son excluyentes. El vigía te dice cuánto pesa el chat; context-mode hace que pese menos lo que entra. Si tu problema es que cierras tarde, empieza por el vigía. Si es que cada herramienta te come 50 KB, prueba context-mode una semana y mira /context antes y después.

## 12 · Qué hacer con el aviso

_cuando suena_

1. Amarillo (200k) y cambiaste de tema: /clear. Cuesta cero; el contexto de una tarea terminada no vale nada. Si quieres volver a esa conversación después, /rename antes para encontrarla con /resume.
2. Amarillo y sigues en la misma tarea: sigue. Marca mentalmente que a partir de aquí cada archivo grande que Claude lea pesa el triple.
3. Rojo (400k) en la misma tarea: /compact con instrucción ('conserva las decisiones y los archivos tocados'). Mientras el cache está caliente, compactar cuesta una fracción de lo que sugiere el tamaño.
4. Aviso de resume con cache caducado: si retomas un proyecto, no sigas en ese chat. /clear y reconstruye desde tu archivo de estado (la guía de inicio y cierre de sesión hace exactamente eso). Si el chat era irrelevante, /clear sin más.
5. Aviso de antigüedad: es la señal de que ayer no cerraste. Cierra hoy: guarda el estado en el repo, /clear, y mañana arrancas en 10k en vez de 400k.

Después de dos semanas con el vigía, el patrón cambió solo: las conversaciones se cierran cuando se cierra la tarea, no cuando se acaba la semana. No porque el aviso obligue, sino porque ver '17x' antes de escribir es más convincente que cualquier regla en un CLAUDE.md.

## Preguntas frecuentes

**¿No basta con el porcentaje de contexto que muestra Claude Code?**

Ese indicador mide cuánto falta para la compactación automática, no cuánto cuesta cada mensaje ni si el cache sigue caliente. Un chat al 40% de la ventana en un modelo de 1M son 400k de contexto releídos en cada mensaje. El vigía traduce eso a 'x veces más caro' y detecta el caso de retomar tras pausa, que el porcentaje no ve.

**¿De verdad no cuesta tokens?**

No mientras solo emita systemMessage. Las docs de hooks distinguen dos canales: systemMessage va al usuario; el stdout en texto plano y additionalContext van al modelo como system reminder. Si modificas el script y en algún camino imprimes texto sin JSON, ese texto entrará al contexto en cada mensaje. La regla es: todo lo que sale por stdout pasa por la función salir().

**¿Funciona en Claude Code en la web o en el SDK?**

En la web no lee ~/.claude/settings.json; ahí los hooks vienen del repo (.claude/settings.json) y de la configuración de la organización, y el transcript no está en tu disco, así que el vigía no aplica. En el Agent SDK y en salida stream-json el systemMessage llega como un mensaje informativo que tu código puede mostrar o ignorar.

**¿Puedo cambiar los umbrales sin editar el script?**

Sí, todo es por variables de entorno: CTX_GUARD_WARN, CTX_GUARD_URGENT, CTX_GUARD_HOURS, CTX_GUARD_FLOOR, CTX_GUARD_LONG_MULT y CTX_GUARD_STATE_DIR. Puedes ponerlas en el bloque env de settings.json para que apliquen a todas las sesiones, o exportarlas en tu shell.

**¿Por qué no bloquear el mensaje cuando el contexto es absurdo?**

Porque decision: 'block' en UserPromptSubmit borra el prompt. Si escribiste tres párrafos y el hook decide que 700k es demasiado, pierdes los tres párrafos. Un vigía que te hace perder trabajo se desinstala en un día. Avisar y dejarte decidir es lo único que sobrevive al uso real.

**¿Cómo mido mi propio piso y mis propias cifras?**

Con un script que recorra ~/.claude/projects/**/*.jsonl, tome las líneas assistant con usage y sume input + cache_creation + cache_read por petición. La primera petición de cada sesión te da el lastre de arranque; la mediana de esos valores es tu CTX_GUARD_FLOOR. Agrupando por sessionId y por día ves cuáles conversaciones se estiran y cuánto se llevan. Es el mismo medidor con el que salieron las cifras de esta guía.

**¿No es más fácil dejar que Claude Code compacte solo?**

Es la red de seguridad, no el plan. La compactación automática llega cuando la ventana ya está llena y resume sin que elijas qué sobrevive. Además es una petición grande: lee todo lo que va a resumir, y si el cache caducó, lo lee sin descuento. Compactar tú a 200k, con instrucción y con el cache caliente, cuesta una fracción y conserva lo que te importa. Si de todos modos quieres mover el umbral automático, /autocompact con un valor lo guarda en tus settings.

**¿Claude Code no ofrece ya retomar desde un resumen?**

En Pro y Max, al retomar una sesión grande tras una pausa larga, Claude Code puede ofrecer arrancar desde un resumen en vez de cargar todo el historial. Es buena opción cuando aparece. El vigía sigue teniendo sentido porque ese ofrecimiento no cubre todos los casos (planes, tamaños, si retomas con /resume dentro de una sesión abierta) y porque el aviso del vigía te dice el costo en número, que es lo que te hace decidir.

**¿opusplan sale más caro por usar Opus?**

No en mi experiencia: Opus solo cubre el modo plan, que es la parte corta, y Sonnet ejecuta, que es donde se va el volumen. Lo que sí pasa es que el error de dirección es lo caro de verdad, y pensar bien el plan con el modelo grande evita rehacer. Escríbelo como una palabra: /model opusplan.

**¿context-mode sustituye al vigía?**

No. Atacan cosas distintas: el vigía mide el contexto acumulado y te avisa antes del mensaje; context-mode reduce lo que cada herramienta mete al chat y guarda una foto antes de compactar. Puedes usar los dos. Solo ten presente que context-mode sí inyecta texto al modelo en cada sesión (por additionalContext) y que su licencia es Elastic 2.0.

## Cierre de la guía

El vigía no ahorra tokens por sí mismo: te pone el número enfrente en el único momento en que todavía puedes hacer algo. Instálalo, deja los umbrales por defecto una semana y luego mide tu piso real y ajústalo. Si además cierras cada sesión en el repo, el aviso de resume dejará de aparecer, que es la mejor señal de que está funcionando. Esta guía vive en el Lab de David Iriza.

## Fuentes oficiales

- [Hooks (Docs de Claude Code)](https://code.claude.com/docs/en/hooks): Eventos SessionStart y UserPromptSubmit, campos del stdin, formato de settings.json con args y timeout, y la diferencia entre systemMessage, stdout plano, additionalContext y decision: block.
- [Administrar costos (Docs de Claude Code)](https://code.claude.com/docs/en/costs): Por qué el consumo sube en sesiones largas, /usage, /clear y /compact, y la sección sobre cache misses tras una pausa.
- [Cómo Claude Code usa el prompt caching (Docs de Claude Code)](https://code.claude.com/docs/en/prompt-caching): Tiempo de vida del cache (1 hora en suscripción, 5 minutos con créditos o API key), qué lo invalida y por qué compactar tras una pausa es la operación más cara.
- [Precios (Docs de la API de Claude)](https://platform.claude.com/docs/en/about-claude/pricing): Multiplicadores de cache (escritura 1.25x, lectura 0.1x) y la nota de que los modelos actuales cobran la ventana de 1M a tarifa estándar.
- [Configuración de modelo (Docs de Claude Code)](https://code.claude.com/docs/en/model-config): Alias opusplan y opusplan[1m], niveles de /effort, la palabra clave ultrathink y las tres formas de fijar la ventana de autocompactación.
- [Comandos (Docs de Claude Code)](https://code.claude.com/docs/en/commands): /context, /usage y su alias /cost, /compact con instrucciones, /clear con nombre, /insights y /plugin.
- [context-mode (repositorio)](https://github.com/mksglu/context-mode): Plugin de sandbox e indexado para Claude Code y otros clientes. Licencia Elastic 2.0.

## Repositorios

- [davidiriza-lab/ctx-guard](https://github.com/davidiriza-lab/ctx-guard): Hook de Claude Code que mide el contexto de la conversación y avisa antes de que se ponga caro. Un archivo, umbrales por variable de entorno. (MIT)

## Guías que se conectan con esta

- [inicio-y-cierre-de-sesion](https://www.davidiriza.com/lab/inicio-y-cierre-de-sesion): Es la otra mitad: el vigía avisa que el chat pesa; el sistema de STATE.md hace barato cerrarlo y arrancar limpio.
- [dieta-de-mcps](https://www.davidiriza.com/lab/dieta-de-mcps): El piso de 35k con el que compara el vigía es en buena parte el listado de herramientas MCP. Podarlo baja el piso y todos los múltiplos.
- [slash-commands-propios](https://www.davidiriza.com/lab/slash-commands-propios): Los comandos de cierre que el vigía te recuerda ejecutar se escriben con esta fórmula.
- [claude-code-sin-saber-programar](https://www.davidiriza.com/lab/claude-code-sin-saber-programar): Si /context, /compact y /model todavía te suenan a otro idioma, empieza por ahí y vuelve a esta guía con el vocabulario puesto.

---

Guía escrita con la información oficial disponible al 28 de agosto de 2026. Esta página no está afiliada a Anthropic. Ante la duda, revisa la documentación oficial de Claude Code: https://code.claude.com/docs/en/hooks

---

Versión markdown de https://www.davidiriza.com/lab/vigia-de-contexto — el sitio negocia por `Accept: text/markdown` y por sufijo `.md`. Índice para agentes: https://www.davidiriza.com/llms.txt