En qué se gastan tus tokens de Claude Code: la respuesta está en tu propia sesión
Tabla de contenidos
Llevas una hora con Claude Code, el aviso del límite asoma y te preguntas qué ha pasado si apenas le has pedido tres cosas. Miras la conversación de arriba abajo y no ves nada raro: unas preguntas, unos ficheros, algún comando.
El problema es que la conversación que ves no es la que paga. Lo que paga es todo lo que el agente ha leído por el camino, y eso no aparece en pantalla con su tamaño al lado.
La buena noticia es que no hace falta adivinarlo. Claude Code apunta cada turno de cada sesión en un fichero de tu disco, con los tokens de cada mensaje. Solo hay que saber dónde mirar y qué preguntarle.
Tu sesión está en un JSONL y cuenta más de lo que crees ¶
Cada proyecto tiene su carpeta en ~/.claude/projects/, con la ruta del proyecto convertida en guiones, y cada sesión es un fichero .jsonl: una línea por evento. Desde la raíz del proyecto, la sesión más reciente sale así:
S=$(ls -t ~/.claude/projects/$(pwd | tr -c 'A-Za-z0-9\n' '-')/*.jsonl | head -1)
echo "$S"
Las líneas de tipo assistant traen un objeto usage con cuatro cifras: la entrada nueva (input_tokens), lo que se escribió en caché (cache_creation_input_tokens), lo que se leyó de la caché (cache_read_input_tokens) y la salida (output_tokens).
Hay una trampa que te va a hacer ver el doble: cuando una respuesta tiene varios bloques (texto, una llamada a herramienta, otra), cada bloque va en su propia línea y todas repiten el mismo usage. Si sumas a lo bruto, inflas el total. Hay que quedarse con un registro por message.id:
jq -s '[.[] | select(.type=="assistant")] | unique_by(.message.id)
| map(.message.usage)
| {mensajes: length,
entrada_cacheada: (map(.cache_read_input_tokens) | add),
entrada_nueva: (map(.input_tokens + .cache_creation_input_tokens) | add),
salida: (map(.output_tokens) | add)}' "$S"
Lo pasé sobre la sesión en la que escribo este post, con 21 mensajes del agente:
{
"mensajes": 21,
"entrada_cacheada": 1583688,
"entrada_nueva": 70108,
"salida": 10103
}
Diez mil tokens escritos por el agente y más de un millón y medio leídos. Esa proporción es la norma, no la excepción: cada turno reenvía todo lo anterior, y la caché lo abarata, pero no lo hace gratis. Si tu plan para gastar menos era que el agente hablara menos, estabas atacando la columna pequeña.
Cómo aplicarlo: antes de cambiar nada, mide una sesión normal tuya. Si la entrada cacheada es cien veces la salida, el gasto está en lo que el agente lee y relee, y ahí es donde hay que mirar.
Los resultados de herramientas que más pesan, con nombre y apellidos ¶
Saber que el gasto está en la entrada no te dice qué entrada. Para eso hay que cruzar dos cosas que el JSONL guarda por separado: las llamadas a herramientas (en las líneas assistant, con su id, su nombre y sus argumentos) y sus resultados (en las líneas user, con un tool_use_id que apunta a la llamada).
Este comando mide el texto de cada resultado, lo pasa a tokens con la aproximación de cuatro caracteres por token y te da los diez más gordos con la herramienta y el comando o fichero que los produjo:
jq -rs '
(map(select(.type=="assistant") | .message.content[]? | select(.type=="tool_use")
| {(.id): (.name + " " + ((.input.command // .input.file_path // "") | tostring | .[0:60]))}) | add) as $herramienta
| map(select(.type=="user") | .message.content | arrays | .[] | select(.type=="tool_result")
| [((if (.content|type)=="string" then .content else ([.content[]? | .text // ""] | join("")) end) | length / 4 | floor),
$herramienta[.tool_use_id]])
| sort_by(-.[0]) | .[0:10][] | @tsv' "$S"
Solo cuenta el texto a propósito. Si una herramienta devuelve capturas de pantalla, el JSONL las guarda en base64 y medirlas por caracteres las dispararía a cientos de miles de “tokens” falsos: las imágenes se cobran por píxeles, no por longitud. En la primera versión del comando me salieron cinco capturas del navegador encabezando la lista y no era verdad.
Lo que sí suele aparecer arriba es lo que esperas cuando lo ves, y no cuando lo haces: un cat de un fichero largo que bastaba con un sed -n, un Read de un fichero entero para cambiar una línea, la salida completa de un comando cuando querías el final.
Y aquí está la segunda parte, la que cambia el orden de la lista. Un resultado no se paga una vez. Se queda en el contexto y viaja en cada turno siguiente hasta que cierras la sesión o compactas. Un log de 5.000 tokens que entra en el mensaje 3 de una sesión de 200 se relee casi doscientas veces; el mismo log en el mensaje 190, diez. El peso real se parece más a tamaño × turnos que quedan que al tamaño a secas.
Por eso los primeros minutos de una sesión son los más caros. Lo que lees “para ponerte en contexto” al empezar lo arrastras toda la tarde.
Cómo aplicarlo: ordena por tamaño y fíjate en cuándo entró cada uno. Un resultado grande al principio de una sesión larga vale más que tres grandes al final. Ese es el primero que no vuelves a pedir entero.
Con esto ya sabes leer la factura de una sesión y señalar al culpable. Lo que no te dice el JSONL es qué hacer con esa lista: qué gestos ahorran de verdad cuando los mides, cuáles se repiten mucho y no ahorran nada, y cómo se hace cada cosa en tu agente.
El resto, en el curso
Ya sabes dónde se van. El curso te enseña a que no se vayan
Esto es un adelanto. El curso premium «Ahorra tokens» recoge las prácticas que probamos y medimos en Claude Code, Codex y OpenCode, con chuletas por agente, infografías con los resultados y la skill token-saver para revisar tu proyecto.
Ver el curso Ahorra tokens →12 recursos para developers cada domingo en tu bandeja de entrada
Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.