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. |
||
|---|---|---|
| .. | ||
| DEVELOPER.md | ||
| README.md | ||
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:
- Fetches server configuration from
<url>/.well-known/codeburn-export.json - Opens your browser to the identity provider's login page
- After login, stores a refresh token securely in your OS keychain
- 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
0600file if no keychain is available. - Token lifetime: typically 30–90 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.