Skip to content

Configuration

Every setting is optional. surface works with nothing but the binary, and a missing config file is the normal case rather than an error — so this page is really a list of the four things worth changing.

Where the file goes

surface.toml, in the config directory:

Path
macOS ~/Library/Application Support/ai.holistic.surface/surface.toml
Linux ~/.config/surface/surface.toml
Windows %APPDATA%\holistic\surface\config\surface.toml

surface --check prints the resolved directory on this machine, which beats guessing:

surface --check

Precedence

Later wins:

  1. Built-in defaults
  2. surface.toml
  3. SURFACE_* environment variables
  4. CLI flags (--offline)

The whole file

Copy this and delete what you do not need — every value shown is the default.

surface.toml
# surface configuration — every setting is optional.
#
# surface works with no config file at all; this documents the defaults. Copy it
# to the path `surface --check` reports as the config directory, as `surface.toml`.
#
#   macOS    ~/Library/Application Support/ai.holistic.surface/surface.toml
#   Linux    ~/.config/surface/surface.toml
#   Windows  %APPDATA%\holistic\surface\config\surface.toml

# ----------------------------------------------------------------- AI sites
#
# Read browser history for visits to known AI domains. Only the domain, a visit
# count and a last-seen date are read: the domain filter runs inside SQLite, so
# no URL, path, query string or page title is ever loaded — not even for the AI
# domains themselves, let alone the rest of your browsing.
[web]
# Set false to skip browser history entirely. The Sites view then says it is
# switched off rather than showing an empty list.
scan_history = true

# How recently a domain must have been visited to appear. Visit counts are the
# browser's lifetime totals; this only decides which domains are listed.
history_lookback_days = 30

# Extra domains to treat as AI services — an internal model gateway, say.
# Matched on the registrable host, so "models.corp.example" also covers
# "www.models.corp.example" but never "notmodels.corp.example".
extra_ai_domains = []

# --------------------------------------------------------------- token usage
#
# Read the transcripts Claude Code, Codex and OpenCode already write, to count
# tokens per day, tool and model. Message content is never read.
[usage]
# Set false to skip transcripts entirely.
scan = true

# How many days of daily totals to keep. Older days are pruned from the ledger,
# along with their deduplication keys.
window_days = 30

# ---------------------------------------------------------------------- cost
#
# surface prices tokens at API rates. If you pay a flat subscription instead,
# tell it what you actually pay and the Cost view will compare the two.
[cost]
# Monthly spend per tool, in USD. Keys are tool ids as they appear in the Usage
# view: claude_code, codex, opencode, gemini_cli, ...
#
# Without an entry here a tool gets no subscription row at all — surface reads no
# account state, so it cannot know your plan and will not guess one.
[cost.subscriptions]
# claude_code = 100.0
# codex = 30.0

# ------------------------------------------------------------ env overrides
#
# Environment variables win over this file, and are handy for one-off runs:
#
#   SURFACE_STATE_DIR              where the ledger and price cache live
#   SURFACE_CONFIG_DIR             where this file is looked for
#   SURFACE_SCAN_HISTORY=false     same as [web] scan_history
#   SURFACE_HISTORY_LOOKBACK_DAYS  same as [web] history_lookback_days
#   SURFACE_SCAN_USAGE=false       same as [usage] scan
#   SURFACE_USAGE_WINDOW_DAYS      same as [usage] window_days

[web]

Browser history, for known AI domains only.

Key Default Does
scan_history true Read browser history at all
history_lookback_days 30 How recently a domain must have been visited to be listed
extra_ai_domains [] Extra domains to treat as AI services
[web]
scan_history = true
history_lookback_days = 90
extra_ai_domains = ["models.corp.example", "llm-gateway.internal"]

extra_ai_domains is matched on the registrable host, so models.corp.example also covers www.models.corp.example but never notmodels.corp.example. An internal model gateway is the usual reason to set it.

Setting scan_history = false makes the Sites view report that it is switched off, rather than showing an empty list.

The lookback filters domains, not visit counts

Visit counts are the browser's lifetime totals. history_lookback_days only decides which domains make the list.

[usage]

Token accounting from the transcripts your tools already write.

Key Default Does
scan true Read transcripts at all
window_days 30 How many days of daily totals to keep
[usage]
scan = true
window_days = 90

window_days is also the retention policy: days that fall outside it are pruned from the ledger, along with their deduplication keys. Widening it does not recover days already pruned — those transcripts will be re-read from scratch, which costs a cold scan once.

Both day counts are clamped to 1–3650, not rejected. A nonsensical window is a typo, and refusing to run over a typo is worse than running over a sane value. An unknown key, on the other hand, is an error at startup: a misspelled setting that silently does nothing is the worse failure.

[cost]

surface prices tokens at API list rates. If you pay a flat subscription instead, tell it what you actually pay and the Cost view will compare the two.

[cost.subscriptions]
claude_code = 100.0
codex = 30.0

Keys are tool ids as they appear in the Usage view — claude_code, codex, opencode, gemini_cli, … — and values are monthly USD.

A tool with no entry gets no subscription row at all. surface reads no account state, so it cannot know your plan and will not guess one. A configured figure is used as given; a list-price fallback is labelled est wherever it is shown.

Environment variables

Handy for a one-off run, and what the test suite uses to stay out of a real profile.

Variable Overrides
SURFACE_STATE_DIR Where the ledger and price cache live
SURFACE_CONFIG_DIR Where surface.toml is looked for
SURFACE_SCAN_HISTORY [web] scan_history
SURFACE_HISTORY_LOOKBACK_DAYS [web] history_lookback_days
SURFACE_SCAN_USAGE [usage] scan
SURFACE_USAGE_WINDOW_DAYS [usage] window_days

Booleans accept 1/true/yes/on and 0/false/no/off; anything else is ignored rather than treated as false.

# Transcripts only — no browser history, nothing written to the real profile
SURFACE_SCAN_HISTORY=false SURFACE_STATE_DIR=$(mktemp -d) surface --json
$env:SURFACE_SCAN_HISTORY = 'false'
$env:SURFACE_STATE_DIR = (New-Item -ItemType Directory -Path "$env:TEMP\surface-scratch" -Force).FullName
surface --json

Those two assignments last for the current PowerShell session. Use [Environment]::SetEnvironmentVariable('SURFACE_SCAN_HISTORY','false','User') to make one stick, or Remove-Item Env:SURFACE_SCAN_HISTORY to undo it now.

Recipes

Tools only — no history, no transcripts
[web]
scan_history = false

[usage]
scan = false

The scan then reports which AI tools are installed and nothing else. Tool detection has no off switch: it is the cheapest section and the one the tool exists for.

A quarter of history instead of a month
[web]
history_lookback_days = 90

[usage]
window_days = 90

Expect one slow scan while the extra transcripts are read, then the usual ~50 ms.

Never touch the network

Pass --offline, or make it permanent with a shell alias:

alias surface='surface --offline'

Prices then come from the cache, and from the built-in table when the cache is missing. There is no config key for this — a flag you can see beats a setting you have forgotten.