# CLAUDE.md Working brief for Claude Code sessions in this repository. Factual, no marketing, under ~150 lines. ## Project Claude Usage Tray is a minimalist Windows 11 system-tray app that shows the current Claude usage limits at a glance: a live percentage rendered into the tray icon, and a dark flyout with every usage window, reset countdowns, plan tier, burn rate, history charts and local token/cost analytics derived from Claude Code session logs. It is an unofficial community tool, not affiliated with or endorsed by Anthropic. It reads the Claude Code OAuth token only to call the same usage endpoint Claude Code itself uses, and it never writes to `~/.claude`. ## Commands (verified on Windows 11, SDK 9.0.311) ```bash dotnet restore dotnet build --configuration Release # warnings are errors in Release dotnet test --configuration Release --no-build dotnet format --verify-no-changes # CI fails on any diff dotnet run --project src/ClaudeTrayApp dotnet run --project src/ClaudeTrayApp -- --probe # one live fetch, redacted summary in the log, exit 0 or 2 dotnet run --project src/ClaudeTrayApp -- --render-icons out/icons # tray icon contact sheets (PNG) for a legibility check dotnet run --project src/ClaudeTrayApp -- --capture-flyout out/flyout.png # opens the flyout after the first poll, screenshots it, exits dotnet run --project src/ClaudeTrayApp -- --capture-settings out/settings.png # opens the settings window, screenshots it, exits dotnet publish src/ClaudeTrayApp -c Release -r win-x64 -o artifacts/publish/win-x64 -p:Version=0.1.0 dotnet publish src/ClaudeTrayApp -c Release -r win-arm64 -o artifacts/publish/win-arm64 -p:Version=0.1.0 git tag -a v0.1.0 -m "Claude Usage Tray 0.1.0" && git push origin v0.1.0 # release: see the Release section gh run watch --exit-status # CI or release run; ids from `gh run list` gh release view v0.1.0 # both zips and SHA256SUMS.txt attached ``` Run from the repo root: `global.json` asks for SDK 9.0.300 or a newer 9.0 feature band (`latestFeature`, no previews) because dev machines may default to a newer preview SDK. Publish output is a single self-contained, ReadyToRun exe (about 65 MB on x64, 61 MB on Arm64) plus `pricing.json`; `artifacts/` is git-ignored. CI runs a win-x64 single-file publish so the single-file analysers, which are errors in Release, fail on `main` rather than at tag time (IL3000 once broke a release that CI had passed); win-arm64 is still published only by the release workflow. ## Layout - `src/ClaudeTrayApp/` WPF app: composition root (`App.xaml.cs`), `Themes/` (`Theme.xaml` tokens, `Controls.xaml` styles, `Palette.Dark.xaml`, `Palette.Light.xaml`), `Theming/` (Windows theme follower), `Tray/` (icon renderer, controller, converter, flyout placement maths), `Views/` (`FlyoutWindow`, `SettingsWindow`), `Controls/` (`RingArc`, `Sparkline`, `LineChart`, `BarChart`: hand-drawn, theme-brushed), `Charts/` (pure chart data builders, loader, palette), `Startup/` (autostart Run entry behind `IAutostartEntry`, the first-run default that turns it on, single instance, settings coordinator, settings window host), `Assets/` (`app.ico`, generated by `tools/make-icon.py`, our own design), `ViewModels/`, `Hosting/`, `Interop/`, `Diagnostics/` (screenshot aid). The only project that references WPF. - `tests/ClaudeTrayApp.Tests/` WPF-side tests (net9.0-windows): tray state, tooltip, renderer pixels on an STA thread, chart data builders, charts view model, settings coordinator, update check service, autostart default, icon conversion, geometry. - `src/ClaudeTrayApp.Core/` `Domain/` (snapshot, windows, humaniser), `Credentials/`, `Providers/` (OAuth endpoint, parser), `Polling/` (state machine, poller), `Cache/`, `Analytics/` (JSONL line parser, incremental scanner, provider with watcher, calculator), `Storage/` (SQLite store: events, scan offsets, history), `Pricing/` (`pricing.json` loader, table, reloadable provider), `History/` (recorder, retention), `Aggregation/`, `Account/`, `Settings/` (settings model and hot-reloading store), `Notifications/` (threshold notifier), `Updates/` (release check and its persisted state), `Security/` (redactor), `Diagnostics/`, `ClaudeCode/` (CLI locator: PATH, then the build bundled with Claude Desktop; headless process runner; version detection). `Credentials/` also holds the refresh nudge that starts the CLI when the token has expired. No UI references, ever. - `tests/ClaudeTrayApp.Core.Tests/` xunit.v3 + Shouldly + NSubstitute. Fixture files with fake tokens and synthetic sessions only. - `docs/` `architecture.md`, `data-sources.md`, `packaging.md` (code signing and winget), `todo.md` (open work and deliberately deferred items), `diagrams/` (Mermaid sources), `screenshots/`. - `packaging/winget//` winget manifests, validated with `winget validate`; see `docs/packaging.md`. Submission to winget-pkgs is the owner's call and has not happened yet. - `.github/` `workflows/ci.yml` (build, test, format, win-x64 publish check), `workflows/release.yml` (tag `v*`: publish both RIDs, zip, `SHA256SUMS.txt`, GitHub release), `release/` (the zip's `README.txt` and the release-notes template), Dependabot, issue templates. - `Directory.Build.props` shared MSBuild settings. `Directory.Packages.props` every package version. `global.json` SDK pin. ## Architecture - Providers produce a `UsageSnapshot` (percentages, resets, tier) or local analytics (tokens, cost, burn rate). They know about transport, never about UI. - `UsageAggregator` merges providers: OAuth data is authoritative for percentages, JSONL data for token and cost analytics. Every field carries its source. - `SqliteStore` (`history.db`) holds usage events deduplicated by message id and request id, per-file scan offsets, and the snapshot time series; `HistoryRecorder` appends every fresh snapshot and prunes past the retention window. Charts read only from it. - `JsonlScanner` reads only bytes appended since the last scan (530 files, 29k events, about 4 s on first run, under 100 ms after); `LocalAnalyticsProvider` rescans on a debounced file watcher and a 5-minute safety net; `AnalyticsCalculator` derives today, top projects and the 5-hour block on demand, pricing from `pricing.json`. - ViewModels (CommunityToolkit.Mvvm source generators) adapt aggregated data for binding; views bind and draw, never compute. - Charts are drawn by three small `FrameworkElement`s in `Controls/` from theme brushes; there is no charting package. `Charts/ChartDataBuilder` turns history rows into series (peak-preserving downsampling, projection clipped at the reset, daily totals with the long tail as "other") and is pure; `ChartDataLoader` runs the SQLite queries on a thread-pool thread; `ChartsViewModel` hands the controls points and brushes. Every chart carries a text summary as its automation name and caption. - `SettingsStore` owns `%APPDATA%\ClaudeTrayApp\settings.json`: defaults written on first run, atomic saves, a debounced watcher for hot reload, and a file that does not parse is left alone and reported (`LastError`). `SettingsCoordinator` pushes changes into the poller (`UpdateOptions` re-times a wait in progress), history retention, the scanner's age cutoff, `PricingProvider` and the theme override; view models subscribe to the store themselves, and the flyout writes back its two toggles (email mask, chart range). `ThresholdNotifier` fires each threshold once per window and period; `TrayIconViewModel` turns alerts into Windows notifications. `SingleInstance` (named mutex plus event) makes a second launch surface the first instance's flyout. - Dependency direction is one way: `ClaudeTrayApp` references `ClaudeTrayApp.Core`. Core never references WPF; `CoreArchitectureTests` fails the build if it does. ## Hard rules - Never log, print, persist or commit a token. Redact `Authorization` headers and anything token-shaped before logging. Tests use fake tokens. - Never write to `~/.claude` or `CLAUDE_CONFIG_DIR`. Read-only, always. The app never refreshes the OAuth token and never calls the OAuth token endpoint. On expiry it starts the Claude Code CLI headless once (`ClaudeCliRefreshNudge`, throttled, 10 s timeout, no MCP servers, no session file, output discarded) so the CLI refreshes its own file, reads the file back, and otherwise tells the user to run the CLI. Only the CLI ever rewrites `.credentials.json`; Claude Desktop and the Claude Code it hosts never do. - Never poll the usage endpoint below the 180 s floor. Default 300 s. On 429 back off exponentially, capped at 30 min, and keep serving the cached snapshot marked stale. - Never fabricate a percentage when the endpoint is unavailable. Show "percentages unavailable" plus local analytics. - Pricing is never hardcoded: `pricing.json` next to the binary, overridable in settings, effective date shown in the UI. Unknown model ids render "cost unknown", never a number. - Only permissive-licensed dependencies (MIT, Apache-2.0, BSD). Ask before adding any package not already in `Directory.Packages.props`. LiveChartsCore was dropped in M6: its WPF view pulls OpenTK and SkiaSharp.Views.WPF built for .NET Framework (NU1701) and the charts here are simple enough to draw by hand. - No telemetry of any kind. Two destinations only: `api.anthropic.com` for usage, and `api.github.com` for the weekly update check, which sends nothing about the user and is one setting away from off. Adding a third needs the user's say-so. ## Conventions - Conventional Commits. Small, reviewable commits. Never commit generated artifacts, user data or anything from `~/.claude`. - Nullable enabled everywhere; `TreatWarningsAsErrors` in Release; analyzers at `latest-recommended`; `.editorconfig` is enforced by `dotnet format`. - MVVM via CommunityToolkit.Mvvm source generators (`[ObservableProperty]`, `[RelayCommand]`). No hand-rolled `INotifyPropertyChanged`. - Theme tokens only: colours, brushes and fonts come from `Theme.xaml`. No literals in views. Light and dark palettes both ship. The `Ok` status colour is the Claude accent (terracotta); amber and red take over as a window fills, and every status is also written as text. - Core is a library: every `await` uses `ConfigureAwait(false)` (CA2007 is a warning under `src/ClaudeTrayApp.Core`). The app starts and stops the host off the UI thread; view models marshal to the dispatcher themselves. - The flyout is hidden, never closed; it is warmed up off-screen at start so a real open takes under 150 ms. Chart data is loaded once at warm-up and then only while the flyout is visible; loads never run on the UI thread. The window re-places itself when its height changes and never exceeds the work area (the body scrolls instead). - Tests are required for anything in Core. Test names use underscores (CA1707 is off under `tests/`). - Accessibility is part of done: keyboard navigation, `AutomationProperties` on every control, contrast at least 4.5:1, never colour alone. - Settings apply the moment they change; the settings window holds no unsaved state and only `SettingsStore` writes settings.json. Start with Windows lives in the registry, not in the file: the Run entry itself, plus a `StartupDefaultApplied` flag under `HKCU\Software\ClaudeUsageTray`. The first launch turns it on and sets the flag, so it is applied exactly once and a user who switches it off stays off, even if settings.json is lost. The flag is read as set when the registry cannot be read, because forcing a startup entry back on is worse than not creating one. ## Release - The version comes from the tag: `vX.Y.Z[-pre]` becomes `dotnet publish -p:Version=X.Y.Z[-pre]`, so the assembly and file version, the About dialog and the first log line all report the tag's version. `VersionPrefix` in `Directory.Build.props` is only what local builds report; bump it together with the CHANGELOG so `dotnet run` and the next tag agree. - Procedure: `main` green (`gh run watch`), `CHANGELOG.md` has a `## [X.Y.Z] - date` section (the workflow copies it into the release body) and its compare links updated, `dotnet publish` passes locally, then `git tag -a vX.Y.Z -m "Claude Usage Tray X.Y.Z"` and `git push origin vX.Y.Z`. Watch the run with `gh run watch --exit-status` and check `gh release view vX.Y.Z` for both zips and `SHA256SUMS.txt`. - The workflow builds and tests like CI, publishes win-x64 and win-arm64, zips each publish folder with `LICENSE.txt` and `.github/release/README.txt` as `ClaudeUsageTray-X.Y.Z-win-.zip`, writes `SHA256SUMS.txt` (`sha256sum` format, lowercase) and creates the release from `.github/release/notes-template.md` (disclaimer, checksums, CHANGELOG section, GitHub's generated notes) with `gh` from the runner under `contents: write`. A tag that is not `v..[-prerelease]` fails the first step; a prerelease suffix marks the release as a pre-release. - A failed run can be re-run, or the tag deleted and re-created after a fix on `main`; the workflow replaces the assets and notes of an existing release instead of failing. Never delete a tag someone may already have fetched for anything but a broken release. - The exe is unsigned; SmartScreen may prompt once. Code signing and winget are out of scope until asked for. ## Known fragilities - `GET https://api.anthropic.com/api/oauth/usage` is undocumented. Its shape can change and it rate-limits aggressively. The `User-Agent: claude-code/` header matters; without it requests land in a throttled bucket. - Credential location varies. Today: `%USERPROFILE%\.claude\.credentials.json`, key `claudeAiOauth.accessToken`, honouring `CLAUDE_CONFIG_DIR`. The same file holds an unrelated `mcpOAuth` block; parse only `claudeAiOauth`. - Access tokens live about eight hours and only the Claude Code CLI refreshes them, when it starts. Claude Desktop's chat uses its own web session and the Claude Code it hosts runs with a host-supplied token (`CLAUDE_CODE_SDK_HAS_HOST_AUTH_REFRESH=1`), so a Desktop-only user's file goes stale until the CLI runs. Expect 401 and show a re-authenticate hint instead of failing. - The refresh nudge relies on a Claude Code implementation detail: `claude -p --input-format stream-json --output-format stream-json --verbose --strict-mcp-config --mcp-config --no-session-persistence` with stdin closed runs the "background startup prefetches" that refresh an expired token, then exits at end of input without a prompt (2.1.276, 2026-09-18). `--bare` skips those prefetches. If a release changes this, the log shows nudges whose expiry never moves and the app degrades to the message; the command is one constant in `ClaudeCliRefreshNudge`. The nudge strips inherited `CLAUDECODE`, `CLAUDE_CODE_*` and API-key variables, otherwise a tray app started from inside a hosted Claude Code session would spawn a CLI that defers to the host and never refreshes. - JSONL session logs are not a contract. One API response is written as several `assistant` lines sharing `message.id` and `requestId`; dedupe them or costs are overcounted several-fold. - The Claude Code CLI on PATH and the desktop app's bundled Claude Code can report different versions. - Extra-usage amounts arrive in minor units (`decimal_places`, or `amount_minor` plus `exponent` in the `spend` block). Codename windows such as `nimbus_quill` appear alongside the documented ones; they render like any unknown key once they carry a value or a reset time, and stay hidden while inactive (0 %, no reset) unless `showInactiveWindows` is on. Documented keys are always shown. - A 200 answer carrying no usage windows is reported as `UsageFetchStatus.SchemaChanged`, not as success with an empty snapshot: the endpoint is undocumented, so "the format may have changed" is the honest reading. An account that genuinely has no windows would see the same message. - The endpoint's own `limits[].severity` said `warning` at 86 %, below this app's 70/90 status thresholds; the mapping from `limits[].kind` to window keys is unverified, so `limits` is not parsed yet. - H.NotifyIcon.Wpf 2.4 dropped net9.0-windows; stay on 2.3.x (Dependabot is told so). Its `IconSource` path rejects `RenderTargetBitmap`, so the tray icon is converted to a `System.Drawing.Icon` by `IconConverter` and set through `TaskbarIcon.Icon`. - The tray icon is rendered at the system DPI (16/20/24/32 px). Per-monitor DPI for the taskbar is not tracked; a DPI change triggers a redraw through `SystemEvents.DisplaySettingsChanged`. - The Run entry points at `Environment.ProcessPath`; after the executable moves, Start with Windows reads as off until toggled again. Notifications use H.NotifyIcon's `ShowNotification`, which Windows may suppress under Focus assist. - `history.db` can be damaged. On 2026-09-10 a tree pointed at pages past the end of the file and every scan and chart load failed with `database disk image is malformed`, right after two `--capture-*` runs had the live file open beside the running app. `SqliteStore` now runs `PRAGMA quick_check` once per process; on a definite verdict (SQLITE_CORRUPT, SQLITE_NOTADB, or a failed check, never merely busy) it moves the file and its `-wal`/`-shm` to `history.corrupt-