Summary
Add an export subcommand that sends AI usage telemetry to a remote OpenTelemetry-compatible endpoint, authenticated via OIDC browser login with refresh tokens stored in the OS credential store.
Motivation
codeburn tracks AI usage locally per-developer. Teams want to aggregate this data across developers for:
- Adoption dashboards — which tools/models are being used, by whom, how much
- Budget tracking — team-level AI spend with alerts
- ROI measurement — correlate AI usage with velocity/quality metrics
Today there's no standard way to get codeburn data off a developer's machine into a shared backend. Teams resort to custom scripts parsing codeburn report --format json output. A first-class export with secure auth solves this cleanly.
UX
Setup (one-time)
codeburn export setup https://metrics.example.com
What happens:
- Fetches
https://metrics.example.com/.well-known/openid-configuration
- Opens system browser for OIDC Authorization Code + PKCE login
- Stores refresh token in OS credential store (Keychain / Credential Manager / libsecret)
- Saves
{ "baseUrl": "https://metrics.example.com" } to ~/.config/codeburn/export.json
That's it. One URL, zero prompts. The base URL serves as both the OIDC provider and the telemetry endpoint — .well-known provides auth config, and a known subpath (e.g. /v1/traces) receives OTEL data.
Export — Manual (on-demand)
# Export last 7 days (default)
codeburn export push
# Export specific period
codeburn export push --since 30d
# Dry-run (show what would be sent)
codeburn export push --dry-run
What happens:
- Reads refresh token from OS credential store
- Exchanges for access token (silent refresh, no browser)
- Collects
ParsedProviderCall data for the period (same data as codeburn report)
- Sends as OTEL spans/metrics to
{baseUrl}/v1/traces with Authorization: Bearer <access_token>
- Prints summary: "Exported 234 calls ($45.20) to metrics.example.com"
Export — Daemon (daily auto-export)
# Enable daily auto-export
codeburn export daemon start
# Check daemon status
codeburn export daemon status
# Disable
codeburn export daemon stop
What happens:
- Registers a daily background job (platform-native: launchd plist on macOS, systemd user timer on Linux, Task Scheduler on Windows)
- Runs once per day (configurable time, default: 9am local)
- Exports the previous day's activity (midnight-to-midnight)
- Silently succeeds or logs failures to
~/.cache/codeburn/export.log
- No terminal output, no user interaction — fully headless
The daemon uses the same stored refresh token as manual export. If the token expires and can't refresh, it logs a warning and stops retrying until the user runs codeburn export setup again.
// ~/.config/codeburn/export.json (with daemon enabled)
{
"baseUrl": "https://metrics.example.com",
"clientId": "codeburn-cli",
"lastExport": "2026-07-05T14:00:00Z",
"daemon": {
"enabled": true,
"schedule": "09:00",
"lastRun": "2026-07-05T09:00:02Z",
"lastStatus": "ok"
}
}
Status / Logout
# Check config and auth status
codeburn export status
# Output: Endpoint: https://metrics.example.com | Auth: valid (expires in 47d) | Last export: 2h ago
# Remove stored credentials
codeburn export logout
Auth Design
OIDC Flow
- Grant type: Authorization Code with PKCE (public/native client — no client secret)
- Redirect URI:
http://localhost:<ephemeral-port>/callback (temporary server during login, same pattern as aws sso login, gcloud auth login, gh auth login)
- Scopes:
openid offline_access (offline_access provides refresh token)
- Token refresh: Automatic on every
export push — transparent to user
- Re-login: Only required when refresh token expires or is revoked
Credential Storage
| Platform |
Store |
Suggested implementation |
| macOS |
Keychain |
keytar or security CLI |
| Windows |
Credential Manager |
keytar or @aspect-build/wincred |
| Linux |
libsecret (GNOME Keyring / KWallet) |
keytar or secret-tool CLI |
keytar is the cross-platform standard (used by VS Code, GitHub CLI, Azure CLI). If keytar native module is undesirable for a pure-JS tool, fall back to encrypted file with OS-derived key.
Token Lifetime and Rotation
Recommended refresh token lifetime: 90 days. Balances convenience (quarterly re-auth) against exposure risk for a low-blast-radius telemetry endpoint.
Rotation: supported but not required. OIDC providers vary:
| Provider |
Rotation |
Notes |
| Auth0 / Okta / Azure AD |
✅ On by default for public clients |
Issues new RT on each use, invalidates old |
| Keycloak / Ory Hydra / Zitadel |
✅ Configurable |
Off by default, enable per-client |
| Cognito / Google |
❌ Not supported |
RT is reusable for full lifetime |
codeburn's behavior is provider-agnostic:
- After every token refresh, store whatever refresh token the server returns (new or same)
- If the server returns a new RT (rotation), overwrite the old one in keychain immediately
- If the server returns the same RT (no rotation), keep using it
- On
invalid_grant error — don't retry, prompt user: "Export auth expired. Run codeburn export setup"
This works correctly with Cognito (no rotation, 90-day reusable token), Auth0 (rotation on, new RT each use), and everything in between. The client doesn't need to know whether rotation is active.
Server-side recommendation (for reference implementations):
- Enable rotation if the provider supports it
- Set reuse grace window to 30 seconds (handles network-drop retries)
- Enable token family invalidation on reuse detection (theft signal)
- For non-rotating providers (Cognito): rely on the 90-day expiry as the primary security boundary
Config File (non-secret)
// ~/.config/codeburn/export.json
{
"baseUrl": "https://metrics.example.com",
"clientId": "codeburn-cli",
"lastExport": "2026-07-05T14:00:00Z"
}
No tokens, secrets, or credentials in this file. clientId is auto-discovered from .well-known response or defaults to "codeburn-cli".
Data Schema
Each ParsedProviderCall becomes one OTEL span with these attributes:
ai.provider = "kiro" | "cursor" | "claude" | ...
ai.model = "claude-sonnet-4-6"
ai.input_tokens = 12500
ai.output_tokens = 3200
ai.cost_usd = 0.085
ai.project = "sample-prism-d1-velocity"
ai.session_id = "421b64df-..."
ai.tools = ["Edit", "Bash", "Read"]
ai.speed = "standard"
ai.cost_estimated = true | false
Data minimization (critical)
NOT exported by default:
userMessage — contains actual prompts (PII, proprietary code context)
- File paths — may leak repo structure
- Bash commands — may contain secrets
Opt-in flag for teams that want it:
codeburn export push --include-messages
Server-side Contract
The server at baseUrl must implement:
GET /.well-known/openid-configuration — standard OIDC discovery
POST /v1/traces — accepts OTEL protobuf or JSON, validates Bearer token
This is intentionally minimal. Any OTEL-compatible backend (Grafana Alloy, AWS ADOT Collector, Datadog Agent) behind an OIDC-aware reverse proxy satisfies it.
.well-known requirement
If /.well-known/openid-configuration returns 404 or is unreachable, exit with an error:
Error: https://metrics.example.com/.well-known/openid-configuration not found.
The server must implement OIDC discovery. See https://openid.net/specs/openid-connect-discovery-1_0.html
No fallback, no prompts, no alternative auth methods. The server either supports the protocol or it doesn't.
Alternatives Considered
| Approach |
Why not |
| API key in env var |
Insecure, easy to leak, no rotation |
| API key in config file |
File permissions are unreliable across OS |
| mTLS client certs |
Complex setup, cert rotation burden |
| Multiple auth methods |
Complexity for marginal gain — one protocol, one flow, zero config |
OIDC + keychain is the same pattern as aws sso login, gcloud auth login, gh auth login — developers already understand it. If the server doesn't support .well-known, it doesn't support codeburn export.
Implementation Notes
Gap Detection and Backfill
The daemon exports "yesterday" — but what if yesterday's export failed, or the machine was off for a week?
Approach: watermark-based catch-up
The config tracks a lastSuccessfulExport timestamp (the newest data point successfully sent). On each daemon run:
- Compare
lastSuccessfulExport to now
- If gap > 1 day, export the entire gap (capped at 30 days to avoid first-run flood)
- Update
lastSuccessfulExport only after the server acknowledges receipt
{
"baseUrl": "https://metrics.example.com",
"lastSuccessfulExport": "2026-07-03T00:00:00Z"
}
If today is Jul 5 and last successful was Jul 3 → daemon exports Jul 3 + Jul 4 in one batch before exporting today's window.
First-run behavior: lastSuccessfulExport doesn't exist → export last 7 days as initial seed (configurable via --seed-days). Not the full 6-month history — that's what codeburn export push --since 6m is for.
Manual backfill: If someone was offline for months:
codeburn export push --since 90d
This explicitly backfills the gap. The daemon's 30-day cap prevents accidental flood; manual push has no cap (other than codeburn's 6-month data retention).
Deduplication: The server uses deduplicationKey (already present on every ParsedProviderCall) as an idempotency key. Re-exporting the same period is safe — duplicates are ignored server-side. This means gap-fill and manual push can overlap without double-counting.
Failure modes:
| Scenario |
Behavior |
| Network down |
Log error, lastSuccessfulExport unchanged, retry next run |
| Token expired (refresh fails) |
Log warning, stop daemon retries, notify on next interactive codeburn run: "Export auth expired. Run codeburn export setup to re-authenticate." |
| Server returns 5xx |
Retry with backoff (3 attempts), then defer to next day |
| Server returns 409 (duplicate) |
Treat as success, advance watermark |
| Machine off for 2 weeks |
Next daemon run exports the full 14-day gap in one batch |
| Machine off for 2 months |
Daemon exports last 30 days, prints note: "Gap exceeds 30d. Run codeburn export push --since 60d to backfill fully." |
- The export should be idempotent — re-exporting the same period deduplicates via
deduplicationKey as span ID
- Batch size: send in chunks of ~1000 spans to avoid payload limits
- Retry: exponential backoff on 429/5xx, fail-fast on 401 (trigger re-login prompt)
- Offline queue: if export fails, optionally queue to
~/.cache/codeburn/export-queue/ for retry on next run
--since should respect the same 6-month lookback cap as codeburn report -p all
Future Extensions
- Team enrollment — server returns team config (which fields to include, export frequency) during setup
- Anonymization mode — hash session IDs and project names for privacy-sensitive deployments
- Export hooks — trigger export automatically after each
codeburn report / dashboard refresh
Summary
Add an
exportsubcommand that sends AI usage telemetry to a remote OpenTelemetry-compatible endpoint, authenticated via OIDC browser login with refresh tokens stored in the OS credential store.Motivation
codeburn tracks AI usage locally per-developer. Teams want to aggregate this data across developers for:
Today there's no standard way to get codeburn data off a developer's machine into a shared backend. Teams resort to custom scripts parsing
codeburn report --format jsonoutput. A first-class export with secure auth solves this cleanly.UX
Setup (one-time)
codeburn export setup https://metrics.example.comWhat happens:
https://metrics.example.com/.well-known/openid-configuration{ "baseUrl": "https://metrics.example.com" }to~/.config/codeburn/export.jsonThat's it. One URL, zero prompts. The base URL serves as both the OIDC provider and the telemetry endpoint —
.well-knownprovides auth config, and a known subpath (e.g./v1/traces) receives OTEL data.Export — Manual (on-demand)
What happens:
ParsedProviderCalldata for the period (same data ascodeburn report){baseUrl}/v1/traceswithAuthorization: Bearer <access_token>Export — Daemon (daily auto-export)
What happens:
~/.cache/codeburn/export.logThe daemon uses the same stored refresh token as manual export. If the token expires and can't refresh, it logs a warning and stops retrying until the user runs
codeburn export setupagain.Status / Logout
Auth Design
OIDC Flow
http://localhost:<ephemeral-port>/callback(temporary server during login, same pattern asaws sso login,gcloud auth login,gh auth login)openid offline_access(offline_access provides refresh token)export push— transparent to userCredential Storage
keytarorsecurityCLIkeytaror@aspect-build/wincredkeytarorsecret-toolCLIkeytaris the cross-platform standard (used by VS Code, GitHub CLI, Azure CLI). Ifkeytarnative module is undesirable for a pure-JS tool, fall back to encrypted file with OS-derived key.Token Lifetime and Rotation
Recommended refresh token lifetime: 90 days. Balances convenience (quarterly re-auth) against exposure risk for a low-blast-radius telemetry endpoint.
Rotation: supported but not required. OIDC providers vary:
codeburn's behavior is provider-agnostic:
invalid_granterror — don't retry, prompt user: "Export auth expired. Runcodeburn export setup"This works correctly with Cognito (no rotation, 90-day reusable token), Auth0 (rotation on, new RT each use), and everything in between. The client doesn't need to know whether rotation is active.
Server-side recommendation (for reference implementations):
Config File (non-secret)
No tokens, secrets, or credentials in this file.
clientIdis auto-discovered from.well-knownresponse or defaults to"codeburn-cli".Data Schema
Each
ParsedProviderCallbecomes one OTEL span with these attributes:Data minimization (critical)
NOT exported by default:
userMessage— contains actual prompts (PII, proprietary code context)Opt-in flag for teams that want it:
codeburn export push --include-messagesServer-side Contract
The server at
baseUrlmust implement:GET /.well-known/openid-configuration— standard OIDC discoveryPOST /v1/traces— accepts OTEL protobuf or JSON, validates Bearer tokenThis is intentionally minimal. Any OTEL-compatible backend (Grafana Alloy, AWS ADOT Collector, Datadog Agent) behind an OIDC-aware reverse proxy satisfies it.
.well-known requirement
If
/.well-known/openid-configurationreturns 404 or is unreachable, exit with an error:No fallback, no prompts, no alternative auth methods. The server either supports the protocol or it doesn't.
Alternatives Considered
OIDC + keychain is the same pattern as
aws sso login,gcloud auth login,gh auth login— developers already understand it. If the server doesn't support.well-known, it doesn't support codeburn export.Implementation Notes
Gap Detection and Backfill
The daemon exports "yesterday" — but what if yesterday's export failed, or the machine was off for a week?
Approach: watermark-based catch-up
The config tracks a
lastSuccessfulExporttimestamp (the newest data point successfully sent). On each daemon run:lastSuccessfulExportto nowlastSuccessfulExportonly after the server acknowledges receipt{ "baseUrl": "https://metrics.example.com", "lastSuccessfulExport": "2026-07-03T00:00:00Z" }If today is Jul 5 and last successful was Jul 3 → daemon exports Jul 3 + Jul 4 in one batch before exporting today's window.
First-run behavior:
lastSuccessfulExportdoesn't exist → export last 7 days as initial seed (configurable via--seed-days). Not the full 6-month history — that's whatcodeburn export push --since 6mis for.Manual backfill: If someone was offline for months:
codeburn export push --since 90dThis explicitly backfills the gap. The daemon's 30-day cap prevents accidental flood; manual push has no cap (other than codeburn's 6-month data retention).
Deduplication: The server uses
deduplicationKey(already present on everyParsedProviderCall) as an idempotency key. Re-exporting the same period is safe — duplicates are ignored server-side. This means gap-fill and manual push can overlap without double-counting.Failure modes:
lastSuccessfulExportunchanged, retry next runcodeburnrun: "Export auth expired. Runcodeburn export setupto re-authenticate."codeburn export push --since 60dto backfill fully."deduplicationKeyas span ID~/.cache/codeburn/export-queue/for retry on next run--sinceshould respect the same 6-month lookback cap ascodeburn report -p allFuture Extensions
codeburn report/ dashboard refresh