A Logitech Logi Actions SDK plugin that shows your Claude Code subscription usage on device buttons and the Logi Options+ Actions Ring. It exposes two actions:
- 5-hour Limit — the rolling 5-hour usage window.
- Weekly Limit — the weekly (all-models) usage window.
Each button shows the usage percentage and a large, abbreviated reset
countdown (e.g. 3h, 45m, 2d) over a color-coded fill bar
(green → yellow → orange → red). No title is drawn on the button — the action
name already appears on hover in the Actions Ring. Credentials are read securely
from the macOS Keychain — no API key required.
This is a C# port of the Ulanzi Deck reference plugin; the usage-reading logic (Keychain → Anthropic API → unified rate-limit headers) is identical.
- Reads the Claude Code OAuth token from the Keychain item
Claude Code-credentials(security find-generic-password), parsingclaudeAiOauth.accessToken. - Sends a tiny throwaway request to
https://api.anthropic.com/v1/messages(claude-haiku-4-5,max_tokens: 1) with theanthropic-beta: oauth-2025-04-20header, viacurl(see the runtime note below). - Reads the response headers and renders them:
anthropic-ratelimit-unified-5h-utilization/-5h-resetanthropic-ratelimit-unified-7d-utilization/-7d-reset
- On
401/403it triggers a one-shotclaude -p … --max-budget-usd 0.01to refresh the OAuth token (aborts before spending tokens), then retries once.
A single shared poller refreshes every 5 minutes; pressing a button forces an immediate refresh.
- macOS with Logi Options+ installed (provides
LogiPluginService.appandPluginApi.dll). - .NET 8 SDK —
brew install dotnet@8(setDOTNET_ROOTif prompted:export DOTNET_ROOT=/opt/homebrew/opt/dotnet@8/libexec). Verify withdotnet --version. - A supported device: Logitech MX Creative Console, or an MX mouse/keyboard using the Actions Ring. (Loupedeck CT/Live and Razer Stream Controllers also work.)
- Claude Code logged in (
claudeCLI authenticated) so the Keychain item exists.
make build # dotnet build -c Release; writes the dev .link automatically
make restart # restarts LogiPluginService so it loads the new build
# or both at once:
make deploymake build writes a .link file (containing the absolute path to
ClaudeCodeUsage/Release) into
~/Library/Application Support/Logi/LogiPluginService/Plugins/, and copies the
package metadata next to the binaries. After make restart, open
Logi Options+ → Actions Ring (or your device's button config) and look for
the Claude Code category with the 5-hour Limit and Weekly Limit
actions.
Verify it loaded:
tail ~/Library/Application\ Support/Logi/LogiPluginService/Logs/plugin_logs/ClaudeCodeUsage.log
# expect: "Plugin 'ClaudeCodeUsage' version '1.0.0' loaded ... 2 dynamic actions loaded"make package # builds dist/ClaudeCodeUsage.lplug4A .lplug4 is just a zip with a metadata/ folder (the LoupedeckPackage.yaml
manifest + icon) and a bin/ folder. Double-clicking it installs the plugin in
Logi Options+ 5.0+. The package ships only ClaudeCodeUsage.dll (+ its
.deps.json) — PluginApi.dll, SkiaSharp and the other host assemblies are
provided by the Logi Plugin Service at runtime, so they are intentionally left
out (verified: the plugin loads from the minimal package).
For Marketplace submission, validate the package with the Logi Plugin Tool from the Developer page.
ClaudeCodeUsage/
ClaudeCodeUsage.csproj # net8.0, references PluginApi.dll, dev-install targets
src/
ClaudeCodeUsage.cs # Plugin entry point (universal, no target app)
ClaudeCodeUsageApplication.cs # REQUIRED ClientApplication subclass (see notes)
UsageCommandBase.cs # shared action behavior (poll subscribe + render)
FiveHourCommand.cs # "5-hour Limit" action
WeeklyCommand.cs # "Weekly Limit" action
UsageFetcher.cs # Keychain + curl + header parsing + CLI refresh
UsagePoller.cs # shared 5-min poller, raises Updated event
UsageData.cs # rate-limit snapshot model
Renderer.cs # BitmapBuilder drawing (bar, %, reset, states)
PluginLog.cs # SDK log helper
package/metadata/
LoupedeckPackage.yaml # plugin manifest
Icon256x256.png # package icon (regenerate: make icon)
tools/
generate_icon.py # pure-stdlib PNG generator for the package icon
These are non-obvious requirements of the Logi Plugin Service host. Getting any
of them wrong produces an unhelpful Cannot load plugin from … in
plugin_logs/ClaudeCodeUsage.log.
-
A
ClientApplicationsubclass is mandatory — even for a universal plugin (HasNoApplication => true). WithoutClaudeCodeUsageApplication, the host fails with "Cannot load plugin". This was the single hardest bug to find. -
No
System.Net.Http, noSystem.Text.Json. The host is a trimmed single-file .NET runtime that does not ship those assemblies; referencing them makes the plugin fail to load. We usecurlfor the request and a tiny manual parser for the keychain JSON instead.System.Diagnostics.Processis available (used forsecurity,curl, andclaude). -
The dev
.linkfile must contain an absolute path to the package root (the folder holdingbin/andmetadata/). The csproj writes it withWriteLinesToFile— the Windows-styleecho …\from the SDK sample produces a mangled relative path on macOS. -
Naming must be consistent: assembly name = root namespace last segment =
Pluginclass name (here allClaudeCodeUsagePlugin). A mismatch makes the host hang at "Started loading … type ''". Note these three are independent of the yamlnamefield (ClaudeCodeUsage) and the log file name (which followsname, not the assembly) — see gotcha #8. -
PluginApi.dllis copied into the build (no<Private>false</Private>), matching the official sample, so the plugin's.deps.jsonresolves correctly. -
"disabled as it had crashed before": once a plugin crashes, Logi Options+ remembers that plugin identity and refuses to load it again, even after a fix — and the flag survives service/Options+ restarts (it lives in an Options+ leveldb store). The pragmatic fix is to bump the plugin identity (assembly +
name); that's why this project isClaudeCodeUsagerather thanClaudeUsage. -
macOS only. The token lives in the macOS Keychain, and the Logi Actions Node.js SDK is currently Windows-only — hence the C# SDK (works on macOS via the host's bundled .NET runtime).
-
Marketplace validator: two opposing name rules. The contribute form inspects the
.lplug4(not its outer filename) and enforces, on two different yaml fields:pluginFileName(the DLL / assembly) must end withPlugin— e.g.ClaudeCodeUsagePlugin.dll. ("Plugin file name must end with 'Plugin'".)name(the marketplace identity) must match^[a-zA-Z0-9\-_.]+$and must NOT end withPlugin— e.g.ClaudeCodeUsage. ("Invalid name … name cannot end with 'Plugin'".)
So the assembly is
ClaudeCodeUsagePluginwhile the yamlnamestaysClaudeCodeUsage. This is the SDK template convention (name: MyTest+MyTestPlugin.dll); the original all-ClaudeCodeUsagesetup loaded locally but was rejected by the marketplace.
For Marketplace distribution, package as .lplug4 per the
Distributing the plugin
docs. (For distribution you can drop the copied PluginApi.dll from the
package.)