# Herramientas de costo de Claude Code

Las reglas están en [`REGLAS-CLAUDE-COSTO.md`](../../REGLAS-CLAUDE-COSTO.md). Acá está
lo que las hace cumplir sin depender de que nadie se acuerde.

**Si no programás, no uses este archivo:** [`GUIA-CLAUDE-PARA-TODOS.md`](../../GUIA-CLAUDE-PARA-TODOS.md)
trae un texto que pegás en tu chat y Claude hace todo esto por vos, sin terminal.

Requisito único: **Python 3.6 o más nuevo**. En Mac y Linux ya viene. En Windows, si
`python3` no responde, se instala desde [python.org](https://www.python.org/downloads/)
marcando «Add Python to PATH» — o se le pide a Claude que lo instale.

## Instalar el aviso de contexto

    python3 instalar.py

Agrega un hook a `~/.claude/settings.json` que, cada vez que mandás un mensaje, mira
cuánto contexto arrastra la ventana. Al pasar de **250k** te avisa; al pasar de **500k**
te avisa más fuerte. El aviso lo ves vos, no el modelo: no gasta tokens.

**Después de instalar, abrí `/hooks` una vez** (o reiniciá Claude Code). Las ventanas
que ya están abiertas cargaron la configuración al arrancar y no se enteran solas.

Otras opciones:

    python3 instalar.py --umbral 400   # avisar a los 400k en vez de 250k
    python3 instalar.py --quitar       # desinstalar

## Cuando el aviso no alcanza: `--imponer`

    sudo python3 instalar.py --imponer

Además del aviso, escribe la configuración impuesta del sistema y fija
`autoCompactWindow`: al llegar al umbral, **Claude Code resume la conversación solo**, y
el usuario no puede subir ese valor desde su propia configuración.

**El aviso avisa; esto obliga.** Lo normal es sólo el aviso. Esto se agrega a quien se
sigue topando con el límite después de avisado — y no viene encendido por defecto porque
compactar no es gratis: resume, y resumir pierde detalle.

Necesita `sudo` y por ahora sólo está probado en Mac y Linux. Sin `sudo` el aviso se
instala igual y te dice cómo agregar lo impuesto después. Se saca con
`sudo python3 instalar.py --quitar --imponer`.

**Se funde en los dos sentidos.** El archivo de ajustes impuestos del sistema es donde
también viviría cualquier política de la empresa. Al instalar se agrega lo propio sin
tocar lo demás, y al quitar se sacan **sólo las claves propias**: el archivo se borra
únicamente si no quedaba nada más. Siempre deja un `.bak`.

Y un paso más, que no es obvio: **si la organización ya tenía su propia compactación
configurada, desinstalar la devuelve a como estaba**, no la borra. Para eso, la primera
instalación guarda una copia del archivo original en `.antes-de-claude-costo` y **no la
vuelve a pisar nunca**. El `.bak` se reescribe en cada corrida, así que no sirve para
eso: sin la copia original, un ciclo instalar/desinstalar se llevaría en silencio una
política que ya existía.

> Por qué hace falta: **si nadie configura `autoCompactWindow`, Claude Code no compacta
> hasta llegar al límite del modelo** — un millón de tokens en Opus 5 —, y mientras tanto
> cada mensaje paga por toda la conversación anterior. Medido el 13-ago-2026 en tres
> máquinas del equipo: una iba al 92% de su ventana de 5 horas y otra al 84% de la
> semanal con tres días por delante.

El instalador **no pisa el resto de tu configuración**: lee el archivo, agrega sólo su
hook y lo vuelve a escribir. Deja un respaldo en `settings.json.antes-de-claude-costo`
la primera vez, se puede correr las veces que sea sin duplicarse, y si el
`settings.json` está roto se detiene y lo dice en vez de reemplazarlo.

Al terminar **se prueba a sí mismo**: corre el hook recién instalado contra una
transcripción real y te muestra el aviso tal como lo vas a ver. Si no corre, lo dice.
Instalar sin comprobar deja justo el problema que el hook viene a resolver: algo que
parece hecho y no lo está.

## Reportar al lugar central: `reportar-consumo.py`

    python3 reportar-consumo.py --ver        # muestra lo que enviaría, sin enviar
    python3 reportar-consumo.py              # mide y envía
    python3 reportar-consumo.py --instalar   # lo deja corriendo solo, una vez al día

`medir-consumo.py` sólo ve **esta** máquina, y el panel de la organización da el cuánto
por persona pero no el en qué. Sin esto no se puede saber si la §13 se cumple. Esto lo
resuelve: cada máquina mide sola y manda **su resumen** a la API de la bitácora.

**Qué se manda, exactamente.** Sólo números agregados por día: peticiones, sesiones,
tokens de cada clase, contexto medio, cuántas peticiones pasaron de 500k, y el reparto
por modelo. **Nunca el contenido de las conversaciones**, ni nombres de archivos, ni de
proyectos, ni comandos — el guion sólo mira el bloque `usage` de cada respuesta y su
fecha. El resto de la transcripción no se lee.

**Por qué el envío es saliente.** Hoy mismo se montó un colector de OpenTelemetry en el
servidor de control y **se desmontó a propósito**: pedía abrir un puerto entrante en el
bastión (oci-dr PR #20 y #21). Acá el flujo va al revés — nadie escucha, cada máquina
envía— así que no se abre nada nuevo y el motivo del descarte no aplica.

Se configura con dos variables de entorno, sin tocar el código:

| | |
|---|---|
| `MB_CONSUMO_URL` | a dónde enviar (por omisión, la API de la bitácora) |
| `MB_CONSUMO_CLAVE` | la clave compartida |
| `MB_CONSUMO_PERSONA` | el nombre real; si no, se usa el usuario del sistema |

**Un día sin internet no se pierde.** Lo que no se pudo enviar queda en
`~/.claude/consumo-pendiente.jsonl` y se reintenta en la corrida siguiente, sin duplicar.
Importa porque las transcripciones se borran solas a los 30 días: lo que no se mida
antes, no se recupera.

## Medir en qué se está yendo la cuota

    python3 medir-consumo.py              # el resumen
    python3 medir-consumo.py --sesiones   # el detalle sesión por sesión

Lee las transcripciones de `~/.claude/projects/`, donde cada respuesta trae su consumo
exacto, y las valora con la tarifa pública de la API. Sirve para comparar una práctica
contra otra; no es una factura.

Sólo ve **esta** máquina, y sólo las sesiones que Claude Code todavía no limpió
(`cleanupPeriodDays`, 30 días por omisión). Para el consumo del equipo completo, el
panel de la organización; para saber en qué se fue el tuyo, `/usage`.

## Cómo se probó (13-ago-2026)

| Qué | Resultado |
|---|---|
| Sesión real que terminó en 986k | avisa, nivel grave |
| Sesión real que terminó en 753k | avisa, nivel grave |
| Sesión por debajo del umbral | calla |
| El mismo aviso dos veces en la misma sesión | calla la segunda |
| Transcripción inexistente, o entrada que no es JSON | sale en silencio, código 0 |
| Instalar sobre el `settings.json` real (109 permisos) | los 109 intactos, todas las claves intactas |
| Instalar tres veces seguidas | un solo hook, sin duplicados |
| Desinstalar | el archivo queda **idéntico** al original |
| `settings.json` roto | se detiene con un mensaje claro, no lo toca |
| Sin `settings.json` previo | lo crea bien |
| Instalado de verdad en la Mac de Manuel | los 109 permisos intactos; la autoprueba mostró el aviso |
| Ciclo completo: se llena → avisa → `/clear` → se vuelve a llenar | vuelve a avisar |
| `--umbral 900` / `700` / `100` sobre una ventana de 742k | calla / avisa / avisa |
| `--umbral` con basura, o sin valor | el instalador se planta con un mensaje claro |
| Instalar en un HOME con espacios, `$(...)` y backticks | la ruta se guarda literal; desinstala y vuelve idéntico |
| Consola de Windows en cp1252 (modo estricto) | instala, desinstala y mide sin reventar; las tildes se leen bien |
| Desinstalar en una máquina donde nunca se instaló | lo dice, ya no revienta con un traceback |
| Python 2.7 y Python 3.5 (en Docker) | dicen qué versión hace falta y qué hacer |
| Python 3.6, el mínimo declarado (en Docker) | instala bien |
| `--imponer` sobre un archivo que ya traía política de la empresa | la conserva y agrega lo suyo al lado |
| `--quitar --imponer` sobre ese mismo archivo | saca sólo lo suyo; la política de la empresa queda |
| `--quitar --imponer` cuando no había nada más | borra el archivo, con respaldo `.bak` |
| La organización ya tenía **su propia** compactación en 700k | al quitar vuelve a 700k, no desaparece |
| `--imponer` dos veces con umbrales distintos | el respaldo original no se pisa; al quitar vuelve al de origen |
| El archivo es JSON válido pero una lista, no un objeto | lo dice y no lo toca |

Un detalle que salió de instalarlo en serio: la primera versión anotaba la ruta que
devuelve `sys.executable`, que en Homebrew apunta a la instalación concreta
(`python@3.13/3.13.14_1/...`). Esa ruta **desaparece en la próxima actualización de
Homebrew** y el hook queda muerto sin que nadie se entere. Ahora se prefiere el
`python3` del PATH, que es un enlace estable, después de confirmar que es el mismo
Python.

Un caso que apareció probando y vale contar: una sesión de 517k de contexto medio
terminó marcando 71k, y parecía que el aviso fallaba. No fallaba — esa sesión se había
**auto-compactado** al final. El hook mira el contexto de la última respuesta, que es
justamente lo que se paga en la siguiente.

También ignora las respuestas de los subagentes (`isSidechain`), que traen su propio
contexto mucho más chico: si se colaran, el aviso no aparecería nunca.

Y otras dos que salieron de revisar el código buscando romperlo:

- **El aviso se olvida cuando la ventana vuelve a estar limpia.** El `session_id` no
  cambia al hacer `/clear`, así que la marca de «ya avisé» sobrevivía a la limpieza: una
  ventana que se limpiaba y se volvía a llenar no recibía el aviso **nunca más**, justo
  cuando más falta hacía.
- **El umbral viaja como argumento, no como `VAR=valor` adelante del comando.** Ese
  prefijo sólo lo entiende bash; en Windows sin Git Bash el hook corre en PowerShell y
  `--umbral` habría fallado ahí y sólo ahí.

Y una que levantó la revisión del PR: el hook se registra con la **forma `args`**, donde
Claude Code ejecuta el intérprete directamente y le pasa la ruta del script como
argumento, **sin pasar por un shell**. La forma anterior armaba una sola cadena
entrecomillada: alcanza con que el HOME de alguien tenga un espacio, un `$` o un
backtick para que el shell la parta mal o evalúe lo que no debe. La autoprueba corre
igual, sin shell y con la misma lista que quedó guardada.

**Sobre la consola de Windows.** Suele venir en **cp1252**, donde los emojis de los avisos
(⚠️, 💡) no existen. Al imprimirlos, Python cortaba con `UnicodeEncodeError` y el
instalador moría a mitad de camino sin decir por qué — **lo reportó María el 13-ago-2026**,
que tuvo que correrlo con `PYTHONIOENCODING=utf-8` para poder instalarlo.

Los tres guiones piden ahora que la salida reemplace lo que no pueda escribir en vez de
fallar. Las tildes sí existen en cp1252, así que el texto en español se lee igual y sólo
los emojis salen como signo de pregunta. **El hook nunca tuvo el problema**: escribe JSON
con los caracteres escapados (`⚠`), o sea ASCII puro — también lo notó ella.

**Sobre correrlo con un Python viejo.** El instalador detecta la versión y dice qué hacer
en vez de reventar. Que eso funcione es más delicado de lo que parece: Python **lee el
archivo entero antes de ejecutar la primera línea**, así que el chequeo sólo llega a
correr si todo el archivo es parseable por el Python viejo. Por eso `instalar.py` no usa
nada de sintaxis 3.6+ (ni f-strings ni literales como `250_000`) y declara
`# -*- coding: utf-8 -*-`, sin la cual Python 2 muere por las tildes del comentario de
arriba de todo. Las dos cosas se descubrieron **probando en Docker**, no leyendo: la
primera la marcó el bot, la segunda no la vio nadie hasta correrlo.

Un aviso para quien vuelva a probar esto a mano: **las transcripciones de sesiones vivas
cambian mientras se las mira**. Probando, un archivo pasó de 988k a 76k entre dos
comandos porque esa ventana se auto-compactó sola. Las pruebas de arriba corren contra
copias congeladas.
