Skip to content
Projects
AI inside4 min read

Claude Usage Tray

A Windows system tray app that shows live Claude.ai usage limits (session and weekly caps) at a glance, scraped from a hidden claude.ai browser view since Anthropic exposes no public usage API.

Role
Sole engineer
Year
2026
Status
prototype
desktoptaurirusttray-appwindowsAI

Overview

Claude Usage Tray is a small Windows desktop utility built with Tauri 2 and Rust that sits in the system tray and shows how much of the Claude.ai usage allowance has been consumed. It renders a donut-style tray icon (green, amber, or red depending on percent used) and a popup card with three progress bars: current session usage, weekly usage across all models, and weekly Sonnet-only usage, each with a "resets in" countdown.

Problem

Claude.ai's web UI exposes usage limits only on the /settings/usage page, and there is no public API for a desktop app to query them directly. A user who wants to keep an eye on how close they are to hitting a session or weekly cap has to keep a browser tab open and manually check. The goal was a lightweight, always-available tray indicator instead.

Approach

Since no official usage API exists, the app opens a hidden Tauri webview pointed at https://claude.ai/settings/usage, using the user's existing logged-in session, and injects a JavaScript content script (src-tauri/src/scraper.js) into that page. The script polls the DOM every 3 seconds (and on mutation) for elements labeled "Current session", "All models", and "Sonnet only", reading percentages from role="progressbar" ARIA attributes or inline width styles, with a regex fallback on "% used" text. It also detects login/SSO redirects to flag when the user needs to sign in.

Because the webview is a separate origin from the Rust host, the scraper cannot call back directly, so it smuggles data out through document.title, prefixed with CUSAGE: and a JSON payload. The Rust side polls the window title every 3 seconds, parses the JSON, and turns it into a typed UsageSnapshot, then broadcasts it to the tray icon, the overlay badge, and the popup window via a Tauri event (usage-updated).

Architecture

  • Rust/Tauri 2 backend (src-tauri/src/lib.rs) creates two webviews: a hidden main popup window and a claude webview loaded with the real claude.ai usage page.
  • webview_scraper.rs injects scraper.js on page load, polls the claude webview's title every 3 seconds, parses the CUSAGE: JSON payload, and maps it to a SnapshotStatus (Ok, LoginRequired, Loading, Error).
  • types.rs defines UsageSnapshot/BarSnapshot structs (shared behind an Arc<Mutex<Option<UsageSnapshot>>>) and builds the tray tooltip text.
  • icon.rs procedurally rasterizes a colored donut-ring PNG (via the image crate) for the tray icon and a solid-circle overlay badge, recoloring based on percent-used thresholds (green under 50%, amber under 80%, red above).
  • Tauri commands (get_snapshot, refresh_now, show_login, quit_app) let the frontend pull the latest snapshot, force a reload of the claude.ai page, surface the sign-in webview, or quit.
  • Frontend is plain HTML/CSS/vanilla JS (src/index.html, src/main.js, src/styles.css) rendering an SVG progress ring plus three labeled bar rows, driven by Tauri's invoke/listen IPC.
  • Tray menu (via tauri::tray) offers Show details, Sign in / refresh, Start with Windows (autostart toggle), and Quit; closing the main window hides it instead of exiting, keeping the app resident in the tray.
  • A first-run nudge shows the claude.ai webview if no usage snapshot arrives within 8 seconds, to prompt sign-in.

Tech stack

  • Rust, Tauri 2 (tray-icon, image-png features)
  • tauri-plugin-autostart, tauri-plugin-single-instance, tauri-plugin-notification, tauri-plugin-opener
  • serde / serde_json, tokio (async runtime), chrono, image (PNG raster)
  • Vanilla HTML/CSS/JavaScript frontend, Tauri's withGlobalTauri JS bridge
  • Bun as the package manager; Windows installer targets (NSIS, MSI) configured in tauri.conf.json

Engineering highlights

  • Solves the "no public API" problem by scraping the user's own already-authenticated claude.ai session in a hidden webview rather than requiring credentials or a separate auth flow.
  • Uses document.title as a cross-origin-safe one-way IPC channel between the untrusted external webview and the Rust host, avoiding custom protocol handlers or postMessage bridging across origins.
  • DOM scraping is resilient by design: it tries ARIA aria-valuenow/aria-valuemax first, falls back to inline width: % styles, and finally regex-matches visible "% used" text, with a MutationObserver to catch late-rendered content.
  • Tray icon and window overlay badge are rendered as raw pixel buffers at runtime (no external icon assets needed for the dynamic ring), with color thresholds giving an at-a-glance severity signal.
  • Explicit SnapshotStatus states (Loading, LoginRequired, Error, Ok) drive both the tray tooltip and the popup UI consistently, so failure modes (not signed in, scrape error) are visible rather than silently blank.

Lessons

  • Scraping a third-party web UI is inherently brittle: the label-based DOM heuristics will break if Claude.ai changes its usage page markup or copy, so this approach trades robustness for the fact that no official API exists.
  • Piggybacking on document.title for cross-origin messaging is a clever workaround but a fragile one; a future iteration would benefit from an official usage API or a more structured IPC channel if the webview APIs allow it.

Want to dig deeper?

Ask my AI agent anything about how this was built, what tradeoffs I made, or how it could fit your team.

Ask my AI →