A privacy-first, local-only AI development platform for Claude Code. Everything runs on your machine — local embeddings and reranking (llama.cpp + ONNX), a local LanceDB vector store, a local SQLite memory graph, and a tamper-evident local audit ledger. No code, embeddings, or telemetry ever leave the host. Primary target is macOS Apple Silicon (Metal-accelerated); Linux is supported (CPU inference + systemd timers).
This document is the complete guide — read it top to bottom the first time, then use the table of contents as a reference.
Part I — Overview
Part II — Architecture
Part III — Installation
Part IV — Onboarding a repository
- One-step onboarding
- What lands in the repo
- Index, verify, auto-reindex
- repo-policy.yaml reference
- Register MCP servers (optional)
Part V — Operations
- Everyday verification
- Memory maintenance
- Approvals
- Observability & budgets
- Governance: compliance, replay, incident, policy-sim
- Knowledge & quality tools
- Team knowledge sync
- Backup & recovery
- Upgrades
- Nightly automation
Part VI — Reference
- CLI command reference
- Troubleshooting
- Appendix A — JetBrains integration
- Appendix B — PostgreSQL migration
- Appendix C — Plugin distribution
- License & use
claude-env wraps Claude Code in a local governance layer. Every file read, command run, retrieval, and memory write is policy-checked, audited to a tamper-evident ledger, and scoped to the repository it belongs to — without any of it leaving the machine.
The design goals, in priority order:
- Privacy — local inference only; no network egress by default (the sole outbound capability is a tier-gated documentation fetch).
- Least privilege — agents and tools get explicit, narrow permissions; anything outside scope is denied or routed to a human approval gate.
- Auditability — an append-only, hash-chained ledger records every action and can be
proven intact (
verify_chain()). - Isolation — each repo gets its own policy, its own RAG index, and its own memory namespace.
Everything runs through a single Python virtual environment at $CLAUDE_ENV_HOME/venv/
(default ~/.claude-env/venv/). The system Python — used once to run bootstrap.py — is
never modified. The CLI, the MCP servers, the git hook, and the nightly jobs all invoke
$CLAUDE_ENV_HOME/venv/bin/python explicitly, so you never activate the venv by hand.
| Area | What you get |
|---|---|
| Guardrails | Layered policy engine (global deny → tier → repo deny → repo allow; deny wins; tier-3 is default-deny) + content secret-scanning. Source/docs/tests allowed by default; secrets/keys/prod-configs/PII blocked. |
| Audit | Append-only, hash-chained audit_events; DB triggers reject any mutation; verify_chain() proves integrity. |
| Local RAG | AST-aware chunking, pluggable local embeddings + optional ONNX cross-encoder rerank, LanceDB hybrid search, one table per repo+branch, incremental re-index on commit. No model is baked into the code — you pick it at setup. |
| Memory graph | Episodic / semantic / procedural / agent memory as nodes+edges in SQLite, with confidence decay, supersede-based corrections, consolidation, pruning, and namespace isolation. |
| Agents | 10 specialists + orchestrator with explicit tools, write scopes, and approval gates; deterministic routing, scoped handoffs, conflict resolver with security veto. |
| MCP layer | Six local stdio servers; the policy-enforcing filesystem server is the single sanctioned path to disk. |
| Hooks | PreToolUse policy + PostToolUse audit extend governance to Claude Code's native tools (Read/Write/Edit/Bash) — including parsing Bash command strings. |
| Onboarding | claude-env onboard <repo> — one interactive step provisions the repo's isolated policy + RAG/memory namespaces and installs a governance CLAUDE.md, engineering skills, and review subagents. |
| Governance ops | report (auditor evidence + chain proof), replay (session forensics), incident on/off (kill switch — everything fails closed), policy-sim (dry-run + drift). |
| Self-learning | Nightly session ingestion turns Claude Code transcripts into memory; a retrieval-feedback loop boosts chunks whose files later get edited. |
| Observability | Local latency / retrieval-quality / token-cost metrics with a terminal summary and an optional read-only Datasette UI; per-repo monthly cost budgets. |
| Dev tools | know (fused memory+RAG+git recall), context-pack, rag-bench, test-impact, doc-drift, nightly digest. |
| Packaging | claude-plugin/ ships hooks + MCP servers + slash commands as one installable Claude Code plugin. |
These hold everywhere; the rest of the system is built to preserve them.
- Persistence is abstracted. No module imports
sqlite3/psycopgdirectly — everything goes throughlib/db.py::get_db(). SQLite is the default; PostgreSQL is a config change (Appendix B). - The audit ledger is the spine.
audit_eventsis append-only and hash-chained; DB triggers reject UPDATE/DELETE. Every other audit-like table is a typed projection written in the same transaction. - Policy is enforced at one chokepoint. The
filesystem-policyMCP server is the only sanctioned path to the filesystem for agents; nothing downstream re-checks policy, so that server is strict and fail-closed. - Retrieved/external text is data, never instructions. RAG results and fetched docs
are wrapped in
<retrieved_context>/<external_doc>delimiters and screened by the poison/injection detectors. - No network egress by default. Models run locally (llama.cpp + ONNX). The only outbound capability is the documentation fetch tool, restricted to tier 0/1.
- Tiers tighten posture monotonically. 0 public → 1 internal → 2 sensitive → 3 highly-restricted. Higher tier = more deny rules, memory isolation, and (tier 3) default-deny on reads.
Everything is local. Claude Code reaches the machine through two enforcement surfaces — the MCP servers (stdio, no ports, per-project env) and the native-tool hooks (Read/Write/Edit/ Bash). Both consult one policy engine and write one append-only, hash-chained audit ledger (the security spine), over local knowledge stores and local inference. The only network egress is the documentation server for tier-0/1 repos.
The six MCP servers are filesystem-policy (the chokepoint), git (read-mostly), lancedb-rag,
memory-graph, terminal (allow-listed), and documentation (tier-gated fetch). They start in a
fixed order (startup_order in config/mcp-servers.json) and import platform code from
$CLAUDE_ENV_HOME, so a DB-backend swap is transparent to them.
File paths refer to this repository; bootstrap.py mirrors the code under
$CLAUDE_ENV_HOME/ at install time.
Components: bootstrap.py, lib/db.py, sql/001_schema.sql, sql/002_retention.sql,
sql/003_extensions.sql, audit/audit_logger.py, validation/validate_installation.py.
The venv at $CLAUDE_ENV_HOME/venv/ is the only Python environment used to run platform
code. bootstrap.py creates it (preferring uv, falling back to stdlib venv + pip) and
installs all runtime deps into it. Persistence is CLAUDE_ENV_DSN (default
sqlite:///$HOME/.claude-env/state/claude-env.db). No secrets are ever stored in config.
Security posture: append-only triggers on audit_events; WAL mode; foreign keys on; DB
under $CLAUDE_ENV_HOME/state (chmod 700 $CLAUDE_ENV_HOME recommended).
Components: audit/audit_logger.py (typed helpers: agent_action, tool_call,
retrieval, memory_read/write, security_event, policy_violation,
human_approval_request/resolve, verify_chain); security/detectors.py
(PromptInjectionDetector, SecretDetector, RagPoisonDetector).
The chain is tamper-evident: event_hash = sha256(prev_hash || canonical_json(payload)).
Canonical-JSON hashing is engine-independent, so PostgreSQL keeps the identical chain.
Detectors are pure functions over text and log every flag as a security_events row, so
detection itself is auditable.
Components: security/policy_engine.py (layered evaluation + content scan,
gitignore-style ** globbing), config/global-policy.yaml (never-overridable global deny
- tier matrix),
config/repo-policy.template.yaml(annotated per-repo schema).
Resolution order: global deny → tier overrides → repo deny → repo allow → fall-through
(tier-3 = default-deny). Deny always wins. Allowed by default: source, docs, ADRs,
RFCs, tests. Blocked by default: .env .pem .p12 .key .crt, .ssh, .gnupg, secrets,
credentials, production configs, customer data, exports, backups. The engine is stateless —
edit the YAML and it recompiles on load. Full schema is in §15.
Components: rag/config.py, rag/chunkers/chunkers.py, rag/embeddings/llama_embedder.py,
rag/rerankers/cross_encoder.py, rag/retrievers/lance_store.py, rag/indexers/indexer.py,
rag/pipelines/retrieve.py, plus bootstrap_rag.py, repository_scan.py,
incremental_index.py, branch_index.py, validate_rag.py.
Model selection lives entirely in config — the code never names a model. Resolution
order: env vars > config/rag.yaml > numeric fallbacks (ctx/dim/gpu only; an unconfigured
embedding.model_path raises a clear error rather than guessing). RAG code always goes
through rag.config.get_embedder() / get_reranker(). Setup is §10.
LanceDB layout: $CLAUDE_ENV_HOME/knowledge/lancedb/<slug>__<branch>.lance, one table
per repo+branch, rows carrying the vector + chunk metadata + tier, with an FTS index for
hybrid search. Security: the indexer runs every candidate path through the policy engine
(blocked paths logged), redacts/blocks secrets before embedding, indexes only committed
files when index_only_committed: true, and screens retrieved chunks with RagPoisonDetector.
Feedback loop (observability/feedback.py): every returned chunk is recorded
(signal='retrieved'); the nightly session ingestor upgrades retrieval→edit correlations to
signal='used', earning a bounded post-rerank boost (0.05·ln(1+min(uses,20))) — enough to
re-order near-ties, never to overrule a clear reranker decision. Disable with
CLAUDE_ENV_FEEDBACK_BOOST=false.
Components: memory/memory_manager.py, memory_retriever.py, memory_consolidator.py,
memory_pruner.py, memory_validator.py, session_ingestor.py, memory_sync.py.
A local property graph (nodes + edges in SQLite). Memory types → storage: episodic
(session/decision/investigation), semantic (entity/concept/architecture), procedural
(workflow/convention/pattern, namespace global), agent (namespace agent:<id>). Edges:
RELATES_TO, DEPENDS_ON, DECISION_ABOUT, DISCOVERED_IN, SUPERSEDES, CONSOLIDATES;
traversal is a recursive CTE (expand()). Confidence decays:
effective = stored · 2^(-age_days / half_life_days) (sessions 30d, decisions/architecture
365d, conventions 540d, …). Security: namespace isolation (isolated=True refuses
cross-namespace reads — enforced for tier ≥ 2); corrections via supersede (never
destructive overwrite); pruning archives to archive/memory/<ns>.jsonl first and never
prunes decision/architecture.
Self-population (nightly): Claude Code transcripts under ~/.claude/projects/ become one
episodic session node each (task, files touched, outcome — secret-redacted, deduped). This
also closes the RAG feedback loop.
Components: agents/agent_registry.yaml, agents/prompts/*.md (11 system prompts),
agents/orchestration/{task_router,agent_handoff,conflict_resolver,approval_gate}.py.
Each agent's allowed_tools, denied_tools, write_paths, memory/rag_access, and
requires_approval are declared in the registry. Examples: architect is read-only on source
(memory write); backend writes src/**,tests/**; database writes migrations/schema and
never touches a live DB; devops requires_approval: true; security is read-only; research
does web fetch only in tiers 0–1. Flow: decompose → task_router.route() picks a
specialist → approval_gate.evaluate() → agent_handoff.handoff() packages scoped context
as data → specialist acts via MCP → conflict_resolver.resolve() reconciles (security veto
wins) → orchestrator synthesizes. Global approval gates: writes outside scope, state-mutating
terminal, any tier-2/3 action, git push/amend/rebase, memory delete/prune.
Components: config/mcp-servers.json, mcp-servers/*/server.py.
| Server | Scope | Notes |
|---|---|---|
filesystem-policy |
read/write/list | The single FS chokepoint; policy + content-scan; rejects path traversal; fail-closed. |
git |
log/diff/blame/status/show | Read-only; denies push/amend/rebase/reset. |
lancedb-rag |
search | Read-only, data-wrapped, poison-screened. |
memory-graph |
recall/read/expand/write/link | Namespace isolation; no delete (approval-gated). |
terminal |
run_tests/benchmarks/audit | Allow-listed only; terminal.run returns an approval directive; scrubbed env + timeouts. |
documentation |
search/fetch | Local search always; external fetch only in tiers 0–1. |
jetbrains |
context/inspect/refactor | Optional, IDE-provided; respects the filesystem policy. |
Components: hooks/policy_hook.py (PreToolUse), hooks/audit_hook.py (PostToolUse),
hooks/install_hooks.py (idempotent ~/.claude/settings.json merge; claude-env hooks).
MCP servers only govern MCP traffic — Claude Code's own Read/Write/Edit/Glob/Grep/Bash would otherwise bypass them. The hooks make the same policy engine govern everything:
PreToolUse decision flow. incident marker → deny all → path policy block → deny
(+ policy_violations row) → secret pattern in Write/Edit content → ask → else allow
silently. Bash is parsed, not skipped (shlex → simple-command segments): a
destructive/state-mutating command is hard-denied regardless of path (rm, rmdir,
shred, dd, truncate, chmod/chown, kill, and git push/reset/rebase/clean/
commit --amend) — the model must edit via the Write/Edit tools or route the command through
terminal.run (approval); a file argument on a policy-denied path is denied; a
network-egress command with a file argument is denied as likely exfiltration; plain network
egress is denied on tier ≥ 2 / asked on tier ≤ 1; an inline secret prompts an ask.
URLs and non-existent bare tokens aren't treated as local paths (so grep pattern and git log
aren't misread as files). Note the limits: arbitrary interpreters (python x.py, make) can't
be statically judged, and the hook fails open by default — so this is a strong stop for the
common destructive-shell class, not a full sandbox; keep CLAUDE_ENV_HOOK_FAIL_CLOSED=true for tier 2+.
Failure posture. Internal errors fail open by default so a broken hook can't brick
the editor; CLAUDE_ENV_HOOK_FAIL_CLOSED=true flips that for tier-2+ machines. PostToolUse
writes native.<Tool> rows for mutating calls (CLAUDE_ENV_HOOK_AUDIT_ALL=true audits all).
- Incident mode (
security/incident.py) — a JSON marker atstate/INCIDENTmakes every policy evaluation fail closed at once (MCP, indexer, hooks).onalso denies pending approvals, snapshots the SQLite DB, and writes a criticalsecurity_event. - Compliance reports (
audit/compliance_report.py) — windowed ledger evidence (events by type/actor, top tools, violations, approval trails, session costs) + a freshverify_chain()proof. Exit 1 when the chain is broken. Formats: md / csv / json. - Session replay (
audit/session_replay.py) — orders a session's ledger rows into a timeline; forensics without touching the DB. - Policy simulation (
security/policy_sim.py) —simulatereports which files a candidate policy would newly block/allow;diffis structural drift against a baseline.
Components: observability/collectors.py, feedback.py, budgets.py, dashboard.py;
metrics_sessions (tokens, est_cost_usd), metrics_latency, metrics_retrieval_quality.
All local; the cost model is a configurable price table (PRICES in collectors.py);
Datasette runs read-only on localhost; metrics_* are prunable and never contain audit data.
All offline, stateless over the working tree + git.
rag/context_pack.py— generatesCLAUDE.generated.mdfrom repo signals + team memory; refuses to overwrite a hand-writtenCLAUDE.md.rag/test_impact.py— changed files → minimal test set (name + import heuristics).rag/doc_drift.py— markdown→code references: broken paths, docs older than the code.agents/analysts/nightly_analyst.py— nightly per-repo digest (doc drift + TODO aging + possibly-dead symbols + hotspots) →logs/digests/and a memory node.agents/orchestration/approvals_ui.py— localhost one-click audited approve/deny.
| Threat | Control |
|---|---|
| Prompt injection | PromptInjectionDetector + data-delimited context + "instructions only from operator" rule |
| RAG poisoning | RagPoisonDetector.scan_chunk at index and serve time |
| Secret exposure | policy ext/path/regex deny + content scan (read and write) + SecretDetector + Bash-command scan |
| Memory corruption | memory_validator (dangling edges, bad confidence, supersede cycles, ns leakage) --repair |
| Policy bypass | single FS chokepoint + path-traversal rejection + fail-closed — extended to native tools by the PreToolUse hook |
| MCP over-permission | explicit scopes + denied tools + allow-listed terminal |
| Active compromise / runaway agent | incident mode: one marker fails every evaluation closed, denies approvals, snapshots the DB |
| Repo isolation | per-repo policy + per-repo LanceDB table + per-repo memory namespace |
| Agent isolation | registry permissions + approval gates + scoped handoffs |
$CLAUDE_ENV_HOME (default ~/.claude-env/) after bootstrap:
state/ claude-env.db (audit + memory + metrics + RAG bookkeeping) · INCIDENT marker
venv/ the ONLY Python environment used to run platform code
knowledge/ lancedb/<slug>__<branch>.lance docs/ (local doc corpus)
models/ downloaded GGUF / ONNX model files (real files, never symlinks)
archive/ memory/ audit/ security/ + DB snapshots
logs/ incremental_index.log · memory.log · digests/
config/ global-policy.yaml · repo-policy.template.yaml · rag.yaml · mcp-servers.json · budgets.yaml
bin/ claude-env CLI · <mirrored platform code: security/ audit/ rag/ memory/ agents/ mcp-servers/ hooks/ templates/ …>
Source-tree layout of this repository:
claude-env/
├── bootstrap.py # one-command setup (uv-preferred venv + deps)
├── requirements.txt # locked runtime deps
├── bin/claude-env # CLI dispatcher (auto-resolves the venv Python)
├── lib/db.py # persistence abstraction (SQLite default, PG-ready)
├── sql/ # 001_schema · 002_retention · 003_extensions
├── audit/ # audit_logger · compliance_report · session_replay
├── hooks/ # policy_hook (PreToolUse) · audit_hook (PostToolUse) · install_hooks
├── security/ # policy_engine · policy_sim · incident · detectors
├── config/ # global-policy · repo-policy.template · rag.yaml · mcp-servers.json · budgets
├── rag/ # config · chunkers · embeddings · rerankers · retrievers · indexers · pipelines · tools
├── memory/ # manager · retriever · consolidator · pruner · validator · session_ingestor · sync
├── agents/ # agent_registry.yaml · prompts/*.md · orchestration/ · analysts/
├── mcp-servers/ # filesystem-policy · git · lancedb-rag · memory-graph · terminal · documentation
├── observability/ # collectors · feedback · budgets · dashboard
├── validation/ # validate_installation · _security · _memory · _agents · _mcp · _features
├── scripts/ # post-commit · nightly_memory.sh · systemd/ · launchd.README.md
├── templates/repo-onboarding # CLAUDE.md + .claude/{skills,agents} installed into onboarded repos
├── claude-plugin/ # Claude Code plugin packaging (hooks + MCP + slash commands)
└── tests/ # test_policy_engine · test_policy_hook_bash
| Check | Command | Expected |
|---|---|---|
| macOS Apple Silicon | uname -m |
arm64 (Linux also supported) |
| FileVault | — | enabled (encrypts the local store, weights, DB) |
| Xcode CLT | xcode-select -p |
a path |
| Homebrew | brew --version |
any version |
| Python ≥ 3.13 | python3 --version |
3.13.x+ (brew install python@3.13 if older) |
| Git ≥ 2.40 | git --version |
2.40+ |
| Claude Code | claude --version |
any version |
Run once on a new machine, from the repo checkout:
python3 bootstrap.py --with-brew # create venv, install deps, init DB + policies, genesis event
chmod 700 ~/.claude-env # lock down the store
echo 'export CLAUDE_ENV_HOME="$HOME/.claude-env"' >> ~/.zshrc
echo 'export PATH="$CLAUDE_ENV_HOME/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
~/.claude-env/venv/bin/python --version # must print 3.13+
claude-env validate installation # green except model warnings until §10What bootstrap does: checks the host → creates ~/.claude-env/ layout → creates the venv
(via uv when available, else stdlib venv) → installs all runtime deps (llama-cpp-python is
built with Metal on Apple Silicon) → applies sql/00*.sql → copies policy + rag config →
mirrors platform code under $CLAUDE_ENV_HOME → writes and verifies the genesis audit event.
Useful flags: --no-deps (skip pip; venv must exist) · --no-venv-create (reuse venv,
update deps) · --recreate-venv (nuke + rebuild, e.g. after a Python upgrade) · --dsn postgresql://… (PostgreSQL target).
Required before indexing. The code ships with no model — you download one and point
config/rag.yaml at it. The models named below are suggestions; any GGUF embedding model and
any ONNX cross-encoder work. No symlinks — point the config straight at the file.
mkdir -p ~/.claude-env/models
~/.claude-env/venv/bin/pip install huggingface-hub # provides the `hf` CLI
hf download nomic-ai/nomic-embed-text-v1.5-GGUF \
nomic-embed-text-v1.5.Q8_0.gguf --local-dir ~/.claude-env/modelsPoint the deployed config at it — ~/.claude-env/config/rag.yaml is the copy the servers
actually read:
# ~/.claude-env/config/rag.yaml
embedding:
model_path: "~/.claude-env/models/nomic-embed-text-v1.5.Q8_0.gguf"
embedding_dim: 768 # MUST match your model (nomic = 768)
document_prefix: "search_document: " # nomic needs task prefixes; leave empty if yours doesn't
query_prefix: "search_query: "claude-env validate installation # "embedding model file present" must PASSSuggested settings (not requirements): 768-dim GGUF at Q8_0 (Q4/Q5 degrade recall); context 2048; chunk sizing 512 tokens (code) / 384 (markdown) / 256 (sections) with ~12% overlap.
# download an ONNX cross-encoder (the arm64-quantized variant is fastest on Apple Silicon)
hf download cross-encoder/ms-marco-MiniLM-L6-v2 \
--include "onnx/model.onnx" "onnx/model_qint8_arm64.onnx" \
"tokenizer.json" "config.json" "vocab.txt" \
"tokenizer_config.json" "special_tokens_map.json" \
--local-dir ~/.claude-env/models/reranker-onnx/
# the loader expects model.onnx in the directory root — move the quantized file into place
mv ~/.claude-env/models/reranker-onnx/onnx/model_qint8_arm64.onnx \
~/.claude-env/models/reranker-onnx/model.onnxPoint the deployed config at the directory, then verify:
# ~/.claude-env/config/rag.yaml
reranker:
model_dir: "~/.claude-env/models/reranker-onnx" # or "" to disableclaude-env validate installation # "reranker loads + runs" PASS (WARN = disabled/absent)The reranker degrades gracefully to fusion order if absent or disabled — RAG still works.
Resolution order: env var > config/rag.yaml > numeric fallback. Relevant env vars:
| Variable | Meaning | Default |
|---|---|---|
EMBED_MODEL_PATH |
path to the .gguf |
required (unset ⇒ clear error) |
EMBED_MODEL_NAME |
provenance label in rag_index_state |
filename |
EMBED_DOC_PREFIX / EMBED_QUERY_PREFIX |
per-task input prefixes | "" |
EMBED_CTX / EMBED_GPU_LAYERS / EMBED_DIM |
context / GPU offload / vector dim | 2048 / -1 / 768 |
RERANKER_DIR |
ONNX cross-encoder dir ("" disables) |
"" |
LANCEDB_PATH |
vector store location | ~/.claude-env/knowledge/lancedb |
Verify with claude-env validate installation — the embedding model file present and
reranker loads + runs lines must PASS (reranker WARN = disabled/absent, RAG still works). In
Python: from rag.config import get_reranker; print(get_reranker().status()) — ok: True
means reranking is live.
Vectors from different models/dimensions are not comparable. After swapping the embedding model, drop the affected LanceDB tables and re-index (§28, "Removing a repo's RAG index"). Gate swaps with
claude-env rag-bench(recall@k + MRR).
Editing config: the servers and tools read the deployed copy at
~/.claude-env/config/rag.yaml— edit that directly and it takes effect immediately. If you instead edit the repo'sconfig/rag.yaml, re-runpython3 bootstrap.py --no-depsto sync the deployed copy.
Extend the policy engine + audit to Claude Code's native tools (Read/Write/Edit/Glob/Grep/Bash):
claude-env hooks # install into ~/.claude/settings.json
claude-env hooks --dry-run # preview
# To uninstall
claude-env hooks --uninstall # uninstall hooks from ~/.claude/settings.jsonRestart Claude Code, then confirm: ask Claude to read a blocked file (e.g. .env) — the call
must be denied with the matched rule. On tier-2+ machines set CLAUDE_ENV_HOOK_FAIL_CLOSED=true
in the environment Claude Code runs under. Behavior detail is in §5.8.
Onboard each repository with a single interactive command:
claude-env onboard /absolute/path/to/repo # interactive (recommended)
claude-env onboard /absolute/path/to/repo --yes # non-interactive; accept detected defaultsInteractively it asks a few questions with detected defaults (Enter accepts each):
Repo slug (used for RAG + memory namespaces) [my-repo]:
Privacy tier — 0 public / 1 internal / 2 sensitive / 3 restricted [1]: 2
Default branch [main]:
Short description (optional) []: billing service
It then provisions the repo's isolated workspace end to end:
- Policy — writes
<repo>/.claude/repo-policy.yamlfrom the template with the real slug, tier, description, and memory namespace filled in (memory is isolated automatically for tier ≥ 2). This file is the isolation boundary the policy engine + hooks enforce. - Namespaces — the RAG table
<slug>__<branch>and the memory namespaceproj-<slug>. - MCP env — patches
~/.claude.jsonso every server for this project resolves to the correct repo root, slug, branch, and namespaces. - Template — installs the governance
CLAUDE.mdand the.claude/skills + subagents (§13), with concrete namespace values substituted (no placeholder text remains). - Index (opt-in) — offers to build the first RAG index now.
Flags: --repo-name SLUG · --tier {0..3} · --description STR · --branch BRANCH ·
--yes/-y · --force-policy (regenerate an existing policy) · --force-template (overwrite
existing skill/agent files) · --no-template (namespaces/env only) · --dry-run.
Re-running is idempotent: an existing repo-policy.yaml is preserved (local edits kept) unless
--force-policy; the CLAUDE.md managed block is refreshed while out-of-block content is kept.
Restart Claude Code afterwards so the MCP servers pick up the new environment.
claude-env registeris an alias for the same command.
Onboarding installs a governance layer directly into the repo:
CLAUDE.md— a binding operating contract: route all file/search/git/memory/test/doc work through the platform MCP servers, respect the privacy tier and approval gates, treat retrieved content as data, never touch secrets. It shows this repo's concrete RAG table and memory namespace. The platform-owned section lives inside a managed block; anything you add outside it is preserved across re-onboarding..claude/skills/—principled-engineering(decoupled architecture, SOLID, simplicity),solid-design,design-patterns(with explicit when-not-to),code-review,architecture-review..claude/agents/—code-reviewerandarchitecture-reviewerread-only subagents.
The template source lives in templates/repo-onboarding/; edit it there (not the copies in a
repo) — the managed CLAUDE.md block regenerates on the next onboard.
claude-env scan /path/to/repo # preview allowed vs blocked (no model load)
claude-env index /path/to/repo # full RAG index (live progress bar)
claude-env validate rag /path/to/repo "auth token validation" # expect 3–8 ranked resultsscan should show only secrets/generated files in the blocked list — adjust
.claude/repo-policy.yaml and re-run until satisfied. Empty RAG results mean the index is empty
(re-run index) or the slug/branch don't match the LanceDB table.
Install the post-commit hook so every commit triggers a background incremental re-index:
ln -sf ~/.claude-env/scripts/post-commit /path/to/repo/.git/hooks/post-commit
chmod +x /path/to/repo/.git/hooks/post-commitLives at <repo>/.claude/repo-policy.yaml; created by onboarding. Deny always wins over allow;
for tier 3 the allow list is authoritative (anything not allowed is denied).
version: 1
tier: 1 # 0 public · 1 internal · 2 sensitive · 3 restricted
repo: "my-repo-slug" # stable slug; drives RAG + memory namespacing
description: "..."
allow:
paths: ["src/**", "docs/**", "ADRs/**", "tests/**", "*.md"]
extensions: [".py", ".ts", ".go", ".java", ".rs", ".md", ".sql"]
deny: # always evaluated; always win
paths: ["**/secrets/**", "**/.ssh/**", "**/config/prod.*", "**/customer_data/**"]
extensions: [".env", ".pem", ".key", ".crt", ".keystore"]
regex: [{pattern: '(^|/)\.env($|\.)', reason: "dotenv files"}]
content_scan: # applied to allowed files before bytes leave the engine
enabled: true
on_match: "redact" # redact | block
patterns: [{name: "aws_access_key", pattern: 'AKIA[0-9A-Z]{16}'}]
rag:
enabled: true # tier 3 forces false unless opted in
index_paths: ["src/**", "docs/**", "README.md"]
exclude_paths: ["**/node_modules/**", "**/dist/**", "src/generated/**"]
index_only_committed: true # never index the working tree / untracked files
memory:
namespace: "proj-my-repo-slug"
isolated: false # tier ≥ 2 forces true (no cross-project reads)
share_with_agents: [architect, backend, testing, documentation]
agent_permissions: # per-agent override of registry defaults for THIS repo
devops: { requires_approval: true }Preview a candidate policy before applying it: claude-env policy-sim simulate /repo --candidate new-policy.yaml.
You normally don't need this. claude-env onboard <repo> (§12)
already writes each project's full MCP server configuration — command, args, and the per-repo
env — straight into ~/.claude.json, so registration happens per repository as part of onboarding.
Do this only to make the servers available in every project (including repos you haven't
onboarded yet), by registering them once at user scope. It matters that claude mcp add
defaults to local scope — the current project only — which is why running it inside a directory
registers the server to just that project. --scope user stores the definitions in ~/.claude.json
for all projects, and the working directory is irrelevant, so run it from anywhere:
# --scope user = machine-wide; run from any directory. Use the venv Python explicitly —
# Claude Code launches servers outside any shell, so bare `python` would not resolve to the venv.
H=~/.claude-env; PY="$H/venv/bin/python"
claude mcp add filesystem-policy --scope user -- "$PY" "$H/mcp-servers/filesystem-policy/server.py"
claude mcp add git --scope user -- "$PY" "$H/mcp-servers/git/server.py"
claude mcp add lancedb-rag --scope user -- "$PY" "$H/mcp-servers/lancedb-rag/server.py"
claude mcp add memory-graph --scope user -- "$PY" "$H/mcp-servers/memory-graph/server.py"
claude mcp add terminal --scope user -- "$PY" "$H/mcp-servers/terminal/server.py"
claude mcp add documentation --scope user -- "$PY" "$H/mcp-servers/documentation/server.py"
claude mcp list # all six appearThese user-scope entries start with an empty env, so they don't yet know which repo they serve —
claude-env onboard <repo> still supplies each project's CLAUDE_ENV_REPO_ROOT, slug, branch, and
namespaces. Scope precedence is local (project) > user, and Claude Code uses one whole entry
rather than merging them, so an onboarded repo always uses its own project-scoped entry (correct
env); the user-scope entry is only the fallback for not-yet-onboarded projects.
After restarting Claude Code, confirm each server by asking Claude to use it:
| Server | Ask | Expected |
|---|---|---|
git |
"what files are modified?" | lists modified files |
filesystem-policy |
"list files in src/" | file listing |
lancedb-rag |
"search the codebase for X" | ranked chunks |
memory-graph |
"what do you remember?" | memory nodes (may be empty initially) |
terminal |
"run the tests" | runs configured tests or a gated directive |
documentation |
"search docs for X" | local doc matches |
fatal: not a git repository or empty RAG results almost always mean missing/wrong env in
~/.claude.json — re-run claude-env onboard and restart.
PY=~/.claude-env/venv/bin/python
$PY memory/memory_validator.py --all --repair # integrity + safe repair
$PY memory/memory_consolidator.py --all # merge low-value clusters
$PY memory/memory_pruner.py --all # dry-run report
$PY memory/memory_pruner.py --namespace proj-x --apply # actually prune (archives first)Pruning archives to ~/.claude-env/archive/memory/<ns>.jsonl and never prunes decision /
architecture nodes.
Governance has two enforcement layers, and it's worth knowing which one you're hitting:
terminal.runopens a human approval and blocks on it. Asking Claude to run a free-form / state-mutating command opens a pending approval inhuman_approvals, auto-opens the approvals web UI in your browser, and then waits — the tool call blocks (Claude waits, up toCLAUDE_ENV_APPROVAL_WAIT_S, default 120s) until you decide. You review it (project, session, agent, tier, exact command) and Approve/Deny in the UI; the decision records the OS user@host automatically (no name asked). On approve the command then runs (same sandbox: argv-only, no shell/pipes, scrubbed env, repo-root cwd, timeout) and Claude gets the output; on deny/timeout it doesn't run. Auto-UI is toggled withCLAUDE_ENV_APPROVAL_AUTO_UI(default on).- Hard-deny (no gate):
git push/amend/rebase/reset --hardat the git server, and the native-tool hooks (§11) — these simply refuse (deny /ask-to- confirm secret writes) rather than opening an approval. - The orchestration approval gate (
agents/orchestration/approval_gate.py) also opens approvals when you drive work through the agent orchestrator —claude-env route "<task>"— or on any tier-2+ action.
Open the queue with:
claude-env approvals-ui # pretty web UI (Approve/Deny)
claude-env approvals --list-open # terminal list
claude-env services # which local UI is on which port (auto-assigned if 8002 was busy)Ports auto-fall-back. The approvals UI (preferred 8002) and the dashboard (preferred 8001) bind their preferred port if free, else a random free one — so they never collide with your app.
claude-env servicesshows the actual URL of each running UI (--port Npins one,--port 0forces random).
A gate opens when ANY of these is true (evaluated per agent action by the orchestrator):
| Trigger | Example |
|---|---|
the agent's registry entry has requires_approval: true |
the devops agent (any action) |
| the repo is tier 2 or 3 | any action in a sensitive repo |
a write targets a path outside the agent's write_paths |
backend writing infra/main.tf |
| a state-mutating terminal command | terminal.run … (tests/benches/audits run unattended) |
| git history rewrite / push | git push, commit --amend, rebase, reset --hard |
| destructive memory op | memory.delete, memory.prune --apply |
the action matches the agent's denied_tools |
hard stop, surfaced as a gate |
Resolve pending gates:
claude-env approvals --list-open
claude-env approvals-ui --port 8002 --by you # one-click localhost UI (decisions audited)
~/.claude-env/venv/bin/python agents/orchestration/approval_gate.py --resolve <id> --approve --by youOpen approvals also appear in the dashboard and the v_open_approvals view; on macOS a
notification fires when a gate opens.
claude-env dashboard --window 7d
claude-env dashboard serve --port 8001 # Datasette (read-only, localhost)
claude-env budget # per-repo monthly USD; exit 1 if exceeded
claude-env budget --format jsonSet limits in ~/.claude-env/config/budgets.yaml. Update PRICES in
observability/collectors.py to current per-1M-token pricing for accurate cost tracking.
claude-env report --window 30d # markdown evidence + chain proof
claude-env report --window 7d --repo payments --format csv --out evidence.csv
claude-env replay --list # recent sessions
claude-env replay <session_id> # step-by-step timeline
claude-env incident on --reason "suspected token leak" --by you # kill switch (fails everything closed)
claude-env incident status
claude-env incident off --by you
claude-env policy-sim simulate /repo --candidate new-policy.yaml # which files change block/allow
claude-env policy-sim diff /repo/.claude/repo-policy.yaml ~/.claude-env/config/repo-policy.template.yamlA report's integrity line is the first thing to check — a non-VERIFIED chain exits 1.
claude-env know "jwt validation" --repo payments --repo-root /work/payments # fused memory+RAG+git
claude-env context-pack /work/payments --write # CLAUDE.generated.md
claude-env rag-bench /work/payments --create-template # then edit + run
claude-env test-impact /work/payments --since HEAD~1
claude-env doc-drift /work/payments
claude-env digest /work/payments # analyst digest nowFor nightly per-repo digests, list repo paths (one per line) in
~/.claude-env/config/analyst-repos.txt.
claude-env memory-sync export --namespace proj-payments --out team.jsonl # secret-redacted
# review the JSONL, share it, then on the teammate's machine:
claude-env memory-sync import --in team.jsonl # additive; --namespace remapsImports are additive (existing node ids skipped). Review before sharing regardless.
# Backup (schedule daily)
H=~/.claude-env
sqlite3 "$H/state/claude-env.db" ".backup '$H/archive/db-$(date +%F).sqlite'" # online, WAL-safe
rsync -a "$H/knowledge/lancedb/" "$H/archive/lancedb-$(date +%F)/"
# The venv does NOT need backup — rebuild with: python3 bootstrap.py --no-venv-create
# Recovery
cp $H/archive/db-YYYY-MM-DD.sqlite $H/state/claude-env.db
rsync -a --delete $H/archive/lancedb-YYYY-MM-DD/ $H/knowledge/lancedb/
$H/venv/bin/python validation/validate_installation.py # must exit 0Recovery is complete only when validate_installation.py exits 0 and verify_chain() returns
True.
# Schema: add sql/00N_*.sql (bump schema_version), then:
~/.claude-env/venv/bin/python -c "import sys; sys.path.insert(0,'.'); from lib.db import get_db; get_db().apply_schema('sql/00N_whatever.sql')"
# Code + deps:
git pull
python3 bootstrap.py --no-venv-create # update deps in the existing venv
python3 bootstrap.py --recreate-venv # rebuild venv from scratch (after a Python upgrade)Always snapshot the DB (§24) before upgrading; restore on failure.
The nightly job ingests session transcripts, runs memory maintenance, and writes per-repo digests.
- macOS (launchd): follow
scripts/launchd.README.md. - Linux (systemd):
mkdir -p ~/.config/systemd/user cp ~/.claude-env/scripts/systemd/claude-env-nightly.{service,timer} ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now claude-env-nightly.timer
Run any piece manually: claude-env ingest-sessions (parse new transcripts) ·
claude-env digest <repo>.
claude-env <command> dispatches through the venv Python automatically. Run claude-env
(or claude-env help) for a grouped overview, claude-env help <command> for a one-line
summary, and claude-env <command> --help for a command's full options.
| Command | Purpose |
|---|---|
bootstrap |
run bootstrap.py |
onboard / register |
interactive repo onboarding (policy + namespaces + env + template) |
scan |
preview policy allow/block split for a repo |
index / reindex |
full / incremental RAG index |
validate [all|installation|security|rag|memory|agents|mcp|features] |
run validators |
hooks |
install/preview/uninstall native-tool hooks |
know |
fused memory + RAG + git recall with provenance |
context-pack |
generate CLAUDE.generated.md from repo signals + memory |
rag-bench / test-impact / doc-drift / digest |
quality tools |
route |
route a task to a specialist agent |
approvals / approvals-ui |
list / resolve approval gates |
services |
list the live local UI servers and their (possibly auto-assigned) ports |
report / replay |
compliance evidence / session forensics |
incident |
kill switch (on/off/status) |
policy-sim |
candidate-policy dry run / drift diff |
dashboard |
metrics summary / Datasette UI |
budget |
per-repo monthly cost budgets |
ingest-sessions / memory-sync |
transcript ingest / namespace export-import |
| Symptom | Likely cause | Action |
|---|---|---|
validate_installation fails on venv |
bootstrap not run | python3 bootstrap.py |
| MCP server won't start | venv Python not in command | ensure claude mcp add used $H/venv/bin/python, not bare python |
git MCP: fatal: not a git repository |
CLAUDE_ENV_REPO_ROOT unset |
claude-env onboard <repo>; restart Claude Code |
filesystem-policy: "file not found" |
wrong/unset repo root | same as above |
lancedb-rag returns empty |
wrong CLAUDE_ENV_REPO_NAME/BRANCH |
check ls ~/.claude-env/knowledge/lancedb/; re-onboard; restart |
memory.recall always [] |
nodes have embedding=NULL |
re-write nodes after model config, or back-fill (below) |
| RAG quality dropped / dim mismatch | embedding model dimension changed | vectors aren't comparable — drop tables + re-index (below) |
| Agent "BLOCKED" on a normal file | over-broad deny rule | inspect policy_violations; adjust repo deny/allow (never global) |
| Reranker not improving results | ONNX absent/failed/disabled | python -c "from rag.config import get_reranker; print(get_reranker().status())" — ok should be True |
| Audit chain reports broken | manual edit / partial restore | restore from a known-good backup; never edit audit_events |
| Memory leaks across repos | CLAUDE_ENV_MEMORY_ISOLATED unset for tier ≥ 2 |
set tier ≥ 2 in repo policy and re-onboard |
| Terminal command "GATED" | state-mutating command | route via approval_gate.py; only test/bench/audit run unattended |
| Every native tool call denied | incident mode active | claude-env incident status; claude-env incident off --by you |
| Bash command denied unexpectedly | it touches a denied path / network egress | see §5.8; use an allowed path or request approval |
| Hooks not firing | settings not installed / stale session | claude-env hooks then restart Claude Code |
Removing a repo's RAG index (needed after an embedding-model swap):
rm -rf ~/.claude-env/knowledge/lancedb/${REPO_SLUG}__${BRANCH}.lance/
~/.claude-env/venv/bin/python - <<'PY'
import os, sys; sys.path.insert(0, os.path.expanduser("~/.claude-env"))
from lib.db import get_db
db = get_db(); repo, branch = "your-repo-slug", "main"
db.execute("DELETE FROM rag_file_state WHERE repo=? AND branch=?", (repo, branch))
db.execute("DELETE FROM rag_index_state WHERE repo=? AND branch=?", (repo, branch))
try: db.commit()
except AttributeError: pass
print(f"Purged RAG index for {repo}@{branch}")
PYBack-filling memory embeddings (only if nodes were written with embedding=NULL): iterate
memory_nodes WHERE embedding IS NULL, embed name + body_json with
rag.config.get_embedder(), and UPDATE … SET embedding=?.
Logs live in ~/.claude-env/logs/ (incremental_index.log, memory.log, digests/).
Verify the audit chain any time:
~/.claude-env/venv/bin/python -c "import sys; sys.path.insert(0,'$HOME/.claude-env'); \
from audit.audit_logger import AuditLogger; print(AuditLogger('ops',actor='ops').verify_chain())"
# (True, None) on a clean ledgerTarget IDEs: IntelliJ IDEA Ultimate, PyCharm Professional, WebStorm, GoLand, Rider, CLion.
Setup: install the Claude Code plugin from the JetBrains Marketplace (it bundles the MCP
bridge on recent builds) → Settings → Tools → Claude Code: point it at your claude CLI,
enable "Use project MCP configuration", and set the project env (CLAUDE_ENV_REPO_ROOT=$ProjectFileDir$,
tier, CLAUDE_ENV_MEMORY_NS=proj-<slug>). Do not install third-party "AI" plugins that
phone home — this platform is local-only.
Recommended IDE settings: Actions-on-Save → Reformat + Optimize imports (so agent diffs
match house style); keep language inspections strict (the review agents consume them via the
bridge); enable "Run Git hooks" on commit (so the post-commit indexer fires); mark
generated/, vendor/, node_modules/ as Excluded.
Workflows: Code review — open the diff, ask Claude to review; the orchestrator routes to
Security + Performance (read-only) + Testing; findings are file:line clickable and security ones
land in security_events. Refactor — the Backend/Frontend agent proposes a diff within its
write_paths; prefer IDE-native Rename/Extract for mechanical steps; run tests before accepting.
Architecture review — ask the Architect for a module review; output is an ADR/RFC, not code.
Debug — give the failure context; agents localize via git.blame/RAG, propose a fix + a
regression test, and store the investigation as a memory node.
Security: the IDE bridge respects the same filesystem-policy chokepoint — a file blocked by
the repo policy is not surfaced even if open in the editor. No separate IDE credential store; no
outbound calls beyond the tier-gated documentation fetch.
SQLite is the default and is enough for a single machine. Migrate to PostgreSQL for
multi-machine sharing, concurrent writers, or centralized audit retention. Because all DB access
goes through lib/db.py, application code does not change.
Already portable: ? placeholders (rewritten to %s for psycopg), the WITH RECURSIVE
memory traversal, ON CONFLICT … DO UPDATE upserts, and struct float32 embedding BLOBs (→ BYTEA).
Dialect deltas (in a new sql/00N_pg.sql): INTEGER PRIMARY KEY AUTOINCREMENT →
BIGSERIAL; drop SQLite PRAGMAs (use server config); TEXT timestamps → TIMESTAMPTZ
(or keep ISO text for byte-identical audit payloads); BLOB → BYTEA; re-express the
append-only BEFORE UPDATE/DELETE triggers as PL/pgSQL raising an exception; views port unchanged.
Cutover (offline): createdb claude_env → apply the PG schema via get_db().apply_schema(...)
→ copy audit_events first, in ascending event_id order (preserve event_id, prev_hash,
event_hash verbatim — never recompute), then projections, memory, metrics → run verify_chain()
on PostgreSQL (must print (True, None)) → setval(...) the identity sequences → set
CLAUDE_ENV_DSN=$PGDSN in shell/launchd/MCP env → re-run the validators. LanceDB is unaffected
(replicate the dir for multi-machine). Rollback: the SQLite file is read-only during
migration — point CLAUDE_ENV_DSN back at it and re-validate.
Concurrency: the audit writer takes a process lock for monotonic chaining; with multiple
machines writing one PostgreSQL ledger, wrap read-prev-hash + insert in a SERIALIZABLE
transaction (retry on failure) or funnel audit writes through a single writer service. Memory
and metrics tolerate concurrent writers as-is.
claude-plugin/ packages the governance layer for a team: the policy + audit hooks
(hooks/hooks.json), the core MCP servers (.mcp.json), and slash commands
(/claude-env:know, /claude-env:report, /claude-env:replay).
/plugin marketplace add <org>/claude-env
/plugin install claude-env
# or, for local testing:
claude --plugin-dir /path/to/claude-env/claude-plugin
The platform must still be bootstrapped (§9) and models configured
(§10) on each machine — the plugin is only the wiring/distribution layer.
See claude-plugin/README.md. Without the plugin system, claude-env hooks (§11) +
§16 achieve the same wiring.
Internal engineering starter. Review config/global-policy.yaml and the tier matrix against
your organization's data-handling requirements before onboarding sensitive repositories.