# exomem — environment configuration template. # # Copy to `.env` and fill in: cp env.example .env # `.env` is gitignored; this template is the ONE complete reference for every # variable exomem reads. Not every var is needed for every mode — each block # says which. `exomem setup` (local) and `exomem setup --remote` write the # blocks they own for you; this file documents the rest. # # Local Claude Code (stdio) -> only EXOMEM_VAULT_PATH. # Remote connector (claude.ai / iOS) -> also the "Remote / GitHub OAuth" block. # Docker/tunnel deployment -> also the "Tunnel + Docker" block. # --------------------------------------------------------------------------- # Core — required in every mode # --------------------------------------------------------------------------- # Absolute path to your Obsidian vault ROOT — the folder that CONTAINS # "Knowledge Base/" (not the Knowledge Base folder itself). Required for every # mode: claude.ai connects over HTTP and passes no environment of its own, so # the remote service resolves the vault solely from here at startup. EXOMEM_VAULT_PATH=/path/to/your/Obsidian # --------------------------------------------------------------------------- # Search profile — optional # --------------------------------------------------------------------------- # Capability profile for `exomem doctor` when --profile is omitted. # lean | hybrid | media | remote # EXOMEM_PROFILE=hybrid # # Set to 1 for a lean, keyword/BM25-only install (no embeddings, no torch). # Leave unset for hybrid semantic search (needs `uv sync --extra embeddings`). # NOTE: this gates embedding WRITES + model warmup, not query-time reads. If a # `.embeddings.sqlite` sidecar already exists on disk, find() will still use the # vector lane for reads; to force a truly keyword-only comparison, query with # mode="keyword" (or remove/rename the sidecar). # EXOMEM_DISABLE_EMBEDDINGS=1 # Path to the Tesseract OCR binary if it is not on PATH (media profile only). # EXOMEM_TESSERACT_CMD=%ProgramFiles%\Tesseract-OCR\tesseract.exe # --------------------------------------------------------------------------- # Activation packet — optional # --------------------------------------------------------------------------- # What `recall_context` returns. `maps` (the default) returns a map: an index of # what the vault holds, which the client reads and pulls from by ref. `anchors` # restores the earlier packet, which resolves the turn to anchors. Any other # value logs one warning and serves `maps`. A change takes effect when the # service restarts. See docs/deployment.md ("The activation packet mode"). # EXOMEM_ACTIVATION_PACKET=anchors # With the `maps` packet, the turn slice (on by default) lists the leads that the turn's own # words reached, with their evidence, beside the map. `off` restores the map alone. Any other # value logs one warning and serves the slice. A change takes effect when the service # restarts. See docs/deployment.md ("The turn slice"). # EXOMEM_ACTIVATION_TURN_SLICE=off # --------------------------------------------------------------------------- # Compute mode & device (per-machine) — optional # --------------------------------------------------------------------------- # The steady-state default is CPU (idle GPU VRAM ~0). GPU is an opt-in. The # simplest control is the MODE; set it once with `exomem mode `. It writes a # machine-wide config (%PROGRAMDATA%\exomem\config.json on Windows, ~/.exomem/config.json # on macOS/Linux) read by BOTH the server and CLI ops — machine-wide on purpose so a # LocalSystem service and your user CLI share it. The server applies a change live within # ~10s, no restart. EXOMEM_MODE below overrides the file for THIS process only (the # server reads .env; CLI ops do not). EXOMEM_CONFIG_PATH overrides the file location. # quiet - CPU everywhere, no model preload, release models when idle. # normal - default. CPU steady-state (MPS on Apple Silicon); never auto-CUDA. # performance - use the GPU when a capable one is present; preloaded models stay # resident so no request pays a load (aliases: gpu, turbo). # EXOMEM_MODE=normal # # Startup model preload. Normal/quiet default to lazy model load on every OS; # performance preloads. Set 1 only when startup latency matters more than RAM. # Preload also PINS the models against the idle reaper — otherwise the reload # lands on whichever request arrives after the idle window (~35s inside a write). # EXOMEM_PRELOAD_MODELS=1 # # Offline-first weight loading (default on). Weights already in the local HF cache # load without revalidating every file against the hub; an absent or unusable cache # still falls back to a normal download. Set 0 to always revalidate. # EXOMEM_MODEL_OFFLINE=0 # # Where a served model's int8 artefact lives (about 0.55 GB for bge-m3). Default: # `exomem-artifacts` beside the HF hub cache. # EXOMEM_MODEL_ARTIFACT_DIR=/data/hf/exomem-artifacts # # Where the published artefact is downloaded from (a base URL; the asset name is # appended). Every byte is checked against the pinned digest; without a usable # download the host builds it locally, which peaks at about 8.7 GB of memory. # Default: the project's `served-models` GitHub release. `off` always builds locally. # EXOMEM_MODEL_ARTIFACT_URL=https://mirror.example.com/exomem/served-models # # Explicit device override (wins over the mode). cpu | gpu | auto | cuda | cuda:1 | mps # 'gpu'/'auto' opt in with a marginal-VRAM guard; 'cuda' forces it (legacy behavior). # EXOMEM_DEVICE= # global; legacy alias EXOMEM_TORCH_DEVICE # EXOMEM_EMBED_DEVICE= # bge embedder + reranker (text path) only # EXOMEM_CLIP_DEVICE= # CLIP image/text only # EXOMEM_ASR_DEVICE= # faster-whisper ASR (cpu|cuda|auto); own path, not via the above # EXOMEM_GPU_MIN_FREE_GB=2 # min free VRAM to accept a GPU (the marginal-VRAM guard) # # ASR startup preload defaults off on every OS. Product mode starts a disposable # media child only when work arrives. Set 1 to preload inside that child. # EXOMEM_ASR_PREWARM=1 # EXOMEM_DISABLE_ASR_PREWARM=1 # # Idle-unload reaper: release resident models/caches after N idle minutes. # Default on in every mode, except that preloaded MODELS are pinned (caches still # reap). Setting this explicitly wins either way — 'on' reaps models even under # preload, at the cost of a cold load on the next request. # EXOMEM_RELEASE_GPU_WHEN_IDLE= # on|off # EXOMEM_IDLE_MINUTES=15 # EXOMEM_MEDIA_IDLE_SECONDS=300 # disposable media child exits after this idle window # EXOMEM_MEDIA_WORKER_MODE=process # process (product default) | inline (debug rollback) # # Model selection & footprint (advanced). The reranker is a stateless scorer, so it # can be swapped or disabled with no re-index. # EXOMEM_RANKING_MODEL=BAAI/bge-reranker-base # legacy alias EXOMEM_RERANKER_MODEL # EXOMEM_DISABLE_RANKING=1 # turn the reranker OFF entirely (~0.44 GB + latency saved) # # "lite" recipe for a low-resource machine (keeps semantic search, drops the heavy # extras): hybrid embeddings ON, reranker + image search OFF. # EXOMEM_DISABLE_RANKING=1 # EXOMEM_DISABLE_CLIP=1 # # Live write/import semantic indexing. Ordinary note writes embed immediately. # Larger watcher batches keep lexical/freshness indexes current and defer # semantic embeddings so imports do not load the full vector stack in every # local client process. Run `exomem index --scope vault` after a bulk import. # EXOMEM_WATCHER_MAX_EMBED_FILES=32 # EXOMEM_LIVE_EMBED_MAX_CHUNKS=256 # maximum chunks passed to one live encode call # --------------------------------------------------------------------------- # HTTP transport bind — optional (remote / service use) # --------------------------------------------------------------------------- # Bind address for the http transport. Default 127.0.0.1 (fronted by a tunnel). # Set 0.0.0.0 to also serve a direct LAN / Tailscale route. # EXOMEM_HOST=127.0.0.1 # --------------------------------------------------------------------------- # Observability — optional (see docs/observability.md) # --------------------------------------------------------------------------- # Default-on and stdlib-only: every piece here soft-fails independently, so a # bug in logging or metrics can never break a tool call. Nothing below is # required to run exomem; the disable flags exist as an operator escape # hatch, not because the default posture is unsafe. # Log directory. Default: /logs, or /data/logs in the Docker image. # Must be a PROCESS env var (container ENV, service environment, shell) — # logging configures before .env loads, so a value only in .env arrives too late. # EXOMEM_LOG_DIR=/path/to/logs # Root log level. Canonical name; FASTMCP_LOG_LEVEL is honored as a fallback # so fastmcp's own DEBUG lines stay reachable without a code change. # EXOMEM_LOG_LEVEL=INFO # Per-process JSONL file rotation (exomem.log / exomem-cli.log / exomem-media.log). # EXOMEM_LOG_MAX_MB=5 # EXOMEM_LOG_BACKUPS=5 # Size cap for logs/{queries,writes,reads,mutations}.jsonl before rotating to # one kept `.1` generation. # EXOMEM_JSONL_MAX_MB=64 # The call ledger: one hash-chained row per MCP tool call, in logs/ledger.jsonl. # It is the only source that covers every call and the only one that records # which client made it. Arguments are recorded as name + length + sha256, never # as values, so turning it off does not protect content — it only removes the # record you would want during an incident. # EXOMEM_DISABLE_CALL_LEDGER=1 # Move the ledger off the log directory. Keep it host-local and outside the # vault: a ledger that cannot write during a read-only-vault incident cannot # explain that incident. # EXOMEM_CALL_LEDGER_DIR=/path/to/ledger # Live-file bound. Past ROTATE_BYTES, the oldest rows beyond KEEP_ROWS move # byte-exact into logs/ledger-archive/; `sequence` does not reset and the hash # chain spans the boundary. Archives are not pruned automatically. # EXOMEM_CALL_LEDGER_ROTATE_BYTES=8000000 # EXOMEM_CALL_LEDGER_KEEP_ROWS=2000 # How often the metrics registry persists its counters to the writer-lease # state directory so a restart doesn't reset them to zero. 0 disables the # background snapshotter thread entirely. # EXOMEM_METRICS_SNAPSHOT_SECONDS=60 # Turn off the structured HTTP access log middleware (uvicorn's own access # log stays silenced either way — see docs/observability.md). # EXOMEM_DISABLE_ACCESS_LOG=1 # Turn off the metrics registry: /metrics.json returns 404 and counters stop # accumulating. # EXOMEM_DISABLE_METRICS=1 # Writer-lease idle release (HA only). A NON-preferred replica voluntarily # releases the writer lease after this many seconds without a mutation, so # the preferred replica reclaims within about a third of the lease TTL. # Preferred replicas never idle-release; 0 disables. When unset the default # tracks the TTL — max(60, EXOMEM_WRITER_LEASE_TTL) — so raising the TTL # alone never trips validation. Under EXOMEM_WRITER_LEASE_TOPOLOGY=single-host # the unset default is 0, because no other replica can take the lease. An # explicit value must be 0 or >= the TTL. # EXOMEM_WRITER_LEASE_IDLE_SECONDS=60 # --------------------------------------------------------------------------- # Remote connector / GitHub OAuth — required for claude.ai + iOS access # --------------------------------------------------------------------------- # `exomem setup --remote` writes this whole block. See docs/remote-quickstart.md. # Public HTTPS origin the connector reaches. NO trailing slash, NO /mcp suffix. # e.g. https://kb.example.com or https://you.ngrok-free.dev EXOMEM_BASE_URL=https://your-host.example.com # GitHub OAuth App (github.com/settings/developers -> OAuth Apps -> New OAuth App). # The Authorization callback URL must be exactly https:///auth/callback GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= # The single GitHub login allowed to authenticate to this connector. EXOMEM_GITHUB_USERNAME= # Immutable positive numeric ID for that login (`GET /users/` -> `id`). # This prevents a renamed/recycled login from becoming the trusted account. EXOMEM_GITHUB_USER_ID= # Optional. Set to github: when that account # is your own: its remote sign-ins then act as the owner, labelled remote in the # ledgers. Unset, a remote sign-in is a separate non-owner principal. # EXOMEM_OWNER_OAUTH_SUBJECT= # Required stable root for opaque durable sessions. Rotating it intentionally # invalidates every Exomem session. Generate with: # python -c "import secrets; print(secrets.token_urlsafe(48))" EXOMEM_JWT_SIGNING_KEY= # Optional manual cold-standby readiness identity. This is a public, non-secret # host label used only by /health/ready (for example: desktop-01). It must match # [A-Za-z0-9][A-Za-z0-9._-]{0,63}; invalid values are reported as null. # EXOMEM_INSTANCE_ID= # HA only: all three values must be the SAME non-empty random bearer. The # coordinator enforces it; replicas use it for leases and authoritative auth state. # EXOMEM_LEASE_COORDINATOR_TOKEN= # EXOMEM_WRITER_LEASE_TOKEN= # EXOMEM_OAUTH_STORAGE_TOKEN= # --------------------------------------------------------------------------- # Remote extras — optional # --------------------------------------------------------------------------- # Bearer key for the personal REST facade (/api/). Generate: # uv run --no-sync python scripts/set-rest-key.py # # Setting this ALSO unlocks a loopback-only HTTP server: # exomem --transport http --host 127.0.0.1 --port 8765 # GitHub OAuth is skipped for it, but ONLY when all three hold — the bind host # is literally 127.0.0.1 or ::1, EXOMEM_BASE_URL is unset, and this key is set. # /api/* still requires this bearer key; what the skip relaxes is the MCP # endpoints, which become reachable by any local process — the same boundary a # local stdio server already has. Set EXOMEM_BASE_URL plus the GitHub OAuth # block for a remote connector: the two are mutually exclusive by design, so a # misconfigured remote deployment cannot silently degrade into this mode. # EXOMEM_REST_API_KEY= # Secret that enables binary upload/download token minting. Generate: # uv run python scripts/set-upload-token.py # EXOMEM_UPLOAD_TOKEN= # Managed Linux/WSL service only: open a second, loopback-only listener for # same-machine clients holding `exomem auth issue-local` tokens. Off when unset; # takes effect when the service unit restarts. See docs/deployment.md # ("Same-machine clients: the local listener"). # EXOMEM_LOCAL_PORT=8764 # --------------------------------------------------------------------------- # Tunnel + Docker — compose deployment only (see compose.yaml / docs/docker.md) # --------------------------------------------------------------------------- # HOST path bind-mounted to /vault in the container. The compose stack sets the # container's own EXOMEM_VAULT_PATH=/vault, so this is the host side of that mount. # EXOMEM_VAULT_HOST_PATH=/path/to/your/Obsidian # ngrok (docker compose --profile ngrok, or scripts/setup-ngrok.ps1): # NGROK_DOMAIN is your free static dev domain; NGROK_AUTHTOKEN from the dashboard. # NGROK_AUTHTOKEN= # NGROK_DOMAIN=you.ngrok-free.dev # Cloudflare Tunnel (docker compose --profile cloudflared): # CLOUDFLARE_TUNNEL_TOKEN=