Overview
flare is two programs that work as one:
- The widget (
ui/, QML for Quickshell) draws the notch, its hover card, the usage panel and the settings page. It never reads a provider itself. flare(core/andcli/, Rust) does all the reading. The widget runs it, it prints what it found as JSON on stdout, and it exits.
That split is deliberate: the widget stays a thin view, every file and network read goes through one small binary you can run and inspect by hand (flare, flare doctor), and a slow provider never freezes the shell.
flare reads six tools: Claude Code, Codex, Cursor, OpenCode, Antigravity and Kiro. Each is read the way it keeps its own numbers: a rate limit becomes a ring, a pay-as-you-go tool shows today's tokens, a credit plan shows the credits left.
How it works
Everything flare does happens on your machine except one kind of request: in the default official mode, a provider's own usage endpoint is asked for your numbers, with the sign-in its CLI already keeps. In local mode even that is gone.
official mode.One refresh, step by step
- The poll timer fires, every
poll.interval_secs(30 s by default), and only while the notch is actually on screen, the usage panel is open, or a shortcut brought it in. A notch tucked away inhoverorshortcutmode reads nothing. - The widget runs
flare --provider all. It finds the binary beside the module (target/release/flare), onPATH, in~/.local/bin, in~/.cargo/bin, or atflare.binary_path. - Every enabled provider is read at once, each on its own thread, so one slow endpoint never holds up the rest.
- Each provider checks its own pace. The widget may refresh every 30 s, but network reads keep their own rhythm: Claude at most every minute; Codex, Cursor and Kiro every five. Between those, the last reading is reused. A server that said "slow down" is not asked again before its deadline, which survives restarts.
- The provider reads: its own files on disk, and in official mode its usage endpoint.
- flare records the reading, its history and the session log in
~/.local/state/flare. - flare prints JSON and exits. Nothing stays running between refreshes.
- The widget draws the rings, the card and the panels from that JSON.
The usage panel's week of hourly use comes from a second, heavier command, flare activity, which runs only while the panel is open and once every five minutes at most.
What it reads and sends
Per provider, in the default official mode. In local mode the "sent" column is empty for everyone: flare never opens a network connection.
| Provider | Read on your machine | Sent in official mode, and where |
|---|---|---|
| Claude Code | The OAuth token in ~/.claude/.credentials.json; the email in ~/.claude.json; reply logs in ~/.claude/projects; live sessions in ~/.claude/sessions; the status line capture | The token as a bearer header, plus anthropic-beta: oauth-2025-04-20→ api.anthropic.com/api/oauth/usage |
| Codex | The session in ~/.codex/auth.json; rollout logs in ~/.codex/sessions | The access token as a bearer header and the ChatGPT-Account-Id it names→ chatgpt.com/backend-api/wham/usage |
| Cursor | The editor's session from ~/.config/Cursor/User/globalStorage/state.vscdb | That session, as the editor itself sends it — only after you say yes once→ cursor.com/api/usage-summary |
| OpenCode | Its database, ~/.local/share/opencode/opencode.db; provider names (not keys) from its auth.json | Nothing |
| Antigravity | The status line capture; conversation databases in ~/.gemini/antigravity-cli/conversations | Nothing |
| Kiro | The sign-in kiro-cli keeps in ~/.local/share/kiro-cli/data.sqlite3; sessions in ~/.kiro/sessions/cli | The sign-in as a bearer header→ q.<region>.amazonaws.com/getUsageLimits |
What never leaves your machine: your prompts, your replies, your code, your project names, your sessions, your history, your config. The endpoints above answer with percentages, reset times and plan names; that is all flare asks for. There is no telemetry, no analytics and no update check.
What it costs to run
Measured on 23 September 2026, with a heavy week of Claude Code logs. Your numbers will differ with your log sizes and hardware, but not by an order of magnitude.
- Machine
- HP Victus laptop
- CPU
- Intel Core i5-12500H, 12 cores, 16 threads
- Graphics
- Intel Iris Xe · NVIDIA GeForce RTX 3050 Ti Laptop
- Memory
- 16 GB
- Storage
- 512 GB NVMe SSD
- System
- CachyOS, Linux 7.2.6
- Desktop
- Hyprland 0.56.2 · Quickshell 0.3.1 · Qt 6.11
Memory
flare run (peak)17 MBflare watch5 MBThe views were measured with software rendering in a bare Quickshell, so the 74 MB they add includes loading Qt Quick itself. Inside a shell that already draws a bar or a dock, most of that is shared and the real cost is lower. flare itself holds no memory between refreshes: each run exits.
Processor
flare --provider all, wall timeTucked away in hover or shortcut mode, the timer stops and the cost drops to zero. flare activity (the usage panel's week) takes about 0.3 s and runs only while that panel is open.
Network
Those are ceilings, reached only while the widget keeps refreshing. A 429 backs off from a minute, doubling to fifteen, and honours the server's Retry-After. local mode makes no requests at all.
Security and privacy
- Credentials are borrowed, read-only. flare reads the sign-in each CLI already keeps. It never writes, copies, refreshes or stores a token, and never prints one:
flare doctordescribes a token by its length. - An expired token is never sent. A 401 or 403 re-reads the credential once, in case the CLI just rotated it, and otherwise waits for you to open the CLI.
- Renewing a sign-in is left to the tool itself. Shortly before Claude's token expires, flare runs
claude -pwith no prompt and no input, which renews and exits; its output is discarded. Kiro's is renewed withkiro-cli whoami. Pin either withclaude.binary_path/kiro.binary_pathinstead of trusting the first match onPATH. - Cursor asks first. Reading Cursor means borrowing a live session cookie from the editor's private state. The widget asks once; declining keeps Cursor out of official mode until
data.cursor_consentchanges. - State files hold no secrets, only readings, history, pacing and the session log. They are written
0600through a temporary file and a rename, so a crash never leaves half a file. - Log trees are read defensively. Symlinks are never followed, recursion stops at depth 12, collection stops at 20 000 files, and a damaged line is reported without losing the rest of the file.
- The status line hooks are passive. They make no network request, never write Claude Code's or agy's own files, and a failed capture never delays or breaks your status line.
- Jumping to a session reads only what it needs: the session's own
/proc/<pid>entries, and from its environment only kitty's window variables. - Nothing phones home. No telemetry, no analytics, no update check. The only outbound requests are the usage endpoints in the table above.
Install
Requires Quickshell 0.3+ and Qt 6.6+, on Hyprland (other Wayland compositors run the widget, without jumping to a session's window).
$ git clone https://github.com/lunanoir21/flare-notch$ cd flare-notch$ ./install.sh
install.sh:
- builds
flarewith Cargo if Rust 1.85+ is installed; otherwise downloads the latest release build (x86_64, glibc 2.39+); - puts it in
~/.local/bin(setFLARE_BIN_DIRto change that); - adds flare to your app launcher:
~/.local/share/applications/flare.desktop, its icon, andflare-settingsbeside the binary; - runs
flare doctor, so you see at once what it found.
By hand, cargo build --release is enough: the widget finds the binary it was built next to. The prebuilt binary on its own: flare-x86_64-linux.tar.gz (sha256).
Running it
On its own
$ quickshell -p /path/to/flare-notch/ui
Inside the shell you already have
import "path/to/flare-notch/ui" as Flare
ShellRoot {
Flare.FlareHost {}
}
At login
In hyprland.conf:
exec-once = quickshell -p /path/to/flare-notch/ui
From your app launcher
Open flare from rofi, wofi, fuzzel or any launcher that reads .desktop files. It opens the settings page in whichever running Quickshell has flare loaded, and starts the widget on its own first if none does.
Using the widget
Three looks — notch.style
- classic
- Codenotch's notch: a black body on the left or right edge, one ring per provider.
- aura
- One provider at a time, large and tinted with its colour; the others wait below as small logos.
next/prevstep through them. - compact
- A thin strip on the top or bottom edge that opens into a panel with a row per provider, on click or on hover (
compact.open_on).
Meeting the edge — notch.mount
- bridge
- Welded to the edge with inverse rounded corners. The default.
- floating
- A rounded panel held
notch.gappixels off the edge. - flush
- A strip along the whole edge that flares into the screen at both ends.
When it shows — notch.reveal
- always
- Always on screen.
- hover
- Tucked past the edge; slides in after the pointer rests there for
reveal_delay_ms, outhide_delay_msafter it leaves. - shortcut
- Tucked away until
qs ipc call flare toggleVisible, which you bind to a key.
Reading a ring
A ring fills clockwise from 12 o'clock with the share of the provider's headline limit used. Under it, notch.label shows the percentage, the time until the reset, or both. A provider with no limit (OpenCode) shows today's tokens instead, and a credit plan (Kiro) shows the credits left.
With theme.ring_color = "monochrome" rings are drawn in contrast only, with one accent reserved for a critical state; "provider" paints each ring in its own aura colour. The hover card's bars go green, orange and red as a limit fills. A reading that could not be refreshed is dimmed, never invented.
With the pointer
- Hover a ring
- Opens its card: every limit with its reset time, the plan, the login, and the open sessions.
- Click a ring
- Opens that provider's own usage page (Claude, Codex, Cursor).
- The card's arrow
- Opens the usage panel on that provider.
- Right-click
- Opens the settings page.
- Drag
- Moves it along its edge; the offset is saved.
Open sessions
The card lists the Claude Code sessions running right now, folded under a Sessions header, each with its name, project, age and state: working, waiting on you (with what it is waiting for), or idle. Clicking a row brings its terminal to the front.
The list comes from the records Claude Code itself writes to ~/.claude/sessions/<pid>.json. A record counts only while its process is alive and still the one that wrote it (the start time in /proc/<pid>/stat must match), and, on Hyprland, while its terminal still has a window.
In kitty, the jump can also select the session's tab or split. That needs kitty's remote control on a socket, in kitty.conf:
allow_remote_control socket-only
listen_on unix:@kitty
The same jump from a terminal: flare focus <pid>. sessions.show = false hides the list entirely.
The usage panel
Open it from the arrow on a card, or with qs ipc call flare usage <provider>. A deck of provider cards runs across the top; pick one with the pointer or ← / →, and Esc closes the panel. For the one picked:
- When you use it: the week of its longest limit hour by hour as a heatmap (the last seven days for a provider with no limit), with the busiest hours, the quietest day and the reset.
- Today's sessions on a timeline, closed ones included; click an open Claude Code one to jump to it.
- Limits: each one, how much of its time has gone, and where today's pace takes it.
- How the longest limit filled over its period, with today's pace dashed ahead.
- The last seven days and the hours of the day.
- The models that took the most this week.
- This week's sessions as a table.
Token counts are read from each provider's own logs, every reply counted once (Claude Code repeats a reply's usage on each content block, and a resumed session copies old replies; neither is double counted). Kiro counts credits instead. usage.all_providers = true also lists providers switched off in the widget.
More than one login
Two Claude Code logins are two rings, and so are two Codex ones. A folder ~/.claude-<name> or ~/.codex-<name> that holds a sign-in is found on its own and becomes claude:<name>, with the name's initial on its ring. Sign in to one by pointing the CLI at it once:
$ CLAUDE_CONFIG_DIR=~/.claude-work claude # then /login$ CODEX_HOME=~/.codex-work codex login
A login kept anywhere else goes in the config:
[[account]]
provider = "claude" # or "codex"
name = "work" # lower-case letters, digits, - and _
home = "~/work/.claude" # what CLAUDE_CONFIG_DIR / CODEX_HOME is set to
color = "#E0A458" # optional: aura's colour for this login
The default login keeps the plain id (claude), so a config and history from before carry on unchanged. Every login is read, renewed, recorded and reported on its own; providers.claude = false hides every Claude login at once, and providers.accounts_off hides them one by one. Everything that takes a provider takes a login too: card claude:work, flare --provider claude:work.
Notifications
flare watch is the one long-running process: the widget starts it while any notification is switched on, and it exits when they all are off or the widget goes away. It rescans sessions every two seconds (a few small files, no child processes) and usage at the widget's pace, never more than once a minute. It sends, with notify-send:
- notify.waiting
- A session stops and waits on you. The notification's action jumps to it.
- notify.limit
- A limit reaches
notify.limit_atpercent (50–100), once per limit period. - notify.reset
- A limit you had been using resets.
Starting the watcher never replays what was already true: the first reading of anything is only a baseline.
Providers
Claude Code
Official: api.anthropic.com/api/oauth/usage with Claude Code's own OAuth token; the session and weekly limits, and any model-scoped weekly ones. Renewed with claude -p a few minutes before expiry, once per token, at most every ten minutes. Where the endpoint cannot answer, a status line capture younger than half an hour stands in.
Local: the status line capture only.
Tokens: ~/.claude/projects/**/*.jsonl. Pace: 1 min.
Codex
Official: chatgpt.com/backend-api/wham/usage with the session in ~/.codex/auth.json; the primary and secondary windows, and Spark and code review limits under their own headings. The token is never refreshed: a 401 means open Codex once.
Local and fallback: the limits Codex wrote into its newest rollout log, current for five minutes.
Pace: 5 min.
Cursor
Official: cursor.com/api/usage-summary with the editor's own session, after a one-time yes.
Local: Cursor keeps no usage on disk, so nothing.
Pace: 5 min.
OpenCode
Runs on your own API keys, so there is no limit to show: the ring shows today's tokens. Everything comes from its local database, in both modes; sessions and models from its history.
Antigravity
agy keeps each model group's quota in memory and hands it only to its status line command, so the quotas come from the status line capture. Tokens, models and sessions come from its per-conversation databases. No network, in either mode.
Kiro
Official: getUsageLimits with the sign-in kiro-cli keeps; the month's credits, shown as credits left, and when they refill. The sign-in lasts an hour and is renewed shortly before by running kiro-cli whoami.
Local: credits per request from ~/.kiro/sessions/cli, with no allowance. Pace: 5 min.
Status line captures
Claude Code and agy hand their live limits only to their status line command. The hooks in hooks/ save that payload to ~/.local/state/flare and then run your real status line unchanged.
Claude Code
Needed for local mode; in official mode it only stands in when the endpoint is down. In ~/.claude/settings.json, put the hook in front of the command you already use:
"statusLine": {
"type": "command",
"command": "/path/to/flare-notch/hooks/claude-statusline-capture.sh npx -y @owloops/claude-powerline@latest"
}
The hook runs, in order: its own arguments as the command; $FLARE_STATUSLINE_DELEGATE; statusline-command.sh in Claude Code's directory. With none of them it captures and prints nothing. Each login gets its own capture file, so every login's status line can point at the same hook.
Antigravity
In ~/.gemini/antigravity-cli/settings.json, keeping agy's own line:
"statusLine": {
"command": "/path/to/flare-notch/hooks/agy-statusline-capture.sh",
"enabled": true,
"stack_with_default": true
}
Configuration reference
Everything lives in ~/.config/flare/config.toml. Every key is optional; delete a line to get its default back. The settings page and flare config set write the same file, keep its comments, and refuse a value the key cannot hold. The widget picks up a saved change within a second. flare config init writes a commented file holding every default.
| Key | Values | Default | What it does |
|---|---|---|---|
| [data] | |||
data.mode | official, local | official | Where numbers come from. local never touches the network. |
data.cursor_consent | unset, granted, declined | unset | Your answer to reading Cursor's live session. |
| [theme] and [ui] | |||
theme.mode | black, white, auto | black | auto follows the desktop's light/dark preference. |
theme.ring_color | monochrome, provider | monochrome | Rings in contrast only, or in each provider's aura colour. |
ui.language | auto, en, tr | auto | auto follows LC_ALL, LC_MESSAGES or LANG. |
| [notch] | |||
notch.style | classic, aura, compact | classic | The look. |
notch.mount | bridge, floating, flush | bridge | How it meets the edge. |
notch.gap | 0–64 | 8 | Floating only: pixels between the panel and the edge. |
notch.reveal | always, hover, shortcut | always | When it is on screen. |
notch.reveal_delay_ms | 0–5000 | 80 | Hover: rest on the edge this long before it slides in. |
notch.hide_delay_ms | 0–5000 | 400 | Hover: slide away this long after the pointer leaves. |
notch.edge | left, right | left | Edge for classic and aura. |
notch.offset | pixels | 0 | Along the edge from the centre; positive moves it down. |
notch.scale | 0.5–2.0 | 1.0 | Size multiplier. |
notch.screen | output name | "" | As hyprctl monitors names it. Empty: every screen. |
notch.label | percent, time, both | percent | What sits under each ring. |
| [compact] | |||
compact.edge | top, bottom | top | Edge for the compact strip. |
compact.offset | pixels | 0 | Along the edge from the centre; positive moves it right. |
compact.open_on | click, hover | click | How the panel opens. |
| [providers] | |||
providers.claude … .kiro | true, false | true | One switch per provider, for all its logins. |
providers.order | list of ids | claude, codex, opencode, cursor, antigravity, kiro | Drawing order, and the order aura steps through. Logins by id; one left out follows its provider. |
providers.find_accounts | true, false | true | Find logins in ~/.claude-<name> and ~/.codex-<name>. |
providers.accounts_off | list of ids | [] | Logins switched off one by one, e.g. claude:work. |
| [aura], [sessions], [usage] | |||
aura.claude … .kiro | #RRGGBB | each provider's colour | Aura's tint, and the ring colour with ring_color = "provider". |
sessions.show | true, false | true | The card's session list. |
usage.all_providers | true, false | false | Also list, and read, switched-off providers in the usage panel. |
| [notify] | |||
notify.waiting | true, false | true | A session waits on you. |
notify.limit | true, false | true | A limit reaches limit_at. |
notify.limit_at | 50–100 | 90 | The threshold, in percent. |
notify.reset | true, false | true | A used limit resets. |
| [poll], [scan] | |||
poll.interval_secs | ≥ 5 | 30 | Widget refresh. Network reads keep their own slower pace. |
scan.window_days | ≥ 1 | 1 | Days of local logs counted toward token and credit totals. |
| Programs | |||
claude.binary_path | path | PATH lookup | The claude used to renew the token. |
kiro.binary_path | path | PATH lookup | The kiro-cli used to renew the sign-in. |
codex.binary_path, opencode.binary_path | path | PATH lookup | Pin a program that is not on PATH. |
flare.binary_path | path | search | For a compositor that starts Quickshell without your login shell's PATH. |
| [[account]] | |||
provider, name, home, color | see More than one login | — | A login that lives somewhere find_accounts does not look. |
List keys take either form: flare config set providers.order claude,codex,opencode or '["claude", "codex"]'. A file that fails to parse or validate falls back to defaults and says why in flare doctor and on the settings page; a typo never stops the widget from drawing.
IPC reference
flare listens on Quickshell IPC as the target flare.
| Call | Does |
|---|---|
next, prev | Step aura to the next or previous provider. |
toggle | Open or close the compact panel. |
toggleVisible, show, hide | Bring the widget in or tuck it away (hover and shortcut reveal). |
toggleSessions | Open or fold the card's session list. |
card <provider> | Open a provider's card without the pointer, bringing the notch in; again to close. |
usage <provider> | Open the usage panel on a provider; again to close. |
style classic|aura|compact | Switch the look (saved to the config). |
settings | Open or close the settings page. |
openSettings | Open the settings page, leaving it open if it already is. |
refresh | Read now, skipping each provider's pace (a server's retry deadline still holds). |
For Hyprland, with flare inside the shell at ~/.config/quickshell/shell.qml:
bind = SUPER, right, exec, qs ipc call flare next
bind = SUPER, left, exec, qs ipc call flare prev
bind = SUPER, U, exec, qs ipc call flare toggle
bind = SUPER SHIFT, U, exec, qs ipc call flare toggleVisible
bind = SUPER, C, exec, qs ipc call flare card claude
Add -p /path/to/shell.qml after qs when your shell lives elsewhere, or --pid <pid> when more than one Quickshell runs (qs list --all).
Command line
In JSON mode stdout carries JSON and nothing else; diagnostics go to stderr. doctor is the one plain-text command.
| Command | Does |
|---|---|
flare | Read every provider and print a JSON array. The widget runs this. |
flare --provider <id> | One provider or login (claude, claude:work, …) as a JSON object. |
flare --refresh | Read now instead of waiting for each provider's pace. |
flare doctor | Where flare looked and what it found, per provider and login. Never prints a secret. |
flare activity --provider <id> | Use per hour over the last eight days, with the models, as JSON. |
flare focus <pid> | Bring a live session's terminal to the front (Hyprland). |
flare watch | Stay running and send notifications, as [notify] sets them. |
flare config path | Print the config file's path. |
flare config init [--force] | Write a commented config with every default. |
flare config get | The effective config, the drawing order and every login, as JSON. |
flare config set <key> <value> | Set one key, keeping the file's comments. |
The JSON output
Each provider or login is one object. The keys are stable from 1.0.0 on: new ones may appear, existing ones keep their meaning.
| Key | Meaning |
|---|---|
provider | The id: claude, or claude:work for another login. |
status | See the statuses below. |
note | One line on why the status is what it is. |
source | official or local: where these windows came from. |
account, plan | Whose readings these are (a label, never used to sign in) and the plan. |
metered | Whether the provider has a rate limit at all. |
hidden | Switched off in the widget, read only for the usage panel. |
windows | Each limit: id, label, group, used (0–1), window_minutes, resets_at, resets_in_secs, reset_elapsed, and for a credit limit amount (used, limit, unit). |
headline | The window the ring draws, by id. |
fetched_at, backoff_until | When the reading was taken; no network read before this time. |
tokens_today, credits_today | Over scan.window_days. |
cost_today_usd, cost_is_estimated | Where a provider reports cost; an estimate is never a bill. |
error | Category-prefixed detail, e.g. parse error: …. |
sessions | Open now: pid, name, project, state, waiting_for, started_at. |
history | Per window id, [unix seconds, fraction used] over the window's length. |
session_log | Sessions seen over the last week, closed ones included. |
Statuses
- ok
- A reading taken just now, or recent enough to stand (five minutes).
- stale
- The last good reading, kept because a fresh one could not be taken. Drawn dimmed.
- needs_auth
- No usable sign-in. Open the provider's CLI once.
- needs_consent
- Cursor, before you have answered the one-time question.
- backoff
- Asked to slow down, with no earlier reading to show.
- error
- A fresh reading failed and there is no earlier one.
- none
- Present, but nothing metered or recorded yet.
- absent
- Not installed here; no cell is drawn.
Files
| Path | What |
|---|---|
~/.config/flare/config.toml | Your settings. |
~/.local/state/flare/<id>.json | The last reading and its pacing (last attempt, back-off, renewal bookkeeping). No token. |
~/.local/state/flare/history-<id>.json | How each limit filled. |
~/.local/state/flare/sessions-<id>.json | The session log, kept eight days. |
~/.local/state/flare/claude-statusline*.json, agy-statusline.json | Status line captures, one per login. |
~/.local/bin/flare, flare-settings | The binary and the launcher's helper. |
~/.local/share/applications/flare.desktop | The launcher entry; its icon sits under ~/.local/share/icons/hicolor/scalable/apps/. |
Everything under ~/.local/state/flare is safe to delete: flare starts over with an empty history.
Troubleshooting
Start with flare doctor: it prints, per provider and login, the files it looked for, whether a sign-in is usable, where renewal would run, the last capture, and the reading it gets.
- The notch shows nothing at all
- The widget cannot find
flare. Checkqs log; setflare.binary_path, which a compositor-started Quickshell needs when~/.local/binis not on its PATH. - A provider has no ring
- It is not installed (status
absent), or switched off in the settings page. - A ring is dimmed
- The reading is stale: the endpoint failed or is backing off. The card's note says which.
- "Credential expired" on Claude
- flare could not renew the token. Run
claudeonce; if renewal keeps failing, pinclaude.binary_path. - Claude shows nothing in local mode
- Point Claude Code's status line at the capture hook (above).
- A second login is not found
- The folder needs a sign-in in it (
.credentials.jsonfor Claude,auth.jsonfor Codex), and its name must be lower-case letters, digits,-or_. - Clicking a session does nothing
- Jumping needs Hyprland (
hyprctl); selecting kitty's tab needs its remote control socket. - No notifications
notify-sendmust be installed and a notification daemon running; check that[notify]has something switched on.
Uninstall
rm ~/.local/bin/flare ~/.local/bin/flare-settings
rm ~/.local/share/applications/flare.desktop
rm ~/.local/share/icons/hicolor/scalable/apps/flare.svg
rm -r ~/.config/flare ~/.local/state/flare
Then remove the exec-once line or the FlareHost import, and the hook from any statusLine you pointed at it. flare changed nothing else.
Development
core/- The providers, config, sessions, history and pacing (
flare-core). cli/- The
flarecommand. ui/- The QML widget. Every visible string lives in
Strings.qml, in English and Turkish. hooks/- The status line captures.
tools/shots/- Makes every screenshot from a made-up world, drawn by flare's own QML with no compositor:
tools/shots/shots.sh.
$ cargo test$ cargo clippy -- -D warnings
CI runs both on every push. A release is a ## x.y.z section in CHANGELOG.md, the same version in Cargo.toml, and a pushed vX.Y.Z tag: the release workflow checks the three agree, tests, builds on Ubuntu 24.04 and publishes the binary and its checksum.