TL;DR El JSON que recibe tu status line trae un objeto
prompt_cacheconwarm,ttl,expires_atyrecache_tokens_if_cold. Con una docena de líneas dejqla barra se repinta sola en el segundo exacto en que tu caché muere, y te dice cuántos tokens vas a recachear si escribes.
Te levantas a por un café. Vuelves, escribes lo primero que se te ocurre y ese turno reprocesa la conversación entera a precio completo. No te ha avisado nadie, y encima lo pagas sin enterarte de que lo has pagado.
La línea de /cost te lo cuenta, pero después y solo si abres la pantalla. La status line lo puede contar antes, y hay una línea en la documentación que explica por qué: Claude Code vuelve a ejecutar tu script cuando una caché caliente llega a su expires_at. Sabe cuándo va a morir y despierta a tu barra en ese instante.
Lo que viaja en el JSON
Esto es lo que recibió mi propia status line hace un rato, sin retocar:
"prompt_cache": {
"warm": true,
"caching_observed": true,
"ttl": "1h",
"expires_at": 1789595743,
"requests": 238,
"misses": 1,
"expected_rebuilds": 1,
"hit_ratio": 0.9838970255083798,
"cache_write_tokens": 1207215,
"miss_recache_tokens": 340839,
"last_miss_at": 1789590652,
"last_miss_cause": { "causes": ["ttl_expired_1h"] },
"miss_causes": { "ttl_expired_1h": 1 },
"recache_tokens_if_cold": 461120
}
Fíjate en el fallo. Uno solo en 238 peticiones, y su causa es ttl_expired_1h: exactamente lo que cuenta este tip, la caché que expira mientras no miras. Me pasó veinticinco minutos antes de capturar esto y no me enteré, porque mi barra todavía no lo pintaba.
Y fíjate en el último campo, que es el que no puedes ver en ningún otro sitio. recache_tokens_if_cold son 461.120 tokens: lo que va a reprocesar mi siguiente mensaje si para entonces la caché ya se ha enfriado. No es una métrica, es el precio de escribir.
El segmento
Va dentro del script que ya tengas. Si todavía no tienes ninguno, monta primero la status line y vuelve aquí.
if [ "$(echo "$input" | jq '.prompt_cache != null')" = "true" ]; then
WARM=$(echo "$input" | jq -r '.prompt_cache.warm')
PCT=$(echo "$input" | jq -r '(.prompt_cache.hit_ratio // 0) * 100 | floor')
TTL=$(echo "$input" | jq -r '.prompt_cache.ttl // "?"')
COLD=$(echo "$input" | jq -r '(.prompt_cache.recache_tokens_if_cold // 0) / 1000 | floor')
if [ "$WARM" = "true" ]; then
CACHE="${GREEN}●${RST} ${DIM}caché${RST} ${PCT}% ${DIM}${TTL}${RST}"
else
CACHE="${YELLOW}▲ caché fría · +${COLD}k al escribir${RST}"
fi
fi
Dos estados, y esto es lo que imprime con el JSON de arriba:
● caché 98% 1h
▲ caché fría · +461k al escribir
Esa primera comprobación no sobra. El objeto no existe hasta la primera respuesta de la API: lo verifiqué capturando el JSON de una sesión recién abierta y ahí la clave prompt_cache directamente no está. Sin la guarda, el segmento se pinta en ámbar con un +0k antes de que tú hayas escrito nada.
No es una cuenta atrás, y eso es lo bueno
La status line no tiene reloj: corre por eventos. Estos:
- llega un mensaje nuevo del asistente
- termina un
/compact - cambias de modo de permisos o de modo vim
- cambias el
commandde tus settings - una ventana de rate limit llega a su
resets_at - una caché caliente llega a su
expires_at
Ese último es el que hace el trabajo. Mientras escribes, la barra está en verde porque se repinta con cada respuesta. Te vas, y en el segundo en que la caché expira Claude Code lanza tu script una vez más y la barra se pone en ámbar sola, con la sesión parada y sin que tú hagas nada. Vuelves, miras abajo y ya sabes si teclear sale caro.
Si de verdad quieres la cuenta atrás, es otra cosa y la pides tú: añade refreshInterval a tu statusLine y el script corre también cada N segundos, con lo que puedes pintar los minutos que le quedan a expires_at.
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"refreshInterval": 30
}
}
El mínimo es 1 segundo. Y no te preocupes por el gasto: la status line corre en local y no consume tokens de API, lo dicen las propias docs.
Referencia: los campos que vas a usar
| Campo | Qué es | Para qué lo pintas |
|---|---|---|
warm |
Si el prefijo cacheado sigue dentro de su TTL | El semáforo |
ttl |
"5m" o "1h" |
Saber cuánto margen tienes |
expires_at |
Cuándo se enfría, en epoch | La cuenta atrás, con refreshInterval |
recache_tokens_if_cold |
Lo que recachea tu próximo mensaje si ya está fría | El precio de escribir |
hit_ratio |
Tokens leídos de caché sobre la entrada total | La salud de la sesión |
misses · miss_causes |
Fallos y su causa, por nombre | Ver si se repite el mismo culpable |
Los trece campos y su letra pequeña están en las docs. Estas seis filas son las que caben en una barra.
Ese ttl merece un vistazo antes de montar nada, porque la hora entera no la tiene todo el mundo: en la conversación principal son sesenta minutos mientras estés dentro del uso incluido de tu plan, y cinco en cuanto pasas a usage credits o tiras de API key. Con cinco, cualquier interrupción te devuelve en frío, que es justo por lo que conviene compactar antes de levantarte y no al volver.
Con esto la barra te dice tres cosas de un vistazo: si la caché está viva, si tu sesión la está aprovechando y cuánto cuesta el siguiente mensaje. El resto del mecanismo, qué es exactamente lo que se cachea y qué lo rompe, y si prefieres el número a demanda en vez de siempre puesto, la línea de /cost. En ese mismo JSON viaja también el porcentaje de límite consumido, que es el otro vecino que merece un hueco en la barra.
Documentación oficial: Status line: prompt cache fields
Requisitos
- Claude Code v2.1.251 o superior para el objeto
prompt_cache. Ellast_miss_causey elmiss_causesnecesitan la v2.1.260. jqpara el snippet. El JSON de arriba es una captura real de mi sesión en la v2.1.270, y la salida de los dos estados está ejecutada contra ese mismo JSON.- El objeto cuenta solo la conversación principal. Lo que gasten tus subagentes no entra.