Master the context.
Coodra is a local-first MCP server platform that gives AI coding agents project context, memory across sessions, and policy enforcement — so they stop coding blind. This is the complete reference, from your first coodra init to merging your first PR.
What Coodra is, in one breath.
~3 min
A coordination layer between human architects and AI coding agents — Claude Code, Codex. It runs as a local-first MCP (Model Context Protocol) server and feeds every agent session three things, automatically.
Feature Packs
The architectural blueprint for the module the agent is touching. Injected at session start so the agent reads spec → tech-stack → implementation order before writing code.
Context Packs
The durable record of every decision, file change, and policy event from past sessions. One pack per run, queryable in natural language. Cross-session memory the agent can pull on.
Policy enforcement
Pre-tool-use checks recorded in an append-only audit trail. deny writes to .env.production, ask before merging, fail-open on engine fault.
/01.1Design principles
Five things that are true everywhere in the codebase, and that any contribution must respect.
createDb({ kind: 'local' })FAIL_OPEN_RESULTresumed_at. ADR-007DATABASE_URL.Five minutes to your first run.
Zero network
If you have Node 22, pnpm, and a Claude Code / Codex install — you're three commands away. Solo mode is the default after coodra init.
Confirm the prereqs.
You need Node 22.16.0 (pinned in .nvmrc), pnpm 10.33.0 via corepack, git, and a supported agent installed locally. macOS uses launchd; Linux uses a fallback daemon manager with PID files.
# Versions Coodra pins to nvm use # 22.16.0 per .nvmrc corepack enable # activates pnpm 10.33.0 node --version # → v22.16.0 pnpm --version # → 10.33.0
Install the CLI.
The only published artefact is @coodra/cli. It ships as a self-contained bundle — bring-your-own SQLite native module is rebuilt on install.
npm i -g @coodra/cli # or, in a workspace: pnpm add -D @coodra/cli
Initialise the project.
From your repo root, run coodra init. It registers the project under .coodra/, seeds the default policy, and leaves repo-root .mcp.json to the user. Native Coodra plugins carry the MCP server and hook wiring.
coodra init # picks the slug from git remote, asks which IDE, # defaults to --solo. Use --team to wire Clerk. # What it creates: .coodra/config.json {"projectSlug":"my-app"} .coodra/manifest.json project registration ledger ~/.coodra/data.db SQLite primary store ~/.coodra/.env solo-mode env, ready to grow into team
Start the daemons.
One command brings up the MCP server (3100), hooks bridge (3101), and the web UI on 3001. Sync-daemon stays asleep in solo mode. Add --no-web to skip the dashboard, --foreground to tail logs.
coodra start # → mcp-server listening on 127.0.0.1:3100 # → hooks-bridge listening on 127.0.0.1:3101 # → web-v2 http://localhost:3001 # Confirm everything's alive coodra status coodra doctor # 11 essential checks (38 with --full)
Open the agent & watch.
Launch Claude Code in your repo. The SessionStart hook fires POST /v1/hooks/claude-code, opens a runs row, captures base_sha, and returns a Feature Pack body in the additionalContext field. Every tool use after that is gated by policy and logged in run_events.
Visit http://localhost:3001/runs to watch the trace populate live.
Now skim Core concepts to understand what's happening under the hood, or jump to the CLI reference for every subcommand.
Two modes. Same primitives.
COODRA_MODEenv flag
Solo and Team mode share schema, code, and CLI. The difference is who sees what, and whether the sync-daemon boots.
Everything on your laptop.
SQLite at ~/.coodra/data.db. Loopback only. Solo-bypass auth — no Clerk, no JWT, just SOLO_ACTOR stamped on every write. The sync-daemon does not boot. Zero network egress.
Best for: individual contributors, OSS hacking, air-gapped envs, contractors with a strict no-cloud policy.
Cloud-synced memory.
Same local SQLite — plus a Postgres + pgvector mirror you operate, plus Clerk for identity. The sync-daemon pushes sync_to_cloud outbox rows and pulls teammates' runs, decisions, and packs back into local SQLite.
Best for: small teams sharing context, multi-laptop developers, BYO-cloud self-host.
/03.1Mode matrix
| Capability | Solo | Team |
|---|---|---|
| Local SQLite primary store | YES | YES |
| MCP server + hooks bridge (loopback) | YES | YES |
Web UI on :3001 | YES | YES |
| Sync-daemon boots | NO | YES |
| Clerk auth · JWT verification | BYPASS | REQUIRED |
| Cross-teammate visibility | NO | YES |
| Cross-machine (same user) | NO | VIA team install |
| Network egress required | NONE | POSTGRES + CLERK |
Promote a project from solo to team without losing history with coodra team setup → coodra cloud-migrate. The migration is tracked in _migration_attempts with per-row mapping in _migration_map for surgical rollback.
The vocabulary.
It pays back.
Six nouns carry the whole system. Internalise these and the rest of the docs read themselves.
runsruns table — agent type, mode, status, base_sha, created_by_user_id, project_id. Bound to a session by get_run_id; reused across get_run_id calls within the same session. One run per session per projectcontext_packs (one row per run, UNIQUE constraint), mirrored to ~/.coodra/packs/<date>-<runId>.md for grep. source: 'agent' | 'bridge_auto'.coodra/recipes/<slug>/. Pulled on demand via list_recipes / get_recipe when a task matches a recipe's trigger — not auto-injected.sha256(description)[:32]. Surfaced to the next session via recent-decisions on UserPromptSubmitpd:{sessionId}:{toolUseId}:{toolName}:{eventType}. ADR-007/04.1Runs & sessions
A session is what the agent calls itself. A run is what Coodra calls that work. The first call to get_run_id for a session either opens a new runs row, or — if the agent crashed and restarted — finds and resumes the existing one.
// MCP tool: get_run_id · apps/mcp-server/src/tools/get-run-id/ const { runId } = await mcp.call("get_run_id", { sessionId: "cc-sess-2026-05-16-8a3f2c", projectSlug: "my-app", agentType: "claude_code", }); // First call: inserts a runs row, returns runId. // Repeat call: returns the same runId (UNIQUE on project_id, session_id).
Every run_events row, every policy_decisions row, and the eventual context_packs row hang off this runId. run_events.run_id uses ON DELETE SET NULL so PreToolUse events that fire before the run exists still land — they just attach later when the row appears.
/04.2Feature Packs & Context Packs
The two are easy to confuse. They sit on opposite ends of the run.
Feature Pack
Human- or agent-authored. Lives in .coodra/recipes/<slug>/. Reusable guidance the agent pulls on demand via list_recipes / get_recipe when a task matches its trigger.
Direction: human → agent.
Context Pack
Agent-authored (or bridge-auto on session end). One row per run in context_packs with title, content, content_excerpt, meta. Queryable cross-session via search_packs_nl, list_context_packs, read_context_pack.
Direction: agent → future agents.
If a saved pack has source: 'agent' and a new save_context_pack call arrives with the same runId, the response is status: 'idempotent_hit'. If the existing pack was bridge-auto, it gets 'upgraded_from_bridge_auto' in place.
/04.3Decisions
An ADR-grade record the agent emits when it makes a non-trivial choice: pick an ORM, change a public API shape, defer a refactor. They survive across sessions and surface to the next agent via the UserPromptSubmit hook.
await mcp.call("record_decision", { runId, description: "Use better-sqlite3 instead of sql.js for primary store", rationale: "Synchronous API matches MCP request lifecycle; ~3x faster on writes.", alternatives: ["sql.js", "libsql/Turso"], confidence: "high", reversible: false, }); // Idempotent on dec:{runId}:{sha256(description)[:32]} // Resurfaces on next UserPromptSubmit via recent-decisions.ts
/04.4Policy & kill switches
Two layers of guardrail. Kill switches sit above policy — they're operator-level pauses, evaluated first.
(scope, target) — global, project, tool, agent_type. 5s in-process cache. Soft-resume keeps the row.timeout(100ms) + circuitBreaker(5) fuse. Picomatch path globs. First match wins by priority ASC.policy_decisions with ON CONFLICT (idempotency_key) DO NOTHING. Async; the hook returns before the write.{ decision: 'allow', reason: 'policy_check_unavailable' }.Policy rules match on five axes — event type, tool name, path glob, agent type, and priority. Decisions are append-only. The default policy seed lives in packages/db/src/ensure-default-policy.ts.
Process topology.
3 daemons
Four long-running processes plus a one-shot CLI. The agent enters at the top; the cloud (when in team mode) sits at the bottom.
/05.1Long-running processes
| Process | Port | Bind | DB | Started by |
|---|---|---|---|---|
mcp-server16 MCP tools · stdio + HTTP transport |
3100 |
127.0.0.1 |
SQLite (local) | coodra start · native plugin subprocess |
hooks-bridgeHono HTTP · 5 Claude Code events |
3101 |
127.0.0.1 |
SQLite (local) | coodra start |
sync-daemonPush + pull · kill-switch puller |
none | n/a | SQLite + Postgres | coodra start · team mode only |
web-v2Next.js 15 · 46 page/route files |
3001 |
0.0.0.0 |
SQLite (server actions) | coodra start · skip with --no-web |
/05.2Hook lifecycle
Exactly what happens between you typing into Claude Code and an edit landing on disk.
runs row, captures base_sha, returns Feature Pack body via additionalContext.allow/ask/deny. Outbox writes policy_decisions.run_events row with phase, tool name, tool input, outcome.runs.status = completed, runs run-diff-runner to populate run_diffs, writes auto-Context Pack.Data model.
17 + 3 cloud-only
Drizzle dual-dialect schemas. Postgres mirrors SQLite column-for-column, enforced by __tests__/unit/schema-parity.test.ts. The only intentional drift: context_packs.summary_embedding is vector(384) in Postgres, text in SQLite.
| Table | Purpose | Key constraints |
|---|---|---|
projects | Project registry. One per repo (by slug). | slug UNIQUE · org_id |
runs | One per agent session. Bound to project. | UNIQUE (project_id, session_id) |
run_events | Append-only PostToolUse log. | FK→runs · ON DELETE SET NULL |
context_packs | One markdown summary per run. | UNIQUE (run_id) |
decisions | Cross-session architectural choices. | UNIQUE idempotency_key |
policies · policy_rules | Active policies and their rule axes. | UNIQUE (policy_id, priority, …) |
policy_decisions | Append-only audit of every PreToolUse. | UNIQUE idempotency_key |
kill_switches | Polymorphic operator pauses. | idx (resumed_at, scope, target) |
feature_packs · features | Authored blueprints + on-demand skills. | UNIQUE (project_id, slug) |
run_diffs | Unified diff captured at session end. | PK = run_id · CASCADE |
pending_jobs | Durable outbox. Module 03.1. | idx (queue, status, run_after) |
team_invites | HMAC-signed onboarding tokens. | UNIQUE jti |
context_packs_vec | sqlite-vec virtual table for semantic search. | virtual |
/06.1ER summary
projects ─1───n─ runs ─1───n─ run_events │ │ ├─0/1─ context_packs (UNIQUE run_id) │ │ ├─0/1─ run_diffs (PK run_id, CASCADE) │ │ └─0/n─ decisions (FK ON DELETE SET NULL) │ │ │ └──1───n─ policies ─1───n─ policy_rules │ │ │ └─ policy_decisions.matched_rule_id │ ├─ context_packs.project_id ├─ policy_decisions.project_id └─ features.project_id (UNIQUE project_id, slug) # Top-level (not FK'd to projects): feature_packs, kill_switches, team_invites, pending_jobs # Sentinel projects (seeded at every boot): '__global__' ensureGlobalProject() # events before a real project '__solo__' SOLO_ACTOR.orgId # solo-mode org # Cloud-only mirrors (Postgres): _migration_attempts ─1───n─ _migration_map # migrate resume + rollback knowledge_audit # Phase F.3.c audit
/06.2What lives in ~/.coodra/
COODRA_SQLITE_PATH. Managed by coodra db backup / restorecontext_packs rows. Filename: {date}-{sanitised-runId}.md. Gitignored. Useful for grep + AI agent file-side readsgraphify scan. Read-only consumer in apps/mcp-server/src/lib/graphify.ts. No in-repo producercoodra logs <service> --followlaunchctl print.init, login, team setup.coodra login. Consumed by the MCP server identity guard.coodra team setup output./06.3Sync & multi-tenancy
In team mode, the sync-daemon pushes outbox rows to cloud Postgres and pulls teammates' rows back into local SQLite — both directions on a 1-second tick.
['sync_to_cloud']. Every record_decision, save_context_pack, etc. enqueues a paired job that the daemon drains.runs · decisions · context_packs · run_events cloud → local via ON CONFLICT DO NOTHING. Skips __global__ + __solo__.Every shared table carries Clerk user attribution: created_by_user_id on runs, decisions, context_packs, policies; paused_by_user_id / resumed_by_user_id on kill_switches. Web-v2 enforces single-tenant via COODRA_EXPECTED_ORG_ID — a deployment refuses to boot without it.
CLI commands.
38 doctor checks
Every command is a thin shell over a typed core module. Pass --json almost anywhere for machine output. Pass --help for the up-to-date flag list.
/07.1Lifecycle commands
| Command | What it does | Key flags |
|---|---|---|
coodra init | Bootstrap a project. Writes .coodra/config.json + .coodra/manifest.json, seeds the default policy, and leaves repo-root .mcp.json user-owned. | --solo · --team · --ide · --feature-pack · --force · --dry-run |
coodra start | Bring up all daemons. Picks launchd / fallback manager based on platform. | --no-mcp · --no-hooks · --no-sync · --no-web · --foreground · --tunnel |
coodra stop | Tear down daemons. --uninstall removes launchd plists. | --service · --uninstall |
coodra status | Health snapshot of each daemon + DB. | --json |
coodra doctor | 11 essential checks by default. Add --full for all 38. | --full · --fix · --timeout-ms · --json |
coodra agents | Detect installed IDEs and confirm each is wired to the bridge. | --json |
coodra logs <svc> | Tail ~/.coodra/logs/<svc>.log across platforms. | --follow · --lines · --since |
coodra upgrade | Compares installed version against npm view. Restarts daemons unless --no-restart. | --check-only · --no-restart |
coodra uninstall | Removes the CLI. --purge deletes ~/.coodra/. | --purge · --dry-run |
/07.2Data & memory commands
| Command | Subcommands | Use case |
|---|---|---|
coodra project | list · show · reset · promote · demote | Manage which projects Coodra tracks; promote a transient project to active. |
coodra run | list · show · cancel | Inspect or cancel in-flight runs. |
coodra pack | new · list · show · regenerate · delete | Author or regenerate Context Packs. |
coodra feature | add · list · show · edit · index · remove | Manage on-demand skill rows (pull-on-trigger features). |
coodra policy | list · show · add · enable · disable | Author and toggle policy rules per project. |
coodra template | list · install <source> | Install community feature-pack templates. |
coodra export <runId> | — | Export a run as markdown · json · html · slack. With --include-audit for the full policy trail. |
coodra db | migrate · backup · restore | Drizzle migrations + snapshot management. |
/07.3Team & auth commands
| Command | Subcommands | What it does |
|---|---|---|
coodra login | — | Browser handoff to Clerk hosted sign-in. Captures JWT via loopback listener. |
coodra logout | — | Clears ~/.coodra/clerk-token.json. --force skips confirmation. |
coodra invite <email> | — | Mints an HMAC-signed invite URL + writes team_invites row in cloud. |
coodra org | status · switch <slug> | Inspect or switch the active Clerk org for this machine. |
coodra team | setup · init · install · join · leave · migrate · login · logout | Full team lifecycle. setup provisions Clerk + DB; install redeems an invite on a second laptop. |
coodra pause | — | Trip a kill switch. Scope = global · project · tool · agent_type; mode = hard · soft. |
coodra resume | — | Un-pause. --id, --all, or scope+target. |
coodra cloud-migrate | — | One-shot SQLite → Postgres migration. Tracked in _migration_attempts with surgical rollback. |
coodra ui | — | Foreground Ink TUI for live triage. |
The coodra team namespace re-exports login and logout as aliases — both coodra login and coodra team login work. The doctor (--full) runs 38 numbered checks under packages/cli/src/doctor/checks/; the README's claim of "20-check health report" is divergent and being updated.
MCP tools — all 16.
tools/index.ts
Order matches registration order in apps/mcp-server/src/tools/index.ts. There are no jira_* or github_* tools — older docs that mention them are stale.
| # | Tool | Kind | What it does |
|---|---|---|---|
01 | ping | read | Health check. Returns { pong, serverTime, sessionId, idempotencyKey }. |
02 | get_run_id | write | Idempotently opens or returns a runs row for this (project, session). |
03 | get_feature_pack | read | Returns the module blueprint — spec, implementation, techstack, meta. |
04 | save_context_pack | write | Persists the agent-authored Context Pack. Returns created · idempotent_hit · upgraded_from_bridge_auto. |
05 | search_packs_nl | read | Keyword LIKE search. Agent ranks. Ordered by recency, not relevance. |
06 | record_decision | write | Idempotent on (runId, sha256(description)[:32]). Surfaces on next session. |
07 | query_run_history | read | Recent runs + attached pack title, chronological. |
08 | check_policy | write | Returns allow · ask · deny. Fail-open on evaluator faults. |
09 | query_codebase_graph | read | Reads ~/.coodra/graphify/<slug>/graph.json. Requires external graphify scan. |
10 | query_decisions | read | Cross-session decision history. |
11 | list_context_packs | read | Paginated, newest-first. |
12 | read_context_pack | read | Full body + decisions for one pack. |
13 | list_features | read | Skill discovery (filtered to status='published'). |
14 | get_feature | read | Pull-on-trigger; returns frontmatter + body + file list. |
15 | get_feature_file | read | Reads one supporting file from a feature. 256 KB hard cap. Path-escape guard. |
16 | query_run_diff | read | Unified diff + per-file numstat. Soft failures: analysis_pending, no_base_sha, git_diff_failed. |
/08.1Hook events
Wired by native Coodra plugins. Body validated by ClaudeCodeHookPayloadSchema; fail-open on invalid JSON.
| Event | Handler | Side effects |
|---|---|---|
SessionStart | handlers/session-start.ts | Opens / resumes run · captures base_sha · returns Feature Pack body |
UserPromptSubmit | handlers/user-prompt-submit.ts | Injects recent decisions |
PreToolUse | handlers/pre-tool-use.ts | Kill-switch · policy.check · writes policy_decisions |
PostToolUse | handlers/post-tool-use.ts | Inserts run_events row |
Stop · SessionEnd | handlers/session-end.ts | Mark completed · run-diff-runner · auto-Context Pack |
Configuration.
~/.coodra/.env
Defaults are sensible. You usually only need to touch COODRA_MODE, DATABASE_URL, and the Clerk pair. Everything else has a documented fallback.
| Variable | Default | Purpose |
|---|---|---|
COODRA_MODE | solo | solo | team. Controls auth strategy and whether sync-daemon boots. |
DATABASE_URL | none | Postgres connection string. Required in team mode. |
CLERK_SECRET_KEY | sk_test_replace_me | Sentinel value triggers solo bypass; real key required in team mode. |
CLERK_PUBLISHABLE_KEY | pk_test_replace_me | Same sentinel pattern. |
CLERK_JWT_ISSUER | none | Tenant JWT issuer URL for Clerk JWT verification. |
LOCAL_HOOK_SECRET | none | Shared secret for X-Local-Hook-Secret header. Required outside solo bypass. |
COODRA_EXPECTED_ORG_ID | none | Single-tenant pin. Web-v2 refuses to boot without it in team mode. |
COODRA_INVITE_HMAC_SECRET | none | HMAC signing key for invite tokens. |
| Variable | Default | Purpose |
|---|---|---|
MCP_SERVER_PORT | 3100 | HTTP transport port. |
MCP_SERVER_HOST | 127.0.0.1 | HTTP bind. |
MCP_SERVER_TRANSPORT | both | stdio · http · both. |
HOOKS_BRIDGE_PORT | 3101 | Hono listener. |
HOOKS_BRIDGE_HOST | 127.0.0.1 | Hono bind. |
WEB_APP_PORT | 3001 | Next.js port for web-v2. |
COODRA_SQLITE_PATH | ~/.coodra/data.db | Override the SQLite file location. |
COODRA_HOME | ~/.coodra | Override the whole data dir. Used in compose. |
COODRA_CONTEXT_PACKS_ROOT | <cwd>/docs/context-packs | Where pack markdown is materialised. |
COODRA_GRAPHIFY_ROOT | ~/.coodra/graphify | Where to look up graph.json. |
COODRA_SYNC_TICK_MS | 1000 | Outbox + puller cadence. |
COODRA_SYNC_LEASE_MS | 30000 | Worker lease duration. |
COODRA_WEB_URL | http://localhost:3001 | Used by CLI login for browser handoff. |
LOG_LEVEL | info | pino level. |
Some keys in .env.example are no longer consumed by live code: NL_ASSEMBLY_PORT, SEMANTIC_DIFF_PORT, ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, GITHUB_APP_ID, ATLASSIAN_*. They reference modules superseded by ADR-012 + ADR-013 (M05 / M06). REDIS_URL is provisioned in compose but has no live consumer either.
Self-host the team stack.
docker compose
Team mode runs on your own Postgres. deploy/compose.yaml ships a 5-service stack — postgres, a one-shot migrator, the MCP server, the hooks bridge, and the sync-daemon — that any operator can lift end-to-end.
| Service | Image | Depends on | Port |
|---|---|---|---|
postgres | pgvector/pgvector:pg16 | — | ${POSTGRES_PORT:-5432}:5432 |
cloud-migrate | deploy/Dockerfile.cloud-migrate | postgres healthy | one-shot |
mcp-server | deploy/Dockerfile.mcp-server | cloud-migrate done | ${MCP_SERVER_PORT:-3100}:3100 |
hooks-bridge | deploy/Dockerfile.hooks-bridge | cloud-migrate done | ${HOOKS_BRIDGE_PORT:-3101}:3101 |
sync-daemon | deploy/Dockerfile.sync-daemon | cloud-migrate done | — |
# 1. Clone & build images locally git clone https://github.com/your-org/coodra cd coodra cp deploy/.env.example deploy/.env # Fill in: DATABASE_URL, CLERK_*, COODRA_EXPECTED_ORG_ID, LOCAL_HOOK_SECRET # 2. Bring it up docker compose -f deploy/compose.yaml up -d # 3. Point laptops at it # Each developer runs: coodra team install <invite-url> coodra start
/10.1Auth chain & RBAC
The hooks-bridge and MCP server share the same auth chain. First match wins.
CLERK_SECRET_KEY === 'sk_test_replace_me' OR COODRA_MODE === 'solo'. Attaches SOLO_IDENTITY.LOCAL_HOOK_SECRET. Attaches { source: 'local-hook' }. Used by the native plugin subprocess.Authorization: Bearer <token> → verifyClerkJwt. Required in team mode.401 { ok: false, error: 'unauthorized' }. No further processing.RBAC has three roles — viewer, member, admin — parsed from Clerk's org:admin/org:viewer (anything else → member). The shared assertCanEdit helper enforces admin-OR-(member + ownership); viewer never edits. Knowledge mutations have their own gate, assertCanAuthorKnowledge.
When things break.
coodra doctor
Every problem in Coodra leaves a trail in ~/.coodra/logs/, in policy_decisions, or in the doctor output. Start there.
Agent never sees the Feature Pack
SessionStart isn't reaching the bridge. Confirm the native Coodra plugin is installed, then:
coodra status shows hooks-bridge listening on 3101?curl 127.0.0.1:3101/healthz returns ok: true?tail ~/.coodra/logs/hooks-bridge.log shows the inbound POST?
If yes to all three but the agent still doesn't see it — check X-Local-Hook-Secret against LOCAL_HOOK_SECRET.
Policy denies something it shouldn't
Look up the audit row by idempotency_key = pd:{sessionId}:{toolUseId}:{toolName}:{eventType}. The matched_rule_id tells you exactly which rule fired.
If reason: 'policy_check_unavailable' — the engine fail-opened, never a deny. Check the breaker state in logs.
Sync-daemon can't connect
Symptoms: queue depth climbs in doctor (check 26), nothing appears on teammates' laptops.
Confirm DATABASE_URL reachable from this machine. coodra doctor --full runs cloud-reachability check 24. Look at ~/.coodra/logs/sync-daemon.log for connection errors. Restart with coodra stop --service sync-daemon && coodra start.
web-v2 refuses to boot
Almost always COODRA_EXPECTED_ORG_ID missing in team mode. The middleware refuses to start without it — by design, to prevent any Clerk-authed account in this deployment's Clerk app from reading your team's data.
If set but you land on /forbidden?reason=org_mismatch — your Clerk session is in a different org. Sign out and back in, or run coodra org switch <slug>.
SQLite locked or corrupted
Stop all daemons, snapshot the file, repair:
coodra stop && coodra db backupsqlite3 ~/.coodra/data.db "PRAGMA integrity_check;"coodra db restore <snapshot> if needed.
Open an issue with the doctor JSON + log tail.
No run_diff for completed run
query_run_diff returns analysis_pending, no_base_sha, or git_diff_failed. Common cause: the agent operated outside a git repo, or base_sha was unreachable when SessionEnd fired.
Re-trigger with coodra run show <id> --regen-diff after checking out the right commit.
Build & test.
Biome
A monorepo with Turbo for orchestration, Biome for lint, and Vitest for everything from unit to e2e. CI runs four jobs in dependency order.
/12.1Install & build
nvm use # 22.16.0 per .nvmrc corepack enable # pnpm 10.33.0 pnpm install pnpm rebuild # native modules: better-sqlite3 + sqlite-vec # Day-to-day: pnpm build # turbo run build pnpm typecheck # turbo run typecheck pnpm lint # biome check . pnpm lint:fix pnpm --filter @coodra/cli build # tsc + esbuild bundle
/12.2Test tiers
| Command | Runner | Needs |
|---|---|---|
pnpm test:unit | Vitest | nothing — runs entirely in-process |
pnpm test:integration | Vitest | Postgres + Redis service containers |
pnpm test:e2e | Vitest · testcontainers | Docker · fileParallelism: false · 120s hook timeout |
pnpm test:functional | bash | __tests__/functional/run-all.sh · 11 shell scripts |
/12.3CI jobs
pnpm test:integration against postgres + redis containers.scripts/hook-adapters/__tests__/smoke.sh on both ubuntu and macos.Contribution guide.
PRs welcome
Coodra is built linearly, start to finish, as a production-grade system. Contributions are welcome — and held to the same bar.
/13.1Core principles (read first)
- Ship complete or don't merge. No placeholder surfaces, no mocked endpoints to make the UI render, no "we'll come back to this later." Every module ships end-to-end.
- Local-first remains the default. Any new feature must work offline. If it requires the cloud, it must degrade gracefully back to local + offer a clear path to enable.
- Fail-open in the hot path. Hooks return ALLOW on engine fault. Hot-path code paths catch and log async — never throw at the agent.
- Append-only audit. Don't add an UPDATE on
policy_decisions,decisions, orrun_events. Resurface via a new row with the same idempotency key. - Document divergence. If your PR contradicts an ADR, README claim, or essentials doc — update the source-of-truth doc in the same commit.
/13.2Workflow
Pick or file the issue.
Look for good-first-issue or help-wanted. For non-trivial work, open an issue first so we can align on shape before code lands.
Branch off main.
Naming: feat/<short-slug>, fix/<short-slug>, chore/<short-slug>, docs/<short-slug>. Squash-merge expected at the end.
Write the tests.
Unit tests live next to source as *.test.ts. New tables/migrations require a schema-parity test. New MCP tools require a happy-path + soft-failure test at minimum.
Update docs.
If you touched the public surface — env vars, CLI flags, MCP tools, hook payloads — update the relevant table in this docs page in the same PR. Also update essentialsforclaude/ if the agent contract changes.
Pre-commit & push.
The .githooks/pre-commit hook runs the migration-lock check automatically when packages/db/* is staged. Before pushing: pnpm lint:fix && pnpm typecheck && pnpm test:unit.
Open the PR.
Title in conventional-commit form (feat:, fix:, chore:, docs:). Body should state: what changed, why, how to verify locally, and any ADR/docs it touches. CI will run verify → integration → hook-adapter-smoke → e2e.
/13.3Code conventions
Biome is the source of truth. Rules ratcheted up over time — these are the ones most likely to flag a PR.
const. Reach for let only when re-assigning.import type { Foo } from '...'any is a code smell. Prefer unknown + narrowing, or a real type.x!. Prefer explicit guard + early return.apps/mcp-server/src/lib/**. If your import cycles, refactor the shared bits up a layer.logger (pino-backed). console.log only for CLI user output./13.4Where things live
Coodra/ ├── apps/ │ ├── mcp-server/ # 16 MCP tools · stdio + HTTP transport │ ├── hooks-bridge/ # Hono HTTP · 5 Claude Code hook events │ ├── sync-daemon/ # push + pull workers · team mode only │ ├── web/ # DEPRECATED · do not extend │ └── web-v2/ # Next.js 15 dashboard · 46 routes ├── packages/ │ ├── cli/ # @coodra/cli npm package · only published artefact │ ├── db/ # Drizzle dual-dialect schema + migrations │ ├── policy/ # Cockatiel-wrapped evaluator + types │ └── shared/ # auth, roles, hook adapters, logger, env ├── docs/ │ ├── feature-packs/ # 16 module blueprints │ ├── context-packs/ # template + module/phase closeouts │ ├── DEVELOPMENT.md │ └── deploy/self-host.md ├── essentialsforclaude/ # 12 numbered docs auto-imported by CLAUDE.md ├── deploy/ # 4 Dockerfiles + compose.yaml ├── scripts/ # hook-adapter installers + maintenance ├── __tests__/ # e2e + functional + manual harnesses └── .github/workflows/ # ci.yml (4 jobs)
Hot tip: when in doubt about whether a behaviour is correct, search docs/audit/ and docs/verification/. Those reports cite exact file:line ranges for every claim made elsewhere.
Decisions of record.
11-adrs.md
Architectural decisions, locked once accepted, superseded only by another ADR. The ones below are the most load-bearing — if you're new to the codebase, these explain why things look the way they do.
Append-only audit
Policy decisions, decisions records, and kill-switch state changes are append-only. Idempotency keys gate writes. Resumes don't delete — they stamp resumed_at on the existing row. Recovery and forensics become trivial.
Graphify is a reader
Coodra reads ~/.coodra/graphify/<slug>/graph.json produced by an external graphify scan. No in-repo producer ships — separation of concerns and faster iteration on the graph format.
Agent-driven NL assembly
Module 05 originally proposed a Python FastAPI service with sentence-transformers. Replaced with MCP tools (search_packs_nl, list_context_packs, read_context_pack) — the agent does the ranking. No external service. No model dependency.
Run Diff in process
Module 06 was renamed from "Semantic Diff" to "Run Diff". The Python tree-sitter service is gone. Replaced by an in-process git-diff runner in the hooks-bridge that captures base_sha → head_sha at session end.
Tier 2.5 RBAC
Three roles — viewer, member, admin — parsed from Clerk org roles. assertCanEdit centralises the gate; ownership unlocks member-level edits on rows they created. Knowledge mutations are their own narrower gate.
Bridge stays local
Hooks-bridge always runs on the laptop. No cloud bridge ships, even in team mode. The sync-daemon is the only process that opens the cloud connection — preserves the "fail-open & loopback" invariant.
External dependencies.
libraries
Only the libraries that shape behaviour. If something in the runtime surprises you, the explanation is usually here.
| Package | Where | What it gives us |
|---|---|---|
@modelcontextprotocol/sdk | mcp-server | MCP protocol server with stdio + HTTP transports. |
hono · @hono/node-server | hooks-bridge · mcp-server HTTP | Tiny HTTP framework. Validates payloads via @hono/zod-validator. |
drizzle-orm | packages/db | Dual-dialect schemas + migrations. |
better-sqlite3 | everywhere local | Synchronous SQLite driver. Native module — rebuild after install. |
sqlite-vec | packages/db extension | Vector search for the context_packs_vec virtual table. |
postgres | web-v2 · packages/db | Postgres driver. |
next 15 · react 19 | web-v2 | Server framework + UI. |
@clerk/nextjs · @clerk/backend | web-v2 · cli | Identity, org membership, JWT. |
cockatiel | packages/policy | Circuit breaker + timeout fuse around DB reads. |
picomatch | policy · mcp-server | Path glob matching for policy rules. |
zod | everywhere | Schema validation at every untrusted boundary. |
commander · execa · ink | cli | CLI parser, subprocess management, React-for-terminal TUI. |
vitest · testcontainers | tests | Unit + integration + e2e runners. |
biome | repo | Lint + format. |
turbo | repo | Workspace orchestration. |
Not present, in case you're checking: no @anthropic-ai/sdk, no openai, no @google/generative-ai, no web-tree-sitter, no bullmq, no @octokit/*, no JIRA SDK. The MCP server is the only LLM-facing surface — and it talks to the agent, not the model.
Frequently asked.
linked deep dives
mcp-server.~/.coodra/data.db. In team mode: also in the Postgres you operate. Coodra ships no hosted backend.scripts/wipe-disposable-projects.sh or scripts/wipe-all.sh. Append-only doesn't mean undeletable; it means we don't update rows in place..env.example mention services that don't exist?coodra start --no-web skips it. coodra ui gives you a TUI alternative.Glossary.
definitions
userId, orgId, role, source. Solo bypass attaches SOLO_ACTOR.3101 that the agent's hooks POST to.--full.packages/db/src/schema/{sqlite,postgres}.ts.graph.json. Coodra reads it; doesn't produce it.coodra ui.~/Library/LaunchAgents/ during start.pending_jobs table) used for async writes and cloud sync.policy_rules.match_path_glob.runs.base_sha and runs.head_sha, captured at session end.__global__ for events before a real project exists; __solo__ for the solo-mode org.