Open Source
2026-03

ClaudeStatusBar

Native macOS-Menüleisten-App, die Claude Code Planauslastung und Session-Kosten in Echtzeit anzeigt.

SwiftSwiftUImacOSShell
ClaudeStatusBar preview

Motivation

Beim Arbeiten mit Claude Code wollte ich auf einen Blick sehen, wie viel meines 5-Stunden-Plans ich verbraucht habe und was die aktuelle Session kostet, ohne dafür den Workflow zu unterbrechen. Eine native Menüleisten-App ist der richtige Ort für solche Statusdaten.

Was die App zeigt

Das Menüleisten-Icon zeigt die 5-Stunden-Planauslastung mit einem -Präfix und farbkodiertem Status: grün unter 50 %, gelb von 50–79 %, rot ab 80 %. Gehen mehr als 5 Minuten ohne Claude-Antwort vorbei, dimmt das Icon ab, ein stilles Signal, dass die Daten veraltet sein könnten.

Ein Klick ö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. Einen separaten Login braucht es nicht: Der OAuth-Token liegt direkt im macOS-Schlüsselbund, wo Claude Code ihn unter dem Service-Key Claude Code-credentials ablegt. Der Request verwendet einen Bearer-Authorization-Header sowie 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, den einzigen 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 in ~/.claude/session_usage.json, über eine PID-suffixierte Temp-Datei und mv. Das ist bewusst so: 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 Terminal aus.

Die App überwacht ~/.claude/, das Verzeichnis und nicht die Datei direkt, via DispatchSource.makeFileSystemObjectSource mit den Event-Masken .write und .rename. Das ist nötig, weil Kernel-Events bei atomaren Umbenennungen nur am übergeordneten Verzeichnis ausgelöst werden, nicht am alten Pfad. Beim Event liest JSONDecoder mit keyDecodingStrategy = .convertFromSnakeCase die Datei und dekodiert sie in Swift-Modelle.

Implementierungsdetails

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 ist ein @MainActor ObservableObject, das DispatchSource, Staleness-Timer und API-Polling-Timer verwaltet. Nach jedem erfolgreichen Dateilesen wird ein 300-Sekunden-Timer gesetzt. Feuert er, bevor neue Daten ankommen, kippt isStale auf true und das Icon dimmt ab. SwiftUIs @Published-Properties steuern alle Re-Renders automatisch.

TokenUsageView ist 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 über ein lokales jmm-Template formatiert.

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

Ich hätte die App auch mit Electron bauen können, der Aufwand wäre gering gewesen. Aber 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.

Für die Session-Details habe ich bewusst auf Polling verzichtet. Claude Code stellt diese Daten ohnehin am Ende jeder Antwort bereit, der statusLine-Hook löst also genau zum richtigen Zeitpunkt aus. Warum dann regelmäßig eine API abfragen?

Konsequenterweise läuft im Hintergrund auch nichts weiter: kein Daemon, kein Server. Die gesamte App ist ein einzelnes Binary, das Dateien liest und HTTP-Anfragen stellt.

Beim Dateisystem-Monitoring gibt es eine nicht offensichtliche Besonderheit: Einen Dateipfad direkt zu beobachten verpasst atomare Rename-Ereignisse. Das übergeordnete Verzeichnis zu beobachten erfasst sie korrekt, ein kleines Detail mit echtem Effekt.