neuron-explorer
Steering-first neuron explorer for Qwen3.5-4B: push every MLP neuron, keep every rollout, browse the results
Monitor Claude Code token usage and context window status via a tray app or terminal dashboard
AI summary · README
Developer toolsRust · TypeScriptGitHub @HackPointSelf-installOwner unconfirmed
Paste into Claude or ChatGPT for installation instructions suited to your environment.
Have you tried it? One tap is enough. No sign-in needed.
Loading the full README. The saved excerpt is shown below.
See the whole truth about your Claude Code tokens — context fill, real cost, and verifiable optimization.
Lumen is a tray/menu-bar app and terminal dashboard for Claude Code users,
running on macOS, Linux and Windows.
It watches your session files locally and surfaces things Claude Code doesn't show in its own interface:
No account, and no telemetry: none of the above leaves your machine. Lumen makes two
network requests in total, both described under Security & privacy
— a once-a-day check for a new release, and filing a fault report, which only happens
when you ask for it.Read the full README ↗See the whole truth about your Claude Code tokens — context fill, real cost, and verifiable optimization.
Lumen is a tray/menu-bar app and terminal dashboard for Claude Code users, running on macOS, Linux and Windows. It watches your session files locally and surfaces things Claude Code doesn't show in its own interface:
| Signal | Where it appears |
|---|---|
| What Lumen saved — tokens and dollars, net, this week and all time | Home, at the top (from 1.9.0) · lumen · tray |
| Context fill — tokens used / window size, live | Menu-bar icon · tray popover · Details screen gauge |
| Compaction warning — amber at 80%, red at 95% | Tray popover · OS notification · Details screen |
| Session cost — input + output + cache tokens, priced | Tray popover · Settings screen, beside the spend alerts |
| Caching savings — what Claude Code's cache actually saved | Optimizer screen (labeled "reported by Claude Code") |
| Lumen optimized — tokens Lumen's tools kept out of context, with the token ratio and per-tool breakdowns | Optimizer screen · Details screen |
| What to do — files read in full again and again, and what to change | Why screen, shown in the window's tabs only while there is something to act on |
No account, and no telemetry: none of the above leaves your machine. Lumen makes two network requests in total, both described under Security & privacy — a once-a-day check for a new release, and filing a fault report, which only happens when you ask for it.
| Platform | GUI | CLI (lumen) | Notes |
|---|---|---|---|
| macOS (Apple Silicon) | ✅ .dmg + Homebrew cask | ✅ Homebrew | Primary development platform |
| Linux (x86_64) | ✅ AppImage + .deb | ✅ Homebrew + tarball | Requires WebKitGTK 4.1 — see below |
| Windows (x86_64) | ✅ .exe installer | ✅ .zip | GUI installer is un-signed |
| macOS (Intel) | ❌ | ❌ | Build on request — open an issue |
| Linux (arm64) | ❌ | ❌ | No artifact built yet |
Requirements
The GUI is not code-signed or notarized on any platform. Per-platform workarounds are documented in the install sections below.
brew tap HackPoint/tap
brew trust --cask HackPoint/tap/lumen-app # required for casks outside homebrew/cask
brew install --cask lumen-app
Without the trust step Homebrew refuses with "Refusing to load cask … from
untrusted tap". That applies to every third-party cask, not just this one.
The cask is
lumen-app, notlumen. Homebrew already ships an unrelatedlumen(a screen-brightness tool), so that token would install the wrong application. The installed app is still Lumen.app and the terminal command is stilllumen.Upgrading from 1.1.3 or earlier, which used the
lumentoken? Move across once:brew uninstall --cask lumen && brew install --cask lumen-appYour data in the Lumen data directory is untouched.
Homebrew clears the quarantine flag automatically. Lumen opens normally with no "damaged" dialog. The app is still un-notarized (proper Apple Developer ID signing is planned); Homebrew handles the flag for you.
After install, launch Lumen from Spotlight or /Applications/Lumen.app.
Download Lumen_1.1.0_aarch64.dmg from the Releases page
Open the .dmg and drag Lumen to your Applications folder
⚠️ Before opening — run this once in Terminal:
xattr -dr com.apple.quarantine /Applications/Lumen.app
macOS will say "Lumen is damaged and can't be opened." — this is not actual damage. It is the standard block for un-notarized apps. The command removes the quarantine flag; double-click Lumen normally after.
Alternative: try to open it once (blocked), then System Settings → Privacy & Security → Open Anyway.
The permanent fix is Apple Developer ID + notarization ($99/yr) — planned for a later release. Until then, use Homebrew (above) or the xattr command.
On first launch, a Setup screen appears — click through to register the MCP server and hooks. Restart Claude Code after setup.
Two packages are published per release. Both bundle the daemon, MCP server and CLI as sidecars, so there is nothing else to install.
AppImage — works on any distribution, no root needed:
VERSION=1.1.0
curl -LO "https://github.com/HackPoint/lumen/releases/download/v${VERSION}/Lumen-${VERSION}-x86_64.AppImage"
chmod +x "Lumen-${VERSION}-x86_64.AppImage"
./"Lumen-${VERSION}-x86_64.AppImage"
.deb — for Debian, Ubuntu and derivatives:
VERSION=1.1.0
curl -LO "https://github.com/HackPoint/lumen/releases/download/v${VERSION}/lumen_${VERSION}_amd64.deb"
sudo apt install "./lumen_${VERSION}_amd64.deb" # apt resolves the dependencies
Then launch Lumen from your desktop's application menu — the package installs a
.desktop entry. From a shell the binary is Lumen (capital L, matching the product
name).
apt install ./file.deb is deliberate over dpkg -i: the package declares
WebKitGTK and app-indicator dependencies, and only apt will pull them in.
If WebKitGTK is missing, install it directly:
# Debian / Ubuntu
sudo apt install libwebkit2gtk-4.1-0 libgtk-3-0 libayatana-appindicator3-1
# Fedora
sudo dnf install webkit2gtk4.1 gtk3 libappindicator-gtk3
# Arch
sudo pacman -S webkit2gtk-4.1 gtk3 libappindicator-gtk3
Lumen lives in the system tray, and Linux tray support depends on your desktop:
| Desktop | Works out of the box? |
|---|---|
| KDE Plasma, Cinnamon, Budgie, XFCE | ✅ Yes |
| GNOME | ⚠️ Needs the AppIndicator extension — GNOME removed tray icons |
| Sway / i3 / wlroots | ⚠️ Needs a tray-capable bar (Waybar with tray, or i3status-rust) |
With no tray host running, the app still works and the main window still opens —
you just lose the menu-bar icon. If the window does not appear on Wayland, force
X11: GDK_BACKEND=x11 ./Lumen-1.1.0-x86_64.AppImage.
Data lives in ~/.local/share/io.hackpoint.lumen/ (following the XDG layout),
not in ~/Library as on macOS.
Screenshot needed —
docs/assets/screenshot-setup.pngCapture: the Setup screen showing the four step rows with checkmarks.
The first time Lumen launches, a Setup screen appears automatically. It does three things:
~/.claude/lumen/ — a read-intercept script and a token meterlumen MCP server globally in ~/.claude.json~/.claude/settings.jsonSetup is non-destructive: it merges Lumen's entries alongside any existing MCP servers and hooks you have. Nothing existing is removed or overwritten.
Uninstall (also on the Setup screen) reverses all three steps cleanly.
After setup, restart Claude Code for the MCP server and hooks to activate.
Lumen lives in the menu bar / system tray, not in the Dock or taskbar — top-right on macOS, wherever your desktop puts indicators on Linux and Windows.
| Action | Result |
|---|---|
| Left-click the tray icon | Quick popover — context gauge, current cost, mode badge |
| Right-click → "Open Lumen" | Full window with Home, Optimizer, Details and Settings tabs, and Why when it has something to say |
Home answers three questions: how much Lumen saved, whether it is working, and what to do.
Every other figure is on Details (the context gauge, the token ratio, the breakdowns) or
Settings (spend and its alert thresholds, the Repeat reads switch, billing mode, Report a
fault, the version). After Setup, Home asks once how you pay for Claude Code — a
subscription (Pro/Max) or the API, per token — because that decides what a saving is
worth to you; change it later on Settings. The answer is kept as billing_mode in
lumen_settings.json, beside repeat_reads.
The tray icon pulses with ring animations; its color reflects context fill status (green → amber at 80% → red at 95%).
lumen)The lumen terminal command prints one headline line; lumen dash is the live
dashboard — the same data as the GUI, rendered in your terminal.
Correction. Earlier versions of this document said hooks fire only in the Claude Code CLI and not in the VS Code extension, and built a "Full mode vs Soft mode" distinction on it. That is false: hooks fire in the VS Code extension too. Measured directly — 108 built-in
Readevents were recorded by the PostToolUse hook during a single session whoseentrypointwasclaude-vscode, and in the same session every file over the line threshold was intercepted and redirected. Interception does not depend on which client you use.
Via Homebrew:
brew tap HackPoint/tap && brew install HackPoint/tap/lumen-cli
Works on macOS (Apple Silicon) and Linux (x86_64). On Windows, or without
Homebrew, download the archive for your platform from the
Releases page and put lumen on your PATH.
Via the app:
Open Lumen → click "Install CLI" — this symlinks the bundled binary into your PATH.
lumen # one line: what Lumen saved this week and all time, net, and whether it is
# working; names lumen report and lumen dash
lumen dash # live terminal dashboard; --no-anim turns the ring animation off
# press q or Ctrl-C to quit; piped, it prints one line and exits
lumen report # the savings in detail: each period with its band, how it is measured, the
# calls that gained and lost most (--json: the whole summary, per call)
lumen faults # render a fault report, and file it with --yes (see Filing a fault report)
lumen doctor # everything a bug report needs, in one paste
lumen show # ask a running Lumen to show its window (macOS)
Until 1.9.0 bare lumen was the dashboard and lumen report was the fault report. Both old
spellings still work: lumen --no-anim opens the dashboard, and lumen report with any of the
fault report's flags (--dry-run, --yes, --repo, --include-source, --faults) runs
lumen faults. Each says so in one line on stderr, as does --no-anim given to any command
but lumen dash, where it does nothing.
The dashboard reads from the same local SQLite database as the GUI. It works best with the Lumen daemon running (the GUI starts the daemon automatically); if the daemon is not running, the CLI falls back to polling the database directly.
Note: The
lumenCLI is a monitoring and dashboard tool, not a replacement for Claude Code. Keep Claude Code running normally —lumenwatches it.
Screenshot needed —
docs/assets/screenshot-numbers.pngCapture: the main window with the context tab visible and all tiles labeled.
Every number in Lumen comes from your local session files or is computed locally. Here is what each one means, where it comes from, and any honesty caveat.
The top of Home is the savings headline: tokens saved and dollars saved, net, this week and all
time, both in bold, from the savings engine's summary of your own ledger. The tokens are Claude's,
estimated from Lumen's cl100k_base counts by the calibration (below), so they read "≈", with
the band on the ⓘ. The dollar figure is the net on record, a floor: a gain reads "at least $X
saved, net", and a loss, whose floor is the most it can be, "net loss of at most $X" (or "net
loss under half a cent"), in red. On a subscription,
or before you have said how you pay, the dollars are "at API prices", since a plan is not billed
per token, after the share of tokens Lumen kept out of context ("≈ 92% fewer tokens from file
and tool reads this week"). What may be worth more (modelled) and the calls with tokens and no
dollars (unpriced) are tags beside the figure, never in it. Under it: whether Lumen is working
(a loss whose band reaches a gain says Lumen may have cost more than it saved), and the one
thing to do, with its step (Run Setup, or lumen doctor). "How it's measured", from the
headline and from Details, has every rule, the price table and its date, the calibration (its
date, the Claude Code version it was measured through, and the ratio each group of rows was
converted at), each figure's band and count, and the newest 200 calls. The figures are
refreshed every minute and when the window gets focus; if a refresh fails, the last figures
stay, marked as not up to date. Bare lumen and the tray say the same figures in the same words.
The context gauge and its window selector are on Details (they were the top of the Context tab until 1.9.0). The session cost tiles, the usage report and the spend alert thresholds are on Settings.
The large ring shows how full your current context window is. The fill is calculated
from the most recent turn's cache_read token count divided by the inferred window size.
| Color | Meaning |
|---|---|
| Green | Below 80% — plenty of room |
| Amber | 80–95% — compaction is approaching |
| Red | Above 95% — compaction is imminent; Claude Code will soon summarize prior context |
Honesty caveat: the window size is inferred from the model name (200K for most models, 500K or 1M for models that support it). Lumen cannot read your actual plan tier — your real limit may differ. The inferred tiers are listed in Known limitations.
X — tokens currently filling the window (from the latest turn's token counts).
Y — the inferred window size for your current model.
These are the raw numbers behind the gauge ring.
The model identifier from the most recent turn in the active session (e.g.,
claude-sonnet-4-6, claude-opus-4-8). Lumen reads this from the JSONL session
file — it reflects what Claude Code is actually using, not a preference or setting.
The running dollar total for the active session, computed locally from:
Each model's tokens are priced at that model's list price, from Anthropic's published
pricing page; the table is PRICES in crates/lumen-core/src/rates.rs, with the date it
was last checked. A session that mixes models, such as a subagent on Haiku inside an Opus
session, has each part priced at its own rate. Until 1.9.0 every token was priced at
Opus-class rates whatever the model, which overstated a Sonnet session about 1.7 times and
a Haiku one five times.
No figure is shown as more complete than it is. One that is a floor, because part of it
could not be priced or was priced low, is tagged at least (in lumen dash it reads
at least $…), and hovering the tag says why:
<synthetic>, an id from Bedrock, a model newer
than the table, or one Lumen cannot price, such as Claude Haiku 5.5, whose rate depends
on each request's prompt length) adds no dollars. Its tokens are still counted. With
nothing priced at all the figure shows — and is tagged no price; before the app has
loaded the price table, the tag says so instead.usage.speed), and a fast turn on Claude Opus 5.5, Opus 5 or Opus 4.8 costs the
fast-mode price. A turn ingested before 1.9.0 recorded no speed; on those three models it
is priced at the standard rate, and the figure is a floor.usage.inference_geo), and a turn that ran US-only ("us") on Claude Opus 4.6, Sonnet 4.6
or any later model costs 1.1 times every price, input, output, cache writes and cache reads,
fast mode's included. One routed globally ("global") costs the list price, and so does every
turn of an older model, which cannot run US-only. A turn that recorded no region, every one
ingested before 1.9.0, is priced at the global rate on the 4.6 and later models, and the
figure is a floor. So is one that recorded "not_available", which Claude Code writes when it
has no region to report: Anthropic's pricing pages do not say which rate such a turn paid,
and a request that names no region runs in the workspace's default one, which can be the US.Four sub-tiles showing the per-category dollar contribution to the session total. Useful for understanding where your spend is going — typically output tokens dominate for code generation, while cache reads dominate for long-running agentic sessions.
The dollar value of tokens that hit Claude Code's prompt cache, calculated per model as:
cache_read_tokens × (input_price − cache_read_price).
This is reported by Claude Code, not caused by Lumen. The label always reads "Saved by caching (reported by Claude Code)". Lumen displays it for completeness — it does not take credit for it, and it is never added to "Lumen optimized." See How much you save for why these two numbers are kept separate.
Rolling consumption windows — total tokens spent across all sessions in the last 5 hours and 7 days respectively. Each window shows an approximate reset time and an Opus-vs-other model split.
Honesty caveat: these are consumption totals, not "percentage of your plan limit." Lumen cannot read plan limits from Anthropic's API — it only sees what you've actually used, which it can measure precisely.
Calendar rollup totals — spending grouped by calendar day, ISO week (Monday start), and all recorded history. These use local time for day boundaries and the current locale's Monday-based week.
Screenshot needed —
docs/assets/screenshot-optimizer.pngCapture: the Optimizer tab showing effectiveness %, Lumen optimized, Saved by caching, and a by-tool breakdown row.
From 1.9.0 the Optimizer screen has no dollar hero. The net value figure described below is retired: the savings engine's figures are the headline on Home, and How it's measured shows how each was worked out, call by call; the token ratio, the by-tool and by-channel breakdowns and the provenance counts are on the Details screen.
Until 1.9.0 the optimizer's hero metric was dollars, not the token ratio. The token ratio was the headline until 1.3.1, and it flattered the product. Returning 87% fewer tokens sounds decisive, but an intercepted read also costs something — the read was blocked, so the model spends an extra round calling a Lumen tool instead. The honest question is whether the tokens saved are worth more than the round they cost:
value of saving S tokens = S × (one-time price + Σ cache-read price over the R rounds that follow)
cost of an extra round = the turn that read the result, priced as it ran
net = value − extra rounds × that cost, signed
From 1.9.0 every term is derived per call, from your own ledger, by one engine
(lumen_core::savings) that the app, lumen, lumen report and the tray all read. The turn that
read a call's result is found in the call's own conversation (a subagent's call in the subagent's
transcript), and its model, speed, region and cache duration price both the saving and the extra
round: a dollar is always the price of the model that consumed the tokens.
cl100k_base count the ledger records is converted by the
calibration table (below), by the file's type and the model of the turn that read the result,
at the median ratio, with the smallest to the largest ratio any sample came out at as its band.
A row with no such turn, or one read by a model the calibration did not measure, takes the
type's ratio over both tokenizers, weighted alike, with a band that spans them. What Lumen
returned (an outline, a slice, a stub, a delta) is not the raw file text the calibration
measured, so it takes the file's median with the widest band any sample gave: the smallest to
the largest ratio of every sample the model was sent, of every type (every sample of every
model where the model is not known). A Read returns at most 25,000 of Claude's tokens of a file
(Claude Code's documented default, and a file over 256 KB is refused whole, after which the
model reads a page), so a file whose calibrated count is larger counts as 25,000.R is the turns after the one that read the result, up to the first compaction or rewind
the daemon recorded in that transcript, or the first turn whose prompt is smaller than the one
before it: context was taken out there (a compaction, a rewind, cleared tool results), whether
or not the ledger recorded it, and the result may have gone with it. That end is labelled
inferred.R, R may be too long, so the saving is taken at R = 0 (the price of
putting the tokens in context once) and its rounds in full.
"On record" rather than "measured": the provider's usage data is measured, and this figure
also rests on calibrated estimates.R = 0.Every figure is a band, low to high. A token figure's central value is at the median ratios; a
dollar figure's is the conservative one, and the bold figure's is its band's low end. lumen report prints the rules in full.
Tool calls and built-in Read events are counted with a stand-in for Claude's tokenizer.
smart_read, recall_file and compress_logs count tokens in-process with cl100k_base,
OpenAI's BPE tokenizer, because Claude's is not available offline. Every figure is a
tokenizer's count, never a guess, but it is a count in cl100k_base's units, and Claude
counts the same text differently. Measured against Claude's own usage data on 60 samples of
raw file text from public repositories (benchmarks/calibration,
Claude Code 2.1.296, calibration of 2026-10-14), the models measured from Claude Opus 4.7 on
(Opus 4.7, Opus 4.8, Opus 5, Sonnet 5, Fable 5, Opus 5.5, Sonnet 5.5 and Haiku 5.5) count about
1.59× as many tokens (1.44–1.70×), and the earlier models measured (Opus 4.6, Sonnet 4.6, Opus
4.5 and Haiku 4.5) about 1.24× as many (1.14–1.31×). Each band is the 10th to the 90th
percentile of the samples (a savings figure's band, and the floor at its low end, take the
smallest to the largest ratio any sample came out at instead); by kind of file the median on
the models from Opus 4.7 on runs from 1.44× for logs to 1.70× for shell scripts. The samples
are raw text: a Read also sends a line number on every line, every answer comes in a
tool-result wrapper, and an answer Lumen makes from a file (an outline, a slice, a stub) is
text of its own, none of which is in these ratios. Every sample came out higher in Claude's
count than in cl100k_base's, on every model measured, but the samples are English and include
no CSS, HTML, YAML, SQL or non-Latin text.
lumen_core::calibration turns a count into an estimate of Claude's with its band, and every
savings figure goes through it. The usage figures elsewhere in the app (context fill, consumption windows, cache
reads) are not counted by Lumen: they are the provider's own, read from Claude Code's
transcripts.
Since 1.6.0 built-in Read events are counted by lumen-mcp hook meter, with the same
tokenizer, and it never falls back to a guess either: a file that is not text is recorded as 0
tokens with provenance unsupported, and a meter that cannot write its row records a fault
instead of a guess. Before 1.6.0 a shell hook shelled out to lumen-tok and fell back to
bytes ÷ 4 when it could not; before 1.1.5 it did so silently, and on installs set up
from a mounted .dmg before 1.0.1 the baked path pointed inside the disk image — so once
it was ejected, every built-in Read figure became an estimate while this document claimed
otherwise. Each row records token_source (measured / estimated / unsupported), and
rows predating 1.1.5 are marked as unverified provenance rather than reclassified, because
there is no honest way to recover it after the fact.
SUM(saved_tokens) across all smart_read, recall_file, and compress_logs calls
that Lumen actually made. This number starts small and grows with every session. It has no
dollar figure: a read records no model, so there is no price to put on it.
This is the only number Lumen claims credit for. It is small, verifiable, and
derived directly from the database: a sum of cl100k_base counts, with nothing guessed or
extrapolated. It is in cl100k_base's units; see above for how far those are from Claude's.
Shown alongside "Lumen optimized" for context, but in a clearly separate row. See Saved by caching above. The two numbers are never added together.
A breakdown of Lumen-optimized tokens by which tool produced them:
smart_read, recall_file, or compress_logs. Useful for understanding which
reads are being intercepted and which file types are generating the most savings.
Reads intercepted by the PreToolUse hook, in either client. Interception is enforced wherever hooks run: a Read of a file over the line threshold is blocked and redirected before it executes.
The channel breakdown that used to appear here has been removed rather than repaired. Until 1.1.5 the meter wrote the literal string
clion every built-inReadrow, so the chart plotted a constant and the "CLI missed reads" metric filtered on a value that matched every row. Real channel detection landed in 1.1.5; the breakdown returns once enough rows carry a measured channel to make it mean something.
Both clients. Reads on files ≥ 300 lines where Claude used the built-in
Read tool instead of a Lumen tool — i.e., the hook fired but Claude did not follow
the redirect, or the file was excluded. These are tracked as context (never as savings)
so you can see the true adoption rate. A high "not optimized" count in Full mode
suggests Claude is bypassing the redirect; see Verify it's working.
A card that counts what repeat reads answered short, apart from every figure above it, and says what its token figure is counted against. What it shows, and how to read it, is described under that heading.
A persistent badge on the Optimizer tab showing which mode the current (or most recent) session ran in. Full = Claude Code CLI with hooks active. Soft = VS Code extension (tools available; interception not enforced).
How much Lumen can do depends on how you run Claude Code.
npm i -g @anthropic-ai/claude-code # install if needed
claude # open a session
In the CLI, Lumen's PreToolUse hook intercepts every Read call on a large file
(≥ 300 lines) before it runs and redirects Claude to use lumen:smart_read instead.
This guarantees the cheaper read path is taken. Reads that bypass Lumen are also tracked
("not optimized — read in full") so you can see the true adoption rate.
The VS Code extension does not fire PreToolUse/PostToolUse hooks (known upstream limitation). Lumen's MCP tools are available and Claude can use them, but interception is not enforced — Claude routes to optimized reads opportunistically, not on every large-file read. Only reads that actually went through a Lumen tool appear on the Optimizer screen.
The CLI is a dashboard, not a precondition for optimization.
Interception works in both clients, so installing the CLI does not enable or guarantee anything the extension lacks. The earlier claim that it did was based on the same false premise as the Full/Soft mode distinction above. The VS Code extension still gives you the full context gauge, cost tracking, and caching savings display.
Until 1.9.0 the hero metric on the Optimizer screen was the net dollar value of
interception: what the tokens Lumen avoided are worth, less what the extra round cost, computed
with one assumed input, how many rounds a saving keeps paying for. 1.9.0 derives that input
per call (Effectiveness % says how), and lumen and lumen report
show the result for your own ledger. No figure is quoted here: yours depends on your sessions.
Each release against the one before it. Every release is benchmarked against the previous one
on macOS, Ubuntu and Windows, through the real lumen-mcp binary on a pinned public corpus: the
tokens each tool returns, per file and mode, and the latency of every hook and tool call.
benchmarks/README.md has every figure, the machines and the method.
| release | against | tokens returned | latency |
|---|---|---|---|
| 1.9.0 | 1.8.0 | equal | equal, except recall_file on a file under 300 lines on Ubuntu, 2% (30 µs) slower, and two calls on Windows 3% faster |
| 1.8.0 | 1.7.0 | equal | a blocked Read 0.4 to 2.6 ms slower; an identity hook now runs before every Lumen call |
The token ratio — 84% fewer tokens per intercepted read on the author's machine — is
on the Details screen. It is a ratio of two cl100k_base counts, not of Claude's, and on its
own it is not a result: a smaller reply that forces a second round is a loss however good the
ratio looks.
Every intercepted read reports full_tokens vs returned_tokens, counted with the
cl100k_base BPE tokenizer. That is a real tokenizer but not the one Claude bills with: the
counts are deterministic in its units and approximate Claude's, which come out higher: on raw
file text the models measured from Claude Opus 4.7 on (Opus 4.7, Opus 4.8, Opus 5, Sonnet 5,
Fable 5, Opus 5.5, Sonnet 5.5 and Haiku 5.5) count about 1.59× as many tokens (1.44–1.70×), and
the earlier models measured (Opus 4.6, Sonnet 4.6, Opus 4.5 and Haiku 4.5) about 1.24× as many
(1.14–1.31×), from the 10th to the 90th percentile of the 60 samples in the
calibration (calibration of 2026-10-14; a Read's line
numbers and the tool-result wrapper are not in these ratios). Nothing is guessed, extrapolated or scaled up — 96.6% of
that saving rests on a tokenizer count rather than a bytes/4 guess, which is asserted by a test
rather than asserted here.
→ Does the optimizer actually save anything? is the full
measurement, including the things that would make the figure above dishonest: 8 of 1,608
eligible reads bypassed the optimizer, all of them the intercept's deliberate fail-open guards, 3.4M tokens of binary-file noise excluded from the
baseline, and the 31 of 61 files in this repository where interception costs more than it saves
(measured 2026-10-12; the test prints the count and the eight least avoided). A Lumen tool's
reply is held to the size of the file it was asked about, plus the one line a fallback adds and
the header of smart_read(mode="full"), except for a file or log with nothing in it. A corpus
test checks that on this repository's files; the page lists what it does not cover (error
replies, repeat-read replies and the JSON-RPC envelope, among others). The page's repository
figures are produced by
cargo test --release -p lumen-mcp --test efficiency -- --nocapture and its thresholds are
asserted, so a regression fails CI instead of ageing into a stale claim; its ledger figures
come from the machine that ran it.
The Optimizer screen shows two clearly separated numbers:
| Label | What it is | Caused by |
|---|---|---|
| Lumen optimized | SUM(saved_tokens) over smart_read, recall_file, compress_logs calls | Lumen |
| Saved by caching | Cache-read tokens × (input price − cache-read price) | Claude Code's prompt cache |
These are never added together. The caching number is reported by Claude Code; Lumen displays it for completeness but does not claim credit for it. The "Lumen optimized" figure starts small and grows with every session. Small and verified beats large and invented.
Three MCP tools ship with Lumen. Claude uses them automatically when interception is active (Full mode), or on-demand in Soft mode:
| Tool | What it does |
|---|---|
smart_read | Returns a structural outline of a source file — functions, classes, imports with exact line ranges — without reading bodies. Each reply reports its own token count against the full file's, so what it saved is measured per call, not quoted. |
recall_file | Fetches one or more named items (function, class, struct) or an explicit line range, resolved via tree-sitter AST. Use after smart_read once you know what you need. |
compress_logs | Collapses repeated lines, stack-trace runs, and blank-line runs in log files and build output into annotated compact form. Deterministic — not LLM summarization. Repeated lines are kept with their count; the middle of a long stack trace is dropped, leaving the count of frames omitted. |
Languages supported by smart_read / recall_file: Rust, Python, TypeScript, TSX.
compress_logs works on any text.
claude mcp list
# lumen: /Applications/Lumen.app/Contents/MacOS/lumen-mcp ✓ Connected
Or inside a Claude Code session:
/mcp
The lumen server should appear as Connected.
After any session where Claude reads a large file via a Lumen tool, the token counts appear on the Optimizer tab, and the effectiveness ratio on the Details tab.
Ask Claude to read a large source file. Lumen's hook blocks the read and tells Claude:
Lumen intercept: path/to/file.rs is 420 lines.
Instead of reading the full file, call:
1. lumen:smart_read(path="path/to/file.rs") → structural outline with line ranges
2. lumen:recall_file(path="path/to/file.rs", names=["<item>"]) → fetch only what you need
Each call reports the tokens it saved in _meta.saved_tokens.
Use smart_read(mode="full") only if you truly need every line.
If the lumen tools are unavailable to you (server down, permission denied), retry
this exact Read — it will be allowed through. Do not abandon the task.
Claude then uses smart_read and the Optimizer screen records the event. The message
quotes no savings figure: Claude routes on it, and a typical-case number would bias that
routing on files where it does not hold.
If interception is not happening in the CLI:
# confirm hooks are registered
grep -n '"command": .*/lumen_' ~/.claude/settings.json
# expected: lumen_read_intercept.sh four times (PreToolUse on Read, and on the three Lumen
# tools mcp__lumen__smart_read, recall_file and compress_logs)
# and lumen_meter.sh twice (PostToolUse on Read and on Bash)
# more meter lines than that: an older Setup also hooked the lumen tools; run Setup again (below)
# fewer intercept lines: an install from before 1.8.0; run Setup again (below). Without the
# three tool entries the server cannot tell whose call it is and repeat reads answer in full.
# confirm hook scripts exist and are executable
ls -la ~/.claude/lumen/
From a checkout of this repository, ./scripts/verify-hooks.sh goes further: it runs the
installed hooks the way Claude Code does, against a scratch ledger it deletes afterwards,
and says what each one did.
If any check fails, run Setup again: quit Lumen, delete ~/.claude/lumen/.setup_done, open
Lumen (it opens on Setup and runs it), and restart Claude Code. There is no menu item for it
(the tray menu has Open and Quit). On the plugin, update or reinstall it and restart Claude
Code. The Homebrew lumen-cli on its own installs no hooks.
Nothing leaves your machine.
Here is exactly what Lumen's hooks do. Both are short shell scripts in ~/.claude/lumen/
that hand each event to lumen-mcp hook, the binary that also serves the tools. Neither
makes a network call.
lumen_read_intercept.sh (PreToolUse, CLI only) — receives the Read tool call as JSON
on stdin. If the file is a source or log file at or above the line threshold, it blocks
the read and writes a redirect message to stderr for Claude to act on. It opens the
file only to count its lines, and keeps nothing of it.
It is also registered before Lumen's own tools (mcp__lumen__smart_read,
mcp__lumen__recall_file, mcp__lumen__compress_logs, and under the plugin's server name
mcp__plugin_lumen_lumen__*). There it blocks nothing and prints nothing: it writes a small
record naming the call's session (and, inside a subagent, its agent) beside the ledger so the
server can tell which conversation a call is from (see repeat reads below). The record holds
ids and a timestamp, never a path or contents.
It writes two other things, both local, and both there so that a redirect can never leave Claude with no way to read the file at all:
lumen_intercept_<session-id> in the temp directory (inside a
subagent lumen_intercept_<session-id>.<agent-id>), one line
per file already redirected in this session. A file is redirected at most once: if
Claude comes back to the built-in Read for the same file, the Lumen route did not work
for it and the read is allowed through. Without this the hook can deadlock a session —
it blocks the built-in Read while the replacement is unreachable.faults.jsonl beside the database when
a hook lets through a read it would have redirected, loses an event it should have
metered, or a tool call crashes and is answered with an error. It contains the file path,
its line count, what went wrong, and the session id
— never file contents. lumen faults reads these; nothing is sent anywhere unless
you explicitly file a report. Set LUMEN_CAPTURE=0 to keep the hooks and record nothing.If lumen-mcp is gone — Lumen moved or removed — both scripts let the event through, say
so on stderr and record a fault, rather than blocking a read nothing is left to serve.
Set LUMEN_HOOK_ENABLED=0 to disable interception entirely. LUMEN_DEBUG=1, which is off
unless you set it, also writes the payload of each run verbatim to lumen_hook_dump.json in
the temp directory, each run overwriting the last, file contents and full command lines
included; it exists to capture test fixtures.
The only network request Lumen makes without being asked. Once a day, at most, the app
GETs https://api.github.com/repos/HackPoint/lumen/releases/latest and compares the tag to
its own version.
What is sent: nothing beyond what any HTTPS request to GitHub reveals. No credential, no identifier, no version string, nothing about the machine, the projects on it, or the ledger. It is a plain unauthenticated read of a public endpoint.
You are notified only for a minor or major release. Patch releases are silent — a
notification that fires for every x.y.Z gets dismissed reflexively, and then the one that
mattered gets dismissed too. A given version is announced once, and the check state lives
in update_check.json beside the database.
Turn it off entirely:
export LUMEN_UPDATE_CHECK=0
With that set, Lumen makes no unprompted network request of any kind.
→ Filing a fault report walks through it with screenshots. The short version:
Nothing is sent until you ask for it — via lumen faults --yes (lumen report --yes
before 1.9.0, which still works), or the File issue button under Report a fault on the
Settings screen. You see the exact text first,
and it is the text that gets sent; it is never re-generated at send time.
lumen comes from the lumen-cli formula; a cask-only install leaves it off your PATH.
The binary is still in the bundle at
/Applications/Lumen.app/Contents/MacOS/lumen-cli, which is the route that works when the
app will not start — see If lumen is not a
command.
Three routes are tried in order, and the first that works wins:
| order | route | needs | can comment on an existing issue |
|---|---|---|---|
| 1 | GitHub CLI — gh issue create / gh issue comment | gh installed and authenticated | yes |
| 2 | REST API — POST /repos/{repo}/issues | GITHUB_TOKEN or GH_TOKEN in the environment | yes |
| 3 | Prefilled browser form | a browser | no — it opens the existing issue instead |
The browser route is a handoff, not a filing: it opens GitHub's new-issue form with the body already filled in, and nothing exists on the tracker until you press Submit. Lumen says so rather than reporting it as filed.
Why a chain: gh is the only route that can post a follow-up comment without a human,
but almost nobody running the app has it. The browser route needs nothing at all, so
there is always a way to report a fault. When a route is skipped, the reason is shown —
a silent fallback would hide that your preferred one is broken.
Reports are deduplicated on a fingerprint of (kind, variant, version) carried in the
body as an HTML comment. A second report of the same fault comments on the existing
issue instead of opening a duplicate. The lookup reads the tracker over HTTPS and works
without any credentials on a public repository; a token is only ever needed to write.
lumen_meter.sh (PostToolUse) — a shell script that fires after a Read or a Bash
call completes and hands the event to lumen-mcp hook meter. It inserts one row into a
local SQLite database and makes no network calls. What it records differs by tool:
After a Read it counts the tokens in the file that was just read, with the BPE
tokenizer built into lumen-mcp (local, no network), and stores the file's path, line
count and modification time. Files that are not text — images, binaries — get a row
with no token count and a provenance of unsupported; Lumen does not guess a number
for them.
After a Bash call it counts the tokens in the output the command already
produced, to measure how much of your context goes to command output rather than to
files. This is observation only: there is no PreToolUse hook on Bash, nothing
is intercepted, blocked, or wrapped, and no command is ever executed by Lumen.
Of the command line itself, only the program and its subcommand are stored — cargo test, git status — never the full text. Command lines routinely carry credentials
in flags and URLs, and a leading VAR=value assignment is dropped before the label
is taken, so TOKEN=secret curl … is recorded as curl. Command output is
tokenized in memory, from the payload Claude Code hands the hook, and unless
LUMEN_DEBUG=1 is set it is never written anywhere — only the resulting count is stored.
If you would rather not record Bash output at all, set LUMEN_METER_BASH to 0 in the
env block of ~/.claude/settings.json:
"env": { "LUMEN_METER_BASH": "0" }
It applies from the next Claude Code session. Reads are still metered, and Setup keeps
the env block when it runs again or upgrades Lumen. Removing the Bash entry under
PostToolUse also stops it, but only until the next time Setup runs, because Setup puts
that entry back.
When the model asks smart_read or recall_file for something it already holds, the second
answer would repeat tokens it has. Lumen remembers what it last returned in this session for
each (session, request) and answers a repeat with less. The built-in Read is not touched:
Claude Code already dedups it. It was an opt-in flag in 1.7.0 (LUMEN_REPEAT_READS=on); from
1.8.0 it is on unless you turn it off.
Turning it off. Any of these, and the environment variable wins when it says something:
env block of ~/.claude/settings.json (or wherever Claude
Code gets its environment): "env": { "LUMEN_REPEAT_READS": "off" }. It is read when Claude
Code starts the server, so it applies from the next Claude Code session.lumen_settings.json containing {"repeat_reads": false} (macOS
~/Library/Application Support/io.hackpoint.lumen/, Linux ~/.local/share/io.hackpoint.lumen/,
Windows %APPDATA%\io.hackpoint.lumen\; LUMEN_DB moves it with the ledger). The server reads
it on every Lumen call (about 30 microseconds), so a change applies to the next call, with no
restart. lumen doctor prints the path it is using and the state.The switch shows what the settings file says, or the default (on) when there is no file, and
nothing else. It cannot see a LUMEN_REPEAT_READS in the environment Claude Code gives the server,
which wins over the file, nor a LUMEN_DB there that points the server at a ledger elsewhere, whose
own settings file is then the one that counts; its text says so. A file it cannot use is shown as
off, with the reason, and switching on replaces it; the other keys of a file that is valid are kept.
When the setting cannot be read, or a change cannot be saved, the window says that. It does not
show on or off in the meantime.
With it off every read is answered as it always was, nothing is remembered, and the fresh
option is not advertised. Switching from off to on in the middle of a session leaves the
client's cached tool list without fresh until the next session; that is harmless, because the
stub text names fresh=true and the server honours it whether or not it was advertised.
A value that cannot be told is never guessed at: an unrecognised LUMEN_REPEAT_READS, or a
settings file that is not valid JSON, is not a boolean or cannot be read, turns repeat reads
off (every answer in full), is recorded as a repeat_reads_config fault (the app's fault
count and lumen faults pick it up), and lumen doctor names it and says what to fix. Write
the file as UTF-8 without a byte-order mark: a BOM, UTF-16 or an empty file is rejected as
corrupt, which is off plus the fault. A mistyped key (repeat_read) is only an unknown key: it
is ignored and repeat reads stay on, so check the state lumen doctor prints after editing.
LUMEN_REPEAT_READS | behaviour |
|---|---|
| unset or empty | the setting file decides; no file, or no repeat_reads key in it: on |
on, 1, true | on, whatever the file says |
off, 0, false | off, whatever the file says |
ab | half of files, split by a stable hash of the path, take the new path; the other half are answered as before, and the row records what the new path would have done (for measuring) |
| anything else | off, and reported as above; the file is not consulted |
Upgrading from 1.7.0. The server answers repeats as soon as it is updated, but it can only
tell whose call it is through the PreToolUse hooks on the three Lumen tools, which 1.7.0 never
registered. Until they are registered every call is answered in full (nothing breaks, nothing
is saved) and lumen doctor and Setup's hook check say so. How each install gets them:
| install | how the hooks arrive |
|---|---|
| Lumen app | Setup registers them, once. Launching the app does not edit ~/.claude/settings.json. After updating, quit Lumen, delete ~/.claude/lumen/.setup_done, open Lumen (it opens on Setup and runs it), then restart Claude Code |
| Claude Code plugin | hooks/hooks.json carries them; update or reinstall the plugin, then restart Claude Code |
Homebrew lumen-cli alone | there is nothing to update: it installs lumen (doctor, stats) and no MCP server or hooks. Install the app or the plugin to have the tools; lumen doctor says the same |
The table below is what a repeat is answered with.
| the file since the last read in this session | the answer |
|---|---|
| unchanged (git blob hash of the content, CRLF read as LF) | one sentence: "Unchanged since an earlier read of this file in this session. If that read is not in your context, call smart_read(path, fresh=true)." |
| edited | a delta: only the symbols that changed — changed [body], changed [signature], added, removed, moved (same hashes, new line range) — with current line ranges and a count of the unchanged ones |
recall_file of a symbol that changed | a diff scoped to that symbol with its header as context; the whole body when the diff is not smaller |
first read, fresh=true, a compaction or rewind since, or no way to tell | the full answer |
A repeat answer is used only when it costs fewer tokens than the full answer it replaces.
The base is held in memory by lumen-mcp; a restart loses it and the next read is the full
answer. "Unchanged" is decided by content, never by modification time.
Whose read is it? (1.8.0) lumen-mcp is started once per Claude Code process and outlives
/clear, which begins another conversation in the same process. The environment variable the
server was started with names the first conversation only, and the MCP request carries no
session (only a tool_use_id), so the server learns who is asking from the PreToolUse hook:
for each call to smart_read, recall_file or compress_logs the hook leaves
lumen_call_<tool_use_id>.json beside the ledger with the call's session_id and, inside a
subagent, its agent_id, and the server reads and removes it when the request arrives. Bases are
held per conversation, a subagent counting as its own; when the conversation changes, which is
what /clear and /resume do, every base is dropped. A call whose conversation cannot be told
(the hook is not registered, did not run, could not write, or left a record that is malformed or
an hour old) is answered in full, recorded with no session and the plan unidentified, and
nothing is remembered from it. The old variable is never used for this. lumen doctor and
Setup's hook check say when the hook is missing. Hook records for calls the user then declines
are pruned by age and count.
Is the base still in the model's context? The daemon reads the Claude Code transcript and
records one session_events row per compaction (compact_boundary) and per rewind (a message
whose parent branches back past the session's latest real prompt; subagent sidechains are
excluded), and one per resync: a transcript the daemon went on tailing after losing its
tracker, which could hide a rewind. If any of them happened after the base was returned, or the
ledger cannot be asked, the answer is the full one. Two cases cannot be seen: a compaction inside
a subagent, which the daemon does not read, and an event the daemon has not ingested yet. That is
why every repeat answer tells the model to call again with fresh=true. Both tools accept
fresh; it always returns the full result and is recorded, and its rate is the error rate of
this feature.
Each read_events row records repeat_arm and repeat_plan (first, fresh, unchanged,
delta, and for a full answer the reason: compaction, rewind, resync, unverifiable, no_base,
unidentified, outline, delta_too_big, unchanged_not_smaller) and, for a call made inside a
subagent, agent_id. Routes: smart_read_unchanged,
smart_read_delta, recall_file_unchanged, recall_file_delta.
The size of a delta is measured per delta served against the outline it replaces, headers and
the fresh=true sentence included; it is not an overall saving. Overall effect, follow-up
rate and the fresh=true rate are measured live, in the ab split.
What the Optimizer tab shows (1.8.0). A Repeat reads card, below the other figures, counts what repeat reads have done over the ledger's lifetime. It is counted apart from the rest: these answers are in none of the token totals (Lumen optimized, Today, This week) or the by-tool and by-channel breakdowns, and the card never calls anything a saving. (The provenance counts on the Details tab, how many metered events have unverified token provenance, still include these rows: they count every row that carries a token count.)
recall_file, the symbols asked for), which is
usually far smaller than the file, so this is more than repeat reads gain over having them off,
and it is not shown as a saving. It counts only answers whose tokens the tokenizer counted: when
that is fewer than all of them the card says how many, and when it is none the figure is a dash.fresh=true): n of m, where m is the unchanged answers, the
deltas and the fresh requests together, so it is how often the model did not take a repeat
answer. Only requests made while repeat reads were on are counted (under ab, the half that
takes the new path), so the figure stops moving when they are turned off.LUMEN_REPEAT_READS in a Claude Code MCP config would outrank the file unseen. It never says
what the server will do, for the same reason the switch cannot.unidentified, counted while repeat reads were on): the count, with an Open Setup link
to the app's Setup screen, which runs Setup, after which Claude Code needs a restart. That
remedy is for the app install; on the plugin, update or reinstall it, as in the table above.smart_read's outline can be sized by the economics of the call rather than by a fixed
format. Set LUMEN_RANKED_OUTLINE in the environment Claude Code passes to the MCP
server:
| value | behaviour |
|---|---|
| unset, or anything unrecognised | off — the outline that has always shipped |
on | every file uses the ranked outline |
ab | half of files, split by a stable hash of the path, so a given file always takes the same arm |
LUMEN_RANKED_TIME_BUDGET_MS overrides the pipeline's wall-clock ceiling (50 ms by
default). Raise it on a slow machine; a file that exceeds it falls back to the ordinary
outline and records ranked_too_slow.
An intercepted read costs one extra round, so the outline is only worth returning when it
saves more than that round costs. Lumen computes the minimum saving from your own turns
history and refuses files that cannot clear it, recording the refusal and the numbers
behind it. Every decision is written to read_events — budget, s_min, the context,
rounds and output figures used, k_selected of n_total, and coeff_version — so the
two arms can be compared afterwards rather than trusted.
Measured caveat before you enable it. On this repository the ranked outline returns
more tokens than the current one (10,759 vs 6,205 across the files that qualify),
because it captures nested definitions the old outline never did and because the budget is
usually large enough that nothing gets trimmed. The saving comes from the refusals, not
from the ranking. Treat on as an experiment, not an optimisation.
All session and usage data is stored locally at:
~/Library/Application Support/io.hackpoint.lumen/lumen.db
It is a plain SQLite file. You can open it with sqlite3, inspect it, or delete it
at any time. Deleting it resets all history; Lumen recreates it empty on next launch.
lumen-daemon — the background process Lumen launches — watches ~/.claude/projects/
for new JSONL session files and reads token usage from them, writing to the same local DB.
It makes no network calls.
Use the Uninstall button on the Setup screen. It removes:
lumen MCP server entry from ~/.claude.json~/.claude/settings.json~/.claude/lumen/ directoryTo remove everything including the database:
rm -rf /Applications/Lumen.app
rm -rf ~/Library/Application\ Support/io.hackpoint.lumen
rm -f ~/.lumen_db_path
Prerequisites: Rust (stable), Node 22.22.3+, 24.15+ or 26+ (Angular 22's floor — 24.0–24.14 is too old), pnpm
On Linux, also install the GUI toolkit headers:
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev patchelf
git clone https://github.com/HackPoint/lumen.git
cd lumen/lumenator
# Build the three helper binaries and stage them for Tauri
./build-sidecar.sh
# Install frontend dependencies
pnpm install
# Development mode
pnpm tauri dev
# Production build → Lumen.app + Lumen_1.1.0_aarch64.dmg
pnpm tauri build
# Artifacts at: target/release/bundle/macos/ and target/release/bundle/dmg/
Crate layout:
crates/
lumen-core/ shared types: Record parser, schema, tokenizer, structurer, compressor
lumen-daemon/ file watcher + SQLite ingester + WebSocket server
lumen-mcp/ MCP stdio server (smart_read, recall_file, compress_logs, lumen_ping)
also builds lumen-tok (standalone BPE tokenizer)
lumen-stats/ SQLite rollups the GUI displays: usage, sessions, optimizer
lumenator/ Tauri application: Angular frontend + Rust backend
No menu-bar icon on macOS? The most common cause is that the icon was ⌘-dragged off the menu bar at some point — macOS remembers that permanently, and recreates the item hidden on every later launch. Lumen now clears that state at startup and tells you it did. If the icon is still missing:
lumen doctor # names the likely cause and its one-line fix
→ No menu-bar icon (macOS) is the full diagnosis path.
If Lumen is running but you cannot reach it, open -a Lumen (or double-clicking it in
/Applications) reveals the main window, and lumen show does the same from a terminal. The CLI
inside the bundle works even when the GUI does not:
/Applications/Lumen.app/Contents/MacOS/lumen-cli report --dry-run
Logs are at ~/Library/Logs/io.hackpoint.lumen/Lumen.log. Set LUMEN_LOG=debug for more.
(Released builds up to and including 1.5.1 wrote no log at all — the logger was registered
only in debug builds.)
| Unsigned / un-notarized | Workaround documented above. Notarization on the roadmap. |
| No Intel macOS or arm64 Linux build | Build on request — open an issue. |
| Linux tray needs a tray host | GNOME requires the AppIndicator extension; see Linux tray support. |
| Hooks are CLI-only | VS Code extension API does not support PreToolUse/PostToolUse hooks. Soft mode available. |
| Optimizer requires model cooperation in Soft mode | Full mode (CLI) enforces interception; Soft mode doesn't. |
| Context window comes from a built-in model table | Known models use their published window; unrecognised ones fall back to inferring 200K / 500K / 1M from observed fill. Your actual window may differ by plan tier. |
| Plan limits not visible | Lumen reads consumption from session files but cannot query Anthropic for your plan's token limits. |
MIT — issues, questions, and PRs welcome.