Motivation
Beim Arbeiten mit Claude Code wollte ich auf einen Blick sehen, wie viel meines 5-Stunden-Plans ich bereits verbraucht habe und was die aktuelle Session kostet – ohne den Workflow zu unterbrechen. Eine native Menüleisten-App ist der ideale Ort für solche Hintergrundinformationen.
Was die App zeigt
Das Menüleisten-Icon zeigt die 5-Stunden-Planauslastung mit einem ✦-Präfix und farbkodiertem Zustand: grün unter 50%, gelb von 50–79%, rot ab 80%. Wenn seit mehr als 5 Minuten keine Claude-Antwort eingegangen ist, dimmt das Icon, um veraltete Daten zu signalisieren.
Ein Klick auf das Icon öffnet ein Popover mit:
- Einem kreisförmigen Fortschrittsring für die wichtigste Kennzahl (Planauslastung wenn verfügbar, sonst Kontextfenster)
- Horizontalen Fortschrittsbalken für Kontextfenster-% und 7-Tage-Planauslastung
- Session-Kosten in USD und dem aktuellen Modellnamen
- Der Reset-Zeit für das aktive 5-Stunden-Fenster
Architektur
Die App hat zwei unabhängige Datenquellen, die unterschiedlich häufig aktualisiert werden.
Planauslastung — Anthropic OAuth API
Alle 5 Minuten ruft die App https://api.anthropic.com/api/oauth/usage auf, um die Auslastung für das aktuelle 5-Stunden- und 7-Tage-Fenster abzurufen. Ein separater Login ist nicht nötig: Der OAuth-Token wird direkt aus dem macOS-Schlüsselbund über das Security-Framework gelesen, wo Claude Code ihn unter dem Service-Key Claude Code-credentials speichert. Der Request verwendet einen Bearer-Authorization-Header und den Header anthropic-beta: oauth-2025-04-20.
Session-Details — statusLine-Hook
Claude Codes Hook-System kann nach jeder Antwort ein Skript ausführen und dabei den vollen Session-State übergeben. Ich habe ~/.claude/hooks/stop_hook.sh als statusLine-Befehl in ~/.claude/settings.json registriert – der einzige Hook-Typ, der context_window-, cost- und model-Daten enthält.
Das Skript empfängt ein JSON-Payload auf stdin, ergänzt per jq --arg einen UTC-captured_at-Zeitstempel und schreibt das Ergebnis atomar nach ~/.claude/session_usage.json – über eine PID-suffixierte Temp-Datei und mv. Die Verwendung von mv ist bewusst: Auf demselben Dateisystem ist Umbenennen atomar, die App liest also nie eine halb geschriebene Datei. Außerdem gibt das Skript eine lesbare Statuszeile (Claude Sonnet 4.6 | ctx 42% | $0.0312) ans Claude Code Terminal aus.
Die App überwacht ~/.claude/ — das Verzeichnis, nicht die Datei — via DispatchSource.makeFileSystemObjectSource mit den Event-Masken .write und .rename. Das Verzeichnis zu beobachten ist nötig, weil das atomare Rename-Ereignis nur am übergeordneten Verzeichnis ausgelöst wird, nicht am alten Pfad. Beim Event liest JSONDecoder mit keyDecodingStrategy = .convertFromSnakeCase die Datei und dekodiert sie in Swift-Modelle.
Implementierungsdetails
Einstiegspunkt — Die App ist ein Standard-Swift-Package mit SwiftUI @main und MenuBarExtra mit .menuBarExtraStyle(.window) (macOS 13+). Das Menüleisten-Label rendert ein sparkles-SF-Symbol neben einem Prozentstring in Monospace-Schrift; beide ändern die Farbe anhand der Schwellenwerte.
UsageWatcher — Ein @MainActor ObservableObject, das DispatchSource, Staleness-Timer und API-Polling-Timer verwaltet. Nach jedem erfolgreichen Datei-Lesen wird ein 300-Sekunden-Timer gesetzt. Feuert er, bevor neue Daten ankommen, kippt isStale auf true und das Icon dimmt. SwiftUIs @Published-Properties steuern alle Re-Renders automatisch.
TokenUsageView — Ein 260pt-breites SwiftUI-Popover. Der Ring wird mit Circle().trim(from:to:).stroke(style: StrokeStyle(lineCap: .round)) gezeichnet, um –90° rotiert und mit .easeOut(duration: 0.4) animiert. Fortschrittsbalken nutzen GeometryReader für proportionale Breiten. Die Reset-Zeit wird aus dem ISO-8601-Feld resetsAt geparst (mit Fallback für Sekundenbruchteile) und mit DateFormatter per lokalem jmm-Template formatiert.
LaunchAgent — Ein mitgeliefertes Plist registriert die App als macOS LaunchAgent für den automatischen Start beim Login. Entscheidend ist LimitLoadToSessionType = Aqua: Ohne diesen Key schlägt launchctl load mit „Input/output error 5" fehl, weil Menüleisten-Apps Zugriff auf den Windowserver benötigen, der nur in einer Aqua-GUI-Session existiert.
Designentscheidungen
Natives Swift statt Electron — Eine Menüleisten-App sollte unsichtbar sein, wenn man sie nicht braucht. Natives SwiftUI bedeutet keinen Hintergrund-Overhead und eine UI, die sich natürlich in macOS einfügt.
statusLine-Hook statt Polling — Claude Code stellt Session-Details genau dann bereit, wenn sie vorliegen — am Ende jeder Antwort. Ein API-Polling wäre langsamer und ungenauer.
Kein Daemon, kein Server — Die gesamte App ist ein einzelnes Binary, das Dateien liest und HTTP-Calls macht. Im Hintergrund läuft nichts außer der App selbst.
Verzeichnis beobachten statt Datei — Einen Dateipfad direkt zu beobachten verpasst atomare Rename-Ereignisse. Das übergeordnete Verzeichnis zu beobachten erfasst sie korrekt.
