codeburn/docs/sync
ozymandiashh 37a5b46f85 fix(sync): key the device, span and trace digests
The sync path derived three identifiers with bare SHA-256 and sent them to a
configured endpoint.

`deriveDeviceId` hashed `hostname:username` and truncated to 64 bits, commented
"pseudonymous, stable". An unkeyed digest of a host and username pair is not
pseudonymous against anyone who can guess plausible values: hash the guess,
compare, done. `deriveSpanId` hashed the dedup key — and for pi, zerostack,
lingtai-tui and codebuff that key embeds the raw absolute source path, home
directory included, because the bridge passes `source.path` straight through.
Guess a plausible home and project name and the same confirmation works.

This is the project's own standard, not an outside opinion. Decision D1 requires
a caller-supplied HMAC key for fingerprints precisely so digests of paths cannot
be dictionary-attacked, and core's fingerprint module throws on an empty key to
enforce it. The sync path bypassed the primitive entirely. It also contradicted
the project's own user-facing guarantee: docs/sync/README.md promises that
code, file contents, diffs and PATHS stay local, and the unkeyed span id shipped
absolute paths (for the four providers above) in a form confirmable by anyone
with a plausible guess.

All three ids are now HMAC-SHA256 under the per-install privacy key — the same
key core's fingerprints use — with domain prefixes so one value in two
positions never yields the same digest, and composite inputs joined with the
same ASCII Unit Separator (0x1f) core/fingerprint.ts uses so a value containing
':' cannot forge a field boundary. The derive functions throw on an empty key
rather than degrading. The payload builder obtains the key itself, so the
decode path, which runs with an empty key by design, never reaches it.

Sync now REQUIRES the persisted key: privacy-key.ts exposes a strict variant
that aborts the push instead of falling back to per-process randomness when the
config dir is unwritable, and refuses to silently regenerate a key file that
fails validation (truncated by a full disk, a partial write). Cross-process id
stability is load-bearing — partially rejected batches are not ledgered
precisely because deterministic span ids make full-batch retry safe — so a
per-process fallback key would emit fresh ids on every retry and let the backend
double-count accepted spans, and a silent re-key would orphan everything already
pushed. The fingerprint consumers keep the tolerant fallback: they only need
per-process stability.

The refusal is now complete, and enforced for every corrupt shape: "no file at
all" is the only state a first use may create. A file that exists but is
unreadable, zero-byte or whitespace-only (a partial write), or fails hex
validation aborts the push and is left untouched — treating those as MISSING
would silently regenerate the file and re-key every id, which is exactly the
case the strict path exists to refuse. First creation is also exclusive
(O_CREAT|O_EXCL): when two processes race the first use, the loser re-reads and
adopts the winner's key, so concurrent pushes can never mint different keys and
mix cached device ids with spans derived from the other.

Scope, stated honestly: sync is opt-in and needs an endpoint plus credentials,
the digests are of identifiers rather than prompts or file contents, and this
predates the extraction. It is not an active leak of user content. It is a weak
construction the project already knows how to do properly. This change narrows
the exposure rather than closing it: ai.project still ships a project name in
the clear, and in one Claude fallback path that name is a dash-encoded absolute
path.

Blast radius: every id is re-keyed once at upgrade, so anything already pushed
stops correlating with new sends and the backend sees a fresh device identity.
Ids stay stable afterwards unless the key file is lost. The host-side sent
ledger keys off the raw dedup key and is unaffected, so re-push filtering keeps
working.
2026-08-05 18:37:50 +03:00
..
DEVELOPER.md fix(sync): key the device, span and trace digests 2026-08-05 18:37:50 +03:00
README.md fix(sync): key the device, span and trace digests 2026-08-05 18:37:50 +03:00

codeburn sync

Push your AI usage telemetry to a shared backend so teams can track adoption, budgets, and ROI across developers.

Everything stays local-first: codeburn never sends data without your explicit action, and prompts/code are never included.

Quick Start

# One-time setup (opens browser for login)
codeburn sync setup https://metrics.your-team.com

# Push recent usage
codeburn sync push

# Check status
codeburn sync status

Commands

codeburn sync setup <url>

Configures sync with a remote endpoint. Opens your browser for a one-time OIDC login.

codeburn sync setup https://metrics.your-team.com

What happens:

  1. Fetches server configuration from <url>/.well-known/codeburn-export.json
  2. Opens your browser to the identity provider's login page
  3. After login, stores a refresh token securely in your OS keychain
  4. Saves the endpoint configuration (no secrets) to ~/.config/codeburn/sync.json

You only need to do this once. The token refreshes silently on every push.

codeburn sync push

Sends unsent AI usage data to the configured endpoint.

# Push unsent calls from the last 7 days (default)
codeburn sync push

# Push a larger window
codeburn sync push --since 30d

# Preview what would be sent
codeburn sync push --dry-run

codeburn sync status

Shows the current sync configuration and authentication state.

Endpoint: https://metrics.your-team.com
Traces path: /v1/traces
Issuer: https://auth.your-team.com
Auth: configured
Token storage: keychain
Last sync: 2h ago

codeburn sync logout

Removes stored credentials and revokes the token at the identity provider.

codeburn sync logout

codeburn sync reset --confirm

Clears the sent-ledger, causing the next push to re-send all data in the window. Use after a backend migration or if you suspect missing data.

codeburn sync reset --confirm

What Gets Sent

Each AI interaction becomes one OTLP span with these attributes:

Field Example Description
ai.provider kiro, cursor, claude Which AI tool
ai.model claude-sonnet-4-6 Model used
ai.input_tokens 12500 Prompt tokens
ai.output_tokens 3200 Response tokens
ai.cost_usd 0.085 Estimated cost
ai.project my-app Project name
ai.tools ["Edit", "Bash"] Tools invoked

A keyed device_id (HMAC of hostname and username under a per-install key that never leaves the machine) distinguishes your machines without revealing hostnames.

What is NOT sent

  • Prompts — your actual messages to AI are never included
  • Code — file contents, diffs, and paths stay local
  • Bash commands — may contain secrets, never sent
  • Your name/email — identity is derived server-side from your login token

There is no flag to override this. Privacy is structural, not configurable.

Authentication

Sync uses standard OIDC (the same protocol as "Sign in with Google"). Your team's admin sets up the identity provider — you just click through the browser login once.

  • Token storage: macOS Keychain, Windows Credential Manager, or Linux libsecret. Falls back to a 0600 file if no keychain is available.
  • Token lifetime: typically 3090 days (set by your admin). You'll be prompted to re-login when it expires.
  • Re-login: run codeburn sync setup <url> again.

FAQ

Q: Does sync run automatically? A: No. You run codeburn sync push when you want. A future version may offer opportunistic push (after each codeburn report), but it's always explicit.

Q: What if I push the same data twice? A: Safe. A local sent-ledger tracks what's been sent. Re-pushing the same window doesn't create duplicates.

Q: What if I'm offline for a week? A: Next push catches up. The default window is 7 days; use --since 30d or --since all (up to 6 months) for longer gaps. A push runs to completion regardless of size — server rate limits (429) are waited out automatically.

Q: Can my admin see my prompts? A: No. Prompts are never included in the payload. The server only sees token counts, costs, model names, and project names.

Q: How do I stop syncing? A: codeburn sync logout removes everything. Or just stop running push.