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