CLI
surface [OPTIONS]
What AI runs here, and what it costs.
Options:
--json Print the scan as JSON and exit, instead of opening the dashboard
--offline Never fetch model prices; use the cache, then the built-in table
--check Print resolved paths and settings, then exit without scanning
--demo Open the dashboard on mock data, without scanning this machine
-h, --help Print help (see more with '--help')
-V, --version Print version
No arguments, no subcommands, no positional paths. surface scans this machine and nothing else.
Flags
--json
Runs the full scan, prints it as pretty JSON on stdout, exits. Nothing else is written to stdout, so it pipes cleanly:
The payload is documented field by field in JSON output.
--offline
Suppresses the one network request in the program. Prices then come from the
cached table, and from the table compiled into the binary when there is no cache.
The Cost view labels the latter built-in price table, since those rates are as
old as the release.
Everything else is unaffected — the scan reads local files either way.
--check
Prints what surface resolved and exits without scanning. The fastest way to answer "where does it look for my config" and "did it read it":
$ surface --check
state dir /Users/you/Library/Application Support/ai.holistic.surface
config dir /Users/you/Library/Application Support/ai.holistic.surface
config loaded
ledger /Users/you/Library/Application Support/ai.holistic.surface/usage-ledger.json
prices 1284 models, cached 2h ago
sites 30 day lookback
usage 30 day window
| Line | Says |
|---|---|
state dir |
Where the ledger and price cache live |
config dir |
Where surface.toml is looked for |
config |
loaded, or defaults (no file) |
ledger |
Full path to the ledger file |
prices |
Model count, and the cache age — or (built in; no cache, no network) |
sites |
The lookback window, disabled in config, or not compiled in (built without the sqlite feature) |
usage |
The accounting window, or disabled in config |
--check honours --offline, so surface --check --offline never reaches the
network either.
--demo
Opens the dashboard on mock data and scans nothing — no transcripts, no browser history, no ledger read or written. For showing the tool on a machine that has no AI tooling on it, for a screen recording, and for reproducing a layout bug without having to arrange the data that caused it.
surface --demo # the dashboard, populated
surface --demo --json # the same mock scan as JSON, for fixtures
It is labelled everywhere it appears: the sidebar carries a DEMO badge for as
long as it is running, and the JSON payload reports "demo": true — a field that
is present and false on a real scan, so a consumer never has to know the flag
exists to be safe from it.
What the mock data is, precisely:
| Tool and domain names | Real. Taken from the same catalogues the detector uses, by id, so the demo cannot show a product that does not exist |
| Counts | Invented — 30 days of usage across four tools, 13 models and 16 repositories, each with its own lifecycle and rhythm |
| Costs | Computed, not invented. Fake tokens, real price table, so a demo dollar figure is what those tokens would genuinely have cost |
| Warning states | Deliberately present: one unpriced model, one local model, one unreadable browser profile, one unreadable transcript source |
| Reproducibility | Deterministic for a given day. Two runs produce the same dashboard, which is what makes it usable for screenshots |
The scan timings in the footer are fixed values, not measurements — nothing was timed because nothing ran.
Keys in the dashboard
| Key | Does |
|---|---|
| tab, →, l | Next view |
| shift-tab, ←, h | Previous view |
| 1–6 | Jump to Overview, Tools, Sites, Usage, Cost, Projects |
| j/k, ↓/↑ | Move the selection |
| g/G, home/end | First / last row |
| page up/page down | Ten rows |
| u | Overview chart: spend or tokens |
| w | Regroup charts by day, week or month |
| ? | Help overlay |
| q, esc, ctrl-c | Quit |
Full descriptions in The dashboard.
Environment variables
| Variable | Effect |
|---|---|
SURFACE_STATE_DIR |
Override the state directory (ledger, price cache) |
SURFACE_CONFIG_DIR |
Override where surface.toml is looked for |
SURFACE_SCAN_HISTORY |
false disables the browser-history section |
SURFACE_HISTORY_LOOKBACK_DAYS |
Domain lookback window |
SURFACE_SCAN_USAGE |
false disables the transcript section |
SURFACE_USAGE_WINDOW_DAYS |
Token accounting window |
Booleans accept 1/true/yes/on and 0/false/no/off; anything else is
ignored rather than read as false. Day counts are clamped to 1–3650.
Environment variables beat the config file; CLI flags beat both.
Exit codes
| Code | Means |
|---|---|
0 |
The scan completed. Includes a scan that found nothing, and one where a section failed and was reported |
| non-zero | surface could not start or could not restore the terminal — a bad config file, an unwritable state directory, a terminal that refused raw mode |
A section that panics does not change the exit code: it is named in the
footer, or in failed_sections under --json. If you want that to be an error,
assert on it:
An unknown key in surface.toml is a startup error. A misspelled setting that
silently does nothing is the worse outcome.
Files
| Path | Written | Holds |
|---|---|---|
<state>/usage-ledger.json |
Every scan | Daily token totals, dedup keys, byte offsets |
<state>/litellm-prices.json |
When the price table is fetched | The cached rates |
<config>/surface.toml |
Never — read only | Your settings |
Platform defaults for <state> and <config> are in
Installation.
surface writes nothing anywhere else.