Tutoriales

Monitoriza tu consumo de Claude Code en GNOME con Python y AppIndicator

Si trabajas a diario con Claude Code, seguramente te haya pasado: estás a mitad de una tarea larga y de pronto te quedas sin cuota. Existe el comando /usage dentro de Claude Code, pero hay que pararse a escribirlo. Lo cómodo sería tener el dato siempre delante, en la barra superior de Ubuntu.

En este artículo montamos justo eso: un indicador que muestra de forma permanente el porcentaje consumido de la sesión de 5 horas y el de la cuota semanal, con el tiempo que falta para cada reset. Se refresca solo cada minuto y arranca automáticamente al iniciar sesión.

De dónde salen los datos

Claude Code guarda sus credenciales OAuth en ~/.claude/.credentials.json. Dentro hay un accessToken que sirve para consultar el mismo endpoint que usa internamente el comando /usage:

TOKEN=$(python3 -c "import json;print(json.load(open('$HOME/.claude/.credentials.json'))['claudeAiOauth']['accessToken'])")

curl -s https://api.anthropic.com/api/oauth/usage \
  -H "Authorization: Bearer $TOKEN" \
  -H "anthropic-beta: oauth-2025-04-20"

La respuesta trae bastantes campos, pero los dos que nos interesan son estos:

{
  "five_hour": {
    "utilization": 56.0,
    "resets_at": "2026-08-08T11:30:00+00:00"
  },
  "seven_day": {
    "utilization": 14.0,
    "resets_at": "2026-08-13T20:00:00+00:00"
  }
}

utilization es el porcentaje consumido y resets_at la marca temporal en UTC en la que ese contador vuelve a cero. Con eso ya tenemos todo lo necesario.

Un aviso antes de seguir: ese token da acceso a tu cuenta. Úsalo solo en local y no lo pegues en ningún sitio público, ni en capturas de pantalla ni en pastebins.


Por qué una app de bandeja y no una extensión de GNOME

El primer impulso es escribir una extensión de GNOME Shell, que es lo que pinta texto nativo en la barra. Y funciona, pero tiene un inconveniente serio en Ubuntu moderno: bajo Wayland, una extensión recién instalada no se carga hasta que reinicias GNOME Shell, y eso significa cerrar sesión. El clásico Alt+F2 seguido de r solo funciona en X11.

La alternativa que evita el cierre de sesión es una aplicación de bandeja con AyatanaAppIndicator3. Se apoya en la extensión ubuntu-appindicators, que Ubuntu trae activada de serie, y se registra en caliente: la lanzas y aparece al instante.

El script del indicador

Guarda esto en ~/.local/bin/claude-usage-indicator.py:

#!/usr/bin/env python3
import fcntl
import json
import sys
import urllib.request
from datetime import datetime, timezone
from pathlib import Path

import gi

gi.require_version("Gtk", "3.0")
gi.require_version("AyatanaAppIndicator3", "0.1")
from gi.repository import AyatanaAppIndicator3 as AppIndicator
from gi.repository import GLib, Gtk

CRED = Path.home() / ".claude" / ".credentials.json"
URL = "https://api.anthropic.com/api/oauth/usage"
REFRESH_SECONDS = 60


def eta(iso):
    if not iso:
        return ""
    try:
        t = datetime.fromisoformat(iso)
    except ValueError:
        return ""
    secs = (t - datetime.now(timezone.utc)).total_seconds()
    if secs <= 0:
        return "0m"
    d, rem = divmod(int(secs), 86400)
    h, m = divmod(rem // 60, 60)
    if d:
        return f"{d}d{h}h"
    if h:
        return f"{h}h{m:02d}m"
    return f"{m}m"


def reset_clock(iso):
    if not iso:
        return ""
    try:
        t = datetime.fromisoformat(iso).astimezone()
    except ValueError:
        return ""
    return t.strftime("%a %d %H:%M")


def fetch():
    token = json.loads(CRED.read_text())["claudeAiOauth"]["accessToken"]
    req = urllib.request.Request(
        URL,
        headers={
            "Authorization": f"Bearer {token}",
            "anthropic-beta": "oauth-2025-04-20",
        },
    )
    with urllib.request.urlopen(req, timeout=8) as r:
        return json.load(r)


class Indicator:
    def __init__(self):
        self.ind = AppIndicator.Indicator.new(
            "claude-usage",
            "utilities-system-monitor-symbolic",
            AppIndicator.IndicatorCategory.SYSTEM_SERVICES,
        )
        self.ind.set_status(AppIndicator.IndicatorStatus.ACTIVE)

        self.menu = Gtk.Menu()
        self.detail = Gtk.MenuItem(label="Cargando...")
        self.detail.set_sensitive(False)
        self.menu.append(self.detail)
        self.menu.append(Gtk.SeparatorMenuItem())

        refresh = Gtk.MenuItem(label="Actualizar ahora")
        refresh.connect("activate", lambda _w: self.refresh())
        self.menu.append(refresh)

        quit_item = Gtk.MenuItem(label="Salir")
        quit_item.connect("activate", lambda _w: Gtk.main_quit())
        self.menu.append(quit_item)

        self.menu.show_all()
        self.ind.set_menu(self.menu)

        self.refresh()
        GLib.timeout_add_seconds(REFRESH_SECONDS, self.refresh)

    def refresh(self):
        try:
            d = fetch()
            s, w = d["five_hour"], d["seven_day"]
            sp, wp = int(round(s["utilization"])), int(round(w["utilization"]))
            label = f"S {sp}% ({eta(s['resets_at'])}) · W {wp}% ({eta(w['resets_at'])})"
            detail = (
                f"Sesion 5h: {sp}%  ->  resetea {reset_clock(s['resets_at'])}\n"
                f"Semanal:  {wp}%  ->  resetea {reset_clock(w['resets_at'])}"
            )
        except Exception as e:
            label = "Claude: --"
            detail = f"Sin datos: {e}"

        self.ind.set_label(label, "S 100% (0h00m) · W 100% (0d0h)")
        self.detail.set_label(detail)
        return GLib.SOURCE_CONTINUE


if __name__ == "__main__":
    _lock = open("/tmp/claude-usage-indicator.lock", "w")
    try:
        fcntl.flock(_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except BlockingIOError:
        print("Ya hay una instancia en ejecucion.", file=sys.stderr)
        sys.exit(0)

    Indicator()
    Gtk.main()

Hay dos detalles que merece la pena señalar. El primero es el bloqueo de instancia única con fcntl.flock al final: sin él es facilísimo acabar con tres o cuatro copias apiladas en la barra si lanzas el script a mano mientras el autoarranque ya lo tenía corriendo.

El segundo es el segundo argumento de set_label, la llamada guide. Sirve para que el panel reserve el ancho del texto más largo posible y el icono no baile cada vez que el número cambia de una a tres cifras.

Dale permisos de ejecución y pruébalo:

chmod +x ~/.local/bin/claude-usage-indicator.py
python3 ~/.local/bin/claude-usage-indicator.py

Debería aparecer al momento en la barra superior algo así:

S 57% (3h27m) · W 14% (5d11h)

Y al pinchar sobre el icono, un menú con la hora exacta de cada reset, un botón para actualizar al instante y otro para salir.


Que arranque solo al iniciar sesión

GNOME lanza automáticamente todo lo que encuentre en ~/.config/autostart/. Creamos ahí claude-usage-indicator.desktop:

[Desktop Entry]
Type=Application
Name=Claude Usage Indicator
Comment=Muestra la cuota usada de Claude en la barra superior
Exec=/usr/bin/python3 /home/TU_USUARIO/.local/bin/claude-usage-indicator.py
Icon=utilities-system-monitor-symbolic
Terminal=false
X-GNOME-Autostart-enabled=true

Ojo con la línea Exec, porque aquí es donde es fácil pegarse. La tentación es escribir %h para referirse al home, pero %h no existe en la especificación de Desktop Entry. Los únicos códigos válidos son %f, %u, %F, %U, %i, %c y %k. Si pones %h, GNOME descarta la entrada en silencio y te vuelves loco buscando por qué no arranca. Usa la ruta absoluta.

Puedes verificar que el fichero es correcto antes de reiniciar:

desktop-file-validate ~/.config/autostart/claude-usage-indicator.desktop

Si no devuelve nada, está bien. También aparecerá listado en Aplicaciones al Inicio por si algún día lo quieres desactivar desde la interfaz gráfica sin borrar el fichero.


Resumen de ficheros

FicheroPara qué sirve
~/.local/bin/claude-usage-indicator.pyEl indicador de la barra
~/.config/autostart/claude-usage-indicator.desktopArranque automático al iniciar sesión
~/.claude/.credentials.jsonToken OAuth, lo genera Claude Code

La limitación que conviene conocer

El indicador depende del token de acceso que guarda Claude Code, y ese token caduca cada pocas horas. Claude Code lo renueva por su cuenta, pero solo cuando lo usas. Si pasas un día entero sin abrirlo, el indicador mostrará Claude: -- hasta que vuelvas a lanzar claude en una terminal.

Se podría hacer que el script renovase el token él mismo con el refreshToken, pero es mala idea: ese proceso rota el refresh token, y hacerlo desde fuera puede desloguearte de Claude Code. Mejor asumir el hueco y que sea la propia herramienta la que gestione sus credenciales.

Si ves el icono pero no el texto

Puede ocurrir según la versión de ubuntu-appindicators. La extensión sabe pintar etiquetas, pero en algunas builds las propiedades XAyatanaLabel vienen comentadas en su definición de interfaz D-Bus, con este razonamiento en el propio código fuente:

These are commented out because GDBusProxy would otherwise require them, but they are not available for KDE indicators

Puedes comprobar si tu etiqueta se está publicando correctamente por D-Bus con:

gdbus call --session --dest org.kde.StatusNotifierWatcher \
  --object-path /StatusNotifierWatcher \
  --method org.freedesktop.DBus.Properties.Get \
  org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems

Si tu indicador aparece en esa lista, el problema está en el lado de la extensión y no en tu script. En ese caso la salida es escribir una extensión de GNOME Shell propia, asumiendo el cierre de sesión que comentábamos al principio.

Conclusión

Poco más de cien líneas de Python y un fichero de arranque para no volver a quedarte a medias sin saber por qué. Lo interesante del montaje es que no depende de ninguna extensión de terceros ni de reiniciar nada: se apoya en piezas que Ubuntu ya trae puestas.

El mismo endpoint sirve para cualquier otra cosa que se te ocurra: un aviso cuando pases del 80%, un registro histórico de consumo, o integrarlo en Conky o en Polybar si no usas GNOME.

F. Javier Carazo Gil

Cofundador de CODECTION, empresa especializada en WordPress, autor de un libro sobre WordPress (el primero en español) y multitud de artículos (en medios físicos y virtuales) sobre el tema. Participa en la comunidad WordPress de forma activa siendo parte del equipo organizador de la WordPress Meetup de Córdoba, dando charlas en diferentes WordCamp y siendo autor y coautor de multitud de plugins libres y premium para WordPress de gran éxito.

Compartir
Publicado por
F. Javier Carazo Gil
Etiquetas: claudegnmepython

Entradas recientes

No soy yo

4 días hace

Sin batería

2 semanas hace

432 CVEs

3 semanas hace

Anti-IA

4 semanas hace

Memoria

1 mes hace

Mal uso

1 mes hace