claude-o-meter uses a daemon/client architecture to efficiently provide usage metrics to status bar integrations without repeatedly querying the Claude CLI.
flowchart TB
subgraph "Systemd User Service"
daemon["claude-o-meter daemon"]
file[("~/.cache/claude-o-meter/usage.json")]
daemon -->|"writes periodically"| file
end
subgraph "Data Source"
claude["claude /usage"]
end
subgraph "Clients"
hyprpanel["HyprPanel Custom Module"]
other["Other Status Bars / Scripts"]
end
daemon -->|"executes in PTY"| claude
claude -->|"stdout with ANSI"| daemon
hyprpanel -->|"claude-o-meter hyprpanel"| file
other -->|"claude-o-meter hyprpanel"| file
| Component | Description |
|---|---|
| Daemon | Systemd user service running claude-o-meter daemon. Polls claude /usage at configurable intervals and writes JSON to a cache file. |
| Cache File | JSON file at ~/.cache/claude-o-meter/usage.json containing the latest UsageSnapshot. |
| Client Mode | claude-o-meter hyprpanel reads the cache file and formats output for status bar consumption. |
┌─────────────────────────────────────────────────────────────────┐
│ claude-o-meter query → One-shot query, JSON to stdout │
│ claude-o-meter daemon → Background service, writes to file │
│ claude-o-meter hyprpanel → Client reads file, formats output │
└─────────────────────────────────────────────────────────────────┘
- Daemon spawns
claude /usagein a PTY (viascriptcommand) - Daemon monitors output for completion patterns (
% usedor% left) - Daemon parses output, strips ANSI codes, extracts metrics
- Daemon writes
UsageSnapshotJSON to cache file - Client (e.g., HyprPanel module) invokes
claude-o-meter hyprpanel - Client mode reads cache file, transforms to
HyprPanelOutputformat - Status bar displays the formatted metrics
sequenceDiagram
participant HP as HyprPanel
participant CM as claude-o-meter hyprpanel
participant CF as Cache File
participant D as Daemon
participant CLI as claude /usage
Note over D,CLI: Background (every 60s)
loop Daemon Poll Cycle
D->>CLI: Execute in PTY
CLI-->>D: ANSI output with usage data
D->>D: Parse & strip ANSI
D->>CF: Write UsageSnapshot JSON
end
Note over HP,CF: Foreground (every 6s)
loop HyprPanel Poll Cycle
HP->>CM: Invoke binary
CM->>CF: Read JSON
CF-->>CM: UsageSnapshot
CM->>CM: Transform to HyprPanelOutput
CM-->>HP: JSON {text, alt, class, tooltip}
HP->>HP: Update status bar widget
end
- Efficiency: The Claude CLI takes 2-3 seconds to return results. Running it on every status bar refresh would cause delays.
- Decoupling: Status bars poll frequently (every few seconds). The daemon polls infrequently (every 60s by default).
- Reliability: If the daemon fails, clients show stale data rather than hanging.
The daemon also schedules an automatic refresh 60 seconds after the earliest quota reset time. This ensures the status bar displays 0% usage promptly after quotas renew, without waiting for the next poll interval.
The daemon can expose a D-Bus service on the session bus, enabling external tools to trigger immediate usage refreshes without waiting for the next poll interval.
flowchart TB
subgraph "Systemd User Service"
daemon["claude-o-meter daemon"]
dbus["D-Bus Service<br/>com.github.MartinLoeper.ClaudeOMeter"]
file[("~/.cache/claude-o-meter/usage.json")]
daemon -->|"writes periodically"| file
daemon -.->|"exposes"| dbus
end
subgraph "Data Source"
claude["claude /usage"]
end
subgraph "Clients"
hyprpanel["HyprPanel Custom Module"]
other["Other Status Bars / Scripts"]
end
subgraph "External Triggers"
hook["Claude Code Hook"]
script["Custom Script"]
end
daemon -->|"executes in PTY"| claude
claude -->|"stdout with ANSI"| daemon
hyprpanel -->|"claude-o-meter hyprpanel"| file
other -->|"claude-o-meter hyprpanel"| file
hook -->|"dbus-send RefreshNow"| dbus
script -->|"dbus-send RefreshNow"| dbus
dbus -->|"triggers immediate"| daemon
When an external tool triggers a refresh via D-Bus, the daemon immediately queries Claude usage instead of waiting for the next poll cycle:
sequenceDiagram
participant Hook as Claude Code Hook
participant DBus as D-Bus Service
participant D as Daemon
participant CLI as claude /usage
participant CF as Cache File
participant HP as HyprPanel
Note over D: Daemon running with --dbus flag
Hook->>DBus: dbus-send RefreshNow
DBus->>D: Signal refresh
D->>D: Reset poll timer
D->>CLI: Execute in PTY
CLI-->>D: ANSI output with usage data
D->>D: Parse & strip ANSI
D->>CF: Write UsageSnapshot JSON
Note over HP: Next HyprPanel poll (every 6s)
HP->>CF: Read JSON
CF-->>HP: Updated UsageSnapshot
HP->>HP: Display fresh metrics
| Property | Value |
|---|---|
| Service Name | com.github.MartinLoeper.ClaudeOMeter |
| Object Path | /com/github/MartinLoeper/ClaudeOMeter |
| Interface | com.github.MartinLoeper.ClaudeOMeter |
| Method | RefreshNow() |
The primary use case for D-Bus integration is triggering a refresh after Claude Code completes a request. This provides immediate feedback in the status bar about updated usage:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Claude Code │────▶│ PostToolUse │────▶│ dbus-send │
│ completes │ │ Hook fires │ │ RefreshNow │
└─────────────────┘ └─────────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ HyprPanel │◀────│ Cache file │◀────│ Daemon queries │
│ shows update │ │ updated │ │ immediately │
└─────────────────┘ └─────────────────┘ └─────────────────┘
When enableClaudeCodeHooks = true in the Home Manager module, a Claude Code plugin is installed that automatically triggers a usage refresh when Claude conversations end. This provides real-time status bar updates without relying on frequent polling.
flowchart TB
subgraph "Claude Code"
cc["Claude Code CLI"]
plugin["claude-o-meter-refresh plugin"]
hooks["hooks/hooks.json"]
plugin --> hooks
end
subgraph "Systemd User Service"
daemon["claude-o-meter daemon"]
dbus["D-Bus Service<br/>com.github.MartinLoeper.ClaudeOMeter"]
file[("~/.cache/claude-o-meter.json")]
daemon -->|"writes periodically"| file
daemon -.->|"exposes"| dbus
end
subgraph "Status Bar"
hyprpanel["HyprPanel"]
end
cc -->|"Stop event"| hooks
hooks -->|"claude-o-meter refresh"| dbus
dbus -->|"triggers immediate query"| daemon
hyprpanel -->|"reads"| file
- Plugin Installation: The Home Manager module creates a Claude Code marketplace at
~/.claude/claude-o-meter-plugins/containing theclaude-o-meter-refreshplugin - Settings Registration: The plugin is registered in Claude Code's
settings.jsonviaextraKnownMarketplacesandenabledPlugins - Stop Hook: When a Claude conversation ends, the plugin's Stop hook executes
claude-o-meter refresh - D-Bus Refresh: The refresh command sends a D-Bus signal to the daemon, triggering an immediate usage query
- Status Update: The daemon writes fresh metrics to the cache file, which the status bar picks up on its next poll
sequenceDiagram
participant User
participant CC as Claude Code
participant Plugin as Stop Hook
participant DBus as D-Bus
participant Daemon as claude-o-meter daemon
participant CLI as claude /usage
participant File as Cache File
participant HP as HyprPanel
User->>CC: Sends message
CC->>CC: Processes request
CC->>User: Returns response
CC->>Plugin: Fires Stop event
Plugin->>DBus: claude-o-meter refresh
DBus->>Daemon: RefreshNow()
Daemon->>CLI: Execute in PTY
CLI-->>Daemon: Usage data
Daemon->>File: Write updated JSON
Note over HP: Next poll (every 6s)
HP->>File: Read JSON
File-->>HP: Fresh UsageSnapshot
HP->>HP: Display updated metrics
~/.claude/claude-o-meter-plugins/
├── .claude-plugin/
│ └── marketplace.json # Marketplace metadata
└── claude-o-meter-refresh/
├── package.json # Plugin metadata
└── hooks/
└── hooks.json # Stop hook configuration
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/nix/store/.../bin/claude-o-meter refresh",
"timeout": 7
}
]
}
]
}
}The command uses the full Nix store path to ensure it works regardless of the user's PATH.
| Hook | When it fires | Use case |
|---|---|---|
PostToolUse |
After each tool call | Too frequent, would spam refreshes |
Stop |
When conversation ends | Perfect timing for usage update |
The Stop hook fires once per conversation, providing an accurate usage snapshot after Claude has finished processing.
When hooks are enabled, the default polling interval changes from 60 seconds to 5 minutes:
| Mode | Interval | Reason |
|---|---|---|
| Polling only | 60s | Frequent updates needed |
| With hooks | 5m | Hooks provide real-time updates; polling is just a fallback |
Users can override this with the interval option if needed.
The daemon implements a two-phase retry strategy to handle different failure scenarios:
When the daemon starts before the network is available (common with systemd services at boot), it enters startup mode with aggressive 5-second retries:
stateDiagram-v2
[*] --> StartupMode: daemon starts
StartupMode --> StartupMode: query fails (5s retry)
StartupMode --> NormalMode: query succeeds
state StartupMode {
[*] --> InitialQuery
InitialQuery --> RetryIn5s: failure
RetryIn5s --> InitialQuery: 5s elapsed
InitialQuery --> Exit: success
}
After the first successful query, the daemon switches to normal mode:
| State | Retry Interval | Description |
|---|---|---|
| Success | Normal interval (60s/5m) | Regular polling continues |
| First failure | 1 minute | Switches to retry mode |
| Continuing failure | 1 minute | Maintains retry interval |
| Recovery | Normal interval | Resumes normal polling |
stateDiagram-v2
[*] --> Success: startup complete
Success --> Success: query succeeds
Success --> Failure: query fails
Failure --> Failure: query fails (1m retry)
Failure --> Success: query succeeds
| Phase | Condition | Interval |
|---|---|---|
| Startup | Before first success | 5 seconds |
| Normal | Successful queries | 60s (polling) or 5m (with hooks) |
| Retry | After failure in normal mode | 1 minute |
This ensures:
- Fast recovery at boot: When the network becomes available, the daemon recovers within 5 seconds
- Reasonable retry in normal operation: Transient failures don't cause excessive load
When claude-o-meter hyprpanel is invoked before the daemon has written its first cache file, the client blocks until the file exists. This ensures the status bar receives valid data rather than a transient "loading" state.
sequenceDiagram
participant HP as HyprPanel
participant CM as claude-o-meter hyprpanel
participant FS as Filesystem
participant D as Daemon
HP->>CM: Invoke binary
CM->>FS: Check file exists?
FS-->>CM: No
loop Poll every 500ms
CM->>FS: Check file exists?
FS-->>CM: No
end
Note over D: Daemon completes first query
D->>FS: Write cache file
CM->>FS: Check file exists?
FS-->>CM: Yes
CM->>FS: Read JSON
FS-->>CM: UsageSnapshot
CM-->>HP: JSON {text, alt, class, tooltip}
Behavior Summary:
| Scenario | Client Behavior |
|---|---|
| Cache file exists | Read immediately, return formatted output |
| Cache file missing | Block, poll every 500ms until file appears |
Rationale: Blocking prevents the status bar from displaying a flickering "loading" indicator on every poll cycle during startup. The daemon typically writes its first result within 2-3 seconds, so the delay is minimal.
The tool detects the Claude account type by parsing the header line from claude /usage output.
The CLI outputs a header line in the format:
· claude <type> · user@email.com
Three regex patterns match this format (case-insensitive):
| Pattern | Matches | Account Type |
|---|---|---|
(?i)·\s*claude\s+pro |
· claude pro |
pro |
(?i)·\s*claude\s+max |
· claude max |
max |
(?i)·\s*claude\s+api |
· claude api |
api |
Located in detectAccountType() (main.go):
- Check patterns in order: pro → max → api
- Return the first matching account type
- If no pattern matches, return
unknown
The function does not use fallback heuristics. If the header format is unrecognized (e.g., AWS Bedrock, Google Vertex, or future integrations), it returns unknown rather than guessing.
const (
AccountTypePro = "pro" // Claude Pro subscription
AccountTypeMax = "max" // Claude Max subscription
AccountTypeAPI = "api" // API access
AccountTypeUnknown = "unknown" // Unrecognized format
)The daemon can send desktop notifications via D-Bus when usage exceeds a configurable threshold.
| Flag | Description |
|---|---|
--notify-threshold |
Percentage threshold (0-100). Notification triggers when session usage >= threshold |
--notify-timeout |
Display timeout (e.g., "5s"). 0 = never auto-close, unset = server default |
--notify-icon |
Path to notification icon (PNG/SVG) |
services.claude-o-meter = {
enable = true;
notifyThreshold = 80; # Notify at 80% usage
notifyTimeout = "5s"; # Auto-close after 5 seconds
};- Threshold Check: After each daemon refresh, session usage is compared against the threshold
- One-Shot Notification: Notification is sent once when threshold is exceeded
- Reset on Drop: Notification state resets when usage drops below threshold, allowing a new notification on the next crossing
stateDiagram-v2
[*] --> BelowThreshold
BelowThreshold --> AboveThreshold: usage >= threshold
AboveThreshold --> BelowThreshold: usage < threshold
state AboveThreshold {
[*] --> SendNotification
SendNotification --> NotificationSent: success
NotificationSent --> NotificationSent: usage still >= threshold
}
Notifications are sent via the org.freedesktop.Notifications.Notify D-Bus method. The daemon connects to the session bus and calls the notification service provided by the desktop environment (e.g., mako, dunst, GNOME Shell).
| Parameter | Value |
|---|---|
app_name |
claude-o-meter |
app_icon |
Path to Claude icon PNG |
summary |
Claude Usage High |
body |
Session usage at X% (threshold: Y%) |