Every captured request lands in ~/.halton-meter/db.sqlite tagged with a
project slug. The slug is what halton-meter report groups by, what the
HTTP API splits totals on, and what shows up in cost roll-ups. Halton
Meter resolves the slug through a chain of layers — each one explicit,
ordered, and deterministic. This page is how you steer it.
For the architecture of the chain itself, see Project tagging.
The seven attribution layers, in order
| Layer | Source | Wins when |
|---|---|---|
| 1 | HALTON_PROJECT env var | Set in the process environ |
| 2 | .haltonrc walked upward from cwd | A project = "<slug>" line is found |
| 3 | IDE workspace (Windsurf / Cursor / VS Code cmdline sniff) | The IDE was launched with --folder-uri or equivalent |
| 4 | Git repo root basename | A .git directory exists upward from cwd |
| 6.5 | macOS sandbox container bundle id | Process is sandboxed (e.g. mac:com.openai.chat) |
| 7 | cwd basename | Always, as a fallback |
| 8 | unattributed sentinel | Nothing above resolved |
Layers 5 and 6 are reserved for monorepo workspace detection and Linux/Kubernetes container detection respectively. Layer 5 is parked; Layer 6 is not relevant on macOS.
The pure resolver functions live in
daemon/halton_meter/attribution/layers.py. Both the daemon-side and
edge-side resolvers call into that module, so attribution semantics stay
in lock-step across the two paths.
Pinning a slug per repo with .haltonrc
The most common configuration. Drop a .haltonrc at the root of a
project; the slug applies to every cwd inside it.
# Halton Meter — per-project config
project = "billing"
body_capture = on The slug is normalised to lowercase alphanumerics plus -, _, :,
/. A malformed slug is rejected at parse time; the layer falls through
to the next rather than silently corrupting the row.
Overriding for a single command
HALTON_PROJECT wins over everything below. Use it for one-off scripts
that live outside the usual repo tree, or to split traffic from the same
cwd into two slugs:
HALTON_PROJECT=experiments halton-meter run python sweep.py HALTON_PROJECT=client-acme halton-meter run claude How IDE workspaces resolve
When Layer 2 misses (no .haltonrc upward from cwd), Halton Meter sniffs
the launching IDE’s command line for a workspace flag:
- Windsurf / Cursor / VS Code:
--folder-uri file:///path/to/repo - JetBrains:
--project /path/to/repo
The path’s basename becomes the slug after normalisation. This lets a
freshly-opened workspace get a sensible slug before you ever drop a
.haltonrc.
The Layer 3 helpers — extract_workspace_path_from_cmdline(),
decode_windsurf_workspace_id(), decode_file_uri() — live in the same
attribution/layers.py module if you need to confirm what your IDE is
actually emitting.
Inspecting and rewriting historical attributions
halton-meter project show prints the per-project settings row from
SQLite:
halton-meter project show billing To toggle body capture per-project (overrides the daemon-wide
bodies.enabled switch):
halton-meter project set billing body-capture off If a chunk of historical traffic was tagged to the wrong slug — common
the first time someone moves a repo into a new directory — retag
rewrites the rows in place. Defaults to dry-run:
$ halton-meter retag --from old-slug --to new-slug # dry-run
$ halton-meter retag --from old-slug --to new-slug --apply # commit retag writes a _migrations row marking the rewrite so it never runs
twice for the same from/to pair without --force.
What’s next
- Project tagging — the full layer architecture and the design rationale behind it
halton-meter report— slice captured rows by project, model, or date range