Skip to content

Data flow

[Claude Code] ──hooks──▶ [~/.codika-os/events.db (SQLite)]
│ codika-os sync
┌──────────────────────┐
│ Neon Postgres (app) │
└──────────┬───────────┘
│ /api/tracking/*
[Dashboard UI]

Hook scripts live in codika-os-cli/src/cli/commands/hooks/. The codika-os install command writes five entries into ~/.claude/settings.json, each invoking codika-os hooks <name> with the Claude Code payload piped to stdin.

HookWhen it firesWhat it records
SessionStartNew Claude Code session beginssession id, project path, model, started_at
UserPromptSubmitUser sends a promptprompt length, timestamp, session id
StopAgent finishes a turnstopped_at
SessionEndSession closesended_at
PostToolUseAny tool call completestool name, duration (agent-streaming seconds), session id

Hooks must complete in under 50 ms and never make network calls. They open the SQLite file in WAL mode, append one row, close. Sync is a completely separate process.

~/.codika-os/events.db is a SQLite database, WAL-mode, single-writer in practice. The hooks append; the aggregator reads.

~/.codika-os/config.json (mode 0600) holds your profiles and active CLI key. ~/.codika-os/last-sync is a timestamp file for the --if-stale guard on codika-os sync. The session_sync_state table inside ~/.codika-os/events.db holds per-session fingerprints used by incremental sync (below).

codika-os sync runs:

  1. Fingerprint pre-pass. For every session present locally, the aggregator reads its current user_prompt count (cheap SQL COUNT) and stat()s its JSONL transcript (size + mtime). It compares the triple against the row in session_sync_state. Matches are clean; everything else is dirty. Clean sessions whose conversation root has any dirty sibling are upgraded to dirty so multi-session rollups don’t drift.
  2. codika-os-cli/src/core/aggregator.ts walks the dirty set only, joining hook events from local SQLite with the matching JSONL transcript per session.
  3. core/classifier.ts picks a project per session using longest-prefix path matching against project_path_prefixes. The classifier also records each matched prefix’s subcategory so the session can carry attribution_subcategory (for clients and other multi-scope projects). When no prefix matches, the session is uploaded with project_id = NULL and left for the on-demand agent-inline classifier (see Classification).
  4. core/human-time.ts computes human_seconds using the weighted capped-gap convention. agent_seconds is summed from PostToolUse durations.
  5. core/jsonl-parser.ts computes cost_usd_cents from per-message token counts × MODEL_RATES_PER_MTOK.
  6. core/uploader.ts POSTs the rollup (only for dirty sessions) to /api/sync on codika-os-app, with Authorization: Bearer <cos_…> from the active profile.
  7. On 2xx upload, the aggregator’s fingerprint map is written back to session_sync_state. A failed upload leaves the state untouched so the next run retries the same set.

Sync runs opportunistically when a new Claude Code session starts: the SessionStart hook checks ~/.codika-os/last-sync and, if it’s older than 20 hours, fires codika-os sync --if-stale --json as a detached background subprocess. A lockfile (~/.codika-os/sync.lock, atomic mkdir) ensures only one sync runs at a time. No system cron is involved — this works on any OS Claude Code supports. You can also force a sync any time with /codika-os:sync-codika-os or codika-os sync.

codika-os sync --full clears session_sync_state and re-processes every session. Use it after upgrading the aggregator (better classifier, improved human-time math) or to recover from state corruption.

The endpoint is codika-os-app/src/routes/api/sync/+server.ts. It:

  1. Verifies the cos_… bearer key against cli_api_keys (hashed at rest), via src/lib/server/sync-auth.ts.
  2. Opens a single transaction on a pooled Neon connection (createPooledDb(mode)). All writes commit atomically.
  3. Bulk-upserts conversations, sessions (multi-row INSERT ... ON CONFLICT DO UPDATE, chunks of 500) and bulk-inserts session_events, session_tool_calls (multi-row INSERT ... ON CONFLICT DO NOTHING, deduped by the session_events_dedupe_idx and session_tool_calls_dedupe_idx unique indexes added in migration 0007).
  4. Appends one project_attribution_attempts row per session with method = 'path' (audit trail).
  5. Returns { success: true, data: { mode, duration_ms, conversations_upserted, sessions_upserted, events_inserted, tool_calls_inserted, attempts_inserted } }. A sync_ingest JSON log line is emitted with the same fields — that’s the perf-regression tripwire.

Payload chunking. The CLI splits large syncs (more than 250 sessions) into multiple requests, grouped by conversation_root_id so each request is self-contained. Every chunk is idempotent on the server: re-sending a chunk produces zero duplicate rows.

Sync targets. codika-os sync writes to PROD by default — PROD is the source of truth. Pass --include-dev to additionally mirror to DEV (best-effort: DEV failures warn but don’t abort the command). --mode dev is single-target dev for local testing.

Three sibling endpoints support agent-inline classification of the residual:

  • GET /api/projects — returns the full project catalog (one entry per projects row, with joined path_prefixes and category / subcategory / prompt_only). Used by codika-os sync to load the classifier index.
  • GET /api/tracking/unattributed?month=YYYY-MM — returns sessions with project_id IS NULL for the bearer’s worker, plus the active project catalog grouped by category.
  • POST /api/sync/classifications — accepts agent-produced verdicts (projectSlug + optional subcategory, or suggestedSlug + suggestedCategory + optional suggestedSubcategory) and applies them with method = 'agent-inline'.

The dashboard never writes to tracking tables. All inserts go through these endpoints.

Pages under /tracking use SvelteKit server-loads that call aggregators in codika-os-app/src/lib/server/:

PageAggregator
/tracking (overview)tracking-overview.ts
/tracking/projects/[slug]project-detail.ts
/tracking/sessions/[id]session-detail.ts
/timesheetsession-projections.ts
GET /api/projects (catalog for CLI sync)routes/api/projects/+server.ts
GET /api/tracking/unattributed (agent-inline classifier input)routes/api/tracking/unattributed/+server.ts
POST /api/sync/classifications (agent-inline classifier output)routes/api/sync/classifications/+server.ts

The aggregators are the source of truth for what each page sees. If a number on a page looks wrong, the aggregator is where you look — not the schema, not the CLI.

If you need to change…Look here
What gets captured by hookscodika-os-cli/src/cli/commands/hooks/*.ts
Project classification rulescodika-os-cli/src/core/classifier.ts
Project taxonomy (categories, subcategories, discovery rules)codika-os-app/seeds/projects.yaml + scripts/syncProjects.ts
Human-time mathcodika-os-cli/src/core/human-time.ts
Token cost math + model ratescodika-os-cli/src/core/jsonl-parser.ts
Sync payload shapecodika-os-cli/src/core/uploader.ts + src/types/api.ts
Incremental skip / fingerprintcodika-os-cli/src/core/aggregator.ts, core/local-db.ts (session_sync_state)
/api/sync ingestioncodika-os-app/src/routes/api/sync/+server.ts
Schemacodika-os-app/src/lib/db/schema.ts
Migrationscodika-os-app/drizzle/*.sql

A change in any of these usually requires an update to the matching file in another repo (e.g. schema change → CLI uploader update → docs update). The four repos are versioned independently but documented as one system.

  • Dashboard auth — Better Auth, the /login page on codika-os-app. Cookies-and-sessions. Used by humans to view the dashboard.
  • CLI auth — OTP-minted cos_… keys at /api/auth/cli/*. Hashed in cli_api_keys. Used by the CLI to talk to /api/sync.

They’re separate. Logging in to one doesn’t log you in to the other. dashboard_access is read from prod regardless of the app_mode cookie; tracking data queries follow the cookie (prod default).