elfmem Setup and Configuration Guide¶
Complete guide to setting up environment variables and configuration for elfmem MCP server and CLI tools.
Table of Contents¶
- Quick Start
- Initialisation Commands
- Environment Variables
- YAML Configuration
- MCP Server Setup
- Configuration Methods
- Examples
Quick Start¶
Installing from source: use uv sync --extra mcp --extra cli (or --all-extras). Never uv add elfmem[mcp] — since the package is itself named elfmem, uv add tries to add a third-party dependency on itself and fails with a self-dependency error. --extra is singular, repeated once per extra.
First-Time Setup¶
Run elfmem init once before anything else. It creates ~/.elfmem/, generates a default config.yaml, and optionally seeds your SELF frame (agent identity):
# Basic init (config + database only)
elfmem init
# Recommended: seed agent identity at the same time
elfmem init --self "I am an AI assistant focused on [your purpose]."
# Verify everything is ready
elfmem doctor
elfmem init is idempotent — safe to re-run. Config is skipped if it already exists (pass --force to overwrite). Duplicate SELF content is silently rejected.
Minimal MCP Server (All Defaults)¶
# After init, start the server
export ELFMEM_DB=~/.elfmem/agent.db
export ELFMEM_CONFIG=~/.elfmem/config.yaml
elfmem serve
With Custom LLM Provider¶
# Use Ollama (local)
export ELFMEM_DB=~/.elfmem/agent.db
export ELFMEM_LLM_MODEL="llama3.2"
export ELFMEM_LLM_BASE_URL="http://localhost:11434"
elfmem serve --db $ELFMEM_DB
With Config File¶
Initialisation Commands¶
elfmem init¶
Creates the elfmem data directory, generates a default config, and optionally seeds the SELF frame.
elfmem init [OPTIONS]
Options:
--self TEXT Natural language description of agent identity (seeds SELF frame)
--name TEXT Agent invocation name; renders an "Agent Identity" section into .elfmem/AGENT.md
--seed/--no-seed Seed the constitutional cognitive loop (10 role-tagged blocks). Opt-in, default off
--template TEXT Layer a domain-specific seed on top of --seed; `elfmem templates` lists options
--db TEXT Database path (default: auto from project name, env: ELFMEM_DB)
--config TEXT Config YAML path (default: auto-discovered, env: ELFMEM_CONFIG)
--global Force ~/.elfmem/ regardless of project detection
--force Overwrite existing config (default: skip if exists; never overwrites the DB)
--force-new Bypass the orphaned-DB rescue check. Almost never needed — prefer `elfmem rescue`
--docs-file TEXT Write the elfmem section to this agent doc file instead of CLAUDE.md/AGENTS.md
--no-docs Skip writing to CLAUDE.md / AGENTS.md
--json Output JSON instead of human-readable text
Breaking change (v2 step 4): a fresh elfmem init writes zero memory blocks by default — previously it always seeded the 10 constitutional cognitive-loop blocks. Pass --seed explicitly to get the old behaviour. Established instances (config + DB already populated) are unaffected — re-running init without --seed is still a no-op refresh.
When to use:
- First run — before elfmem serve or any CLI/MCP tool use
- When you want to add or update agent identity (re-running with --self is safe; duplicates are rejected)
- After changing config defaults and wanting to regenerate config.yaml (use --force)
When NOT to use:
- Every session — SELF blocks persist; don't re-seed on every agent start
- In production pipelines — use elfmem_setup MCP tool for programmatic identity seeding
elfmem doctor¶
Read-only diagnostic command. Checks config, database, SELF blocks, and API keys. No side effects.
elfmem doctor [OPTIONS]
Options:
--db TEXT Database path (default: auto-discovered, env: ELFMEM_DB)
--config TEXT Config YAML path (default: auto-discovered, env: ELFMEM_CONFIG)
--resolve Make one real LLM call against the configured llm: section to confirm
the key actually works (not just that a key string is present). Costs
time and, for hosted models, money — opt-in
--migrate-mcp Scan MCP configs (~/.claude.json, .mcp.json) for stale entries, print
fixes only — apply with `elfmem migrate apply`
--modules Print the live module map (src/elfmem/project.py KEY_MODULES) and exit
--json Output JSON
Output example:
Config dir: ~/.elfmem/ (exists)
Config: ~/.elfmem/config.yaml (exists)
Database: ~/.elfmem/agent.db (exists)
SELF: 3 SELF blocks found
API keys: ANTHROPIC_API_KEY set
Exits 0 if everything is healthy, 1 if any check fails (useful for CI/CD).
elfmem_setup (MCP tool)¶
Agent-native equivalent of elfmem init --self. Call it from Claude or any MCP client to seed the SELF frame programmatically:
elfmem_setup(
identity="I am Claude Code, an AI-powered software engineering assistant.",
values=["write minimal clean code", "confirm before destructive operations"]
)
# Returns: {"blocks_created": 2, "total_attempted": 2}
Safe to call multiple times — exact duplicates are silently rejected.
Environment Variables¶
All env vars are optional. Provider API keys are read by the Anthropic and OpenAI SDKs from standard env vars at the time of the first API call.
A .env file at the project root (next to .elfmem/config.yaml) is auto-discovered and loaded for every CLI command — remember, recall, doctor, all of them, not just serve --env-file. Real environment variables always win over .env values. elfmem doctor's "API keys" check reports whether a key came from .env or the real environment.
Database & Config Paths¶
| Variable | Purpose | Example |
|---|---|---|
ELFMEM_DB |
SQLite database file path | /data/agent.db |
ELFMEM_CONFIG |
YAML config file path | ./config/elfmem.yaml |
LLM Configuration¶
| Variable | Default | Example | Notes |
|---|---|---|---|
ELFMEM_LLM_MODEL |
claude-sonnet-4-6 |
gpt-4o-mini, ollama/llama3.2 |
Model name (no provider prefix for Ollama) |
ELFMEM_LLM_BASE_URL |
None |
http://localhost:11434 |
Optional: proxy/local endpoint |
Embedding Configuration¶
| Variable | Default | Example | Notes |
|---|---|---|---|
ELFMEM_EMBEDDING_MODEL |
text-embedding-3-small |
nomic-embed-text |
OpenAI/Ollama embeddings |
ELFMEM_EMBEDDING_DIMENSIONS |
1536 |
768 |
Must match model output |
ELFMEM_EMBEDDING_BASE_URL |
None |
http://localhost:11434 |
Ollama or proxy endpoint |
Provider API Keys¶
| Provider | Env Var | Example |
|---|---|---|
| OpenAI | OPENAI_API_KEY |
sk-... |
| Anthropic | ANTHROPIC_API_KEY |
sk-ant-... |
| Groq | GROQ_API_KEY |
gsk_... |
The Anthropic and OpenAI SDKs read these standard env vars automatically. No additional setup needed.
Any other provider — llm.api_key_env / embeddings.api_key_env: every OpenAI-compatible adapter reads the literal OPENAI_API_KEY regardless of base_url unless told otherwise — so Together.ai, Groq, OpenRouter, or any other OpenAI-compatible provider only worked if you misnamed your key OPENAI_API_KEY. Name the real variable explicitly:
llm:
model: "meta-llama/Llama-3.3-70B-Instruct-Turbo"
base_url: "https://api.together.xyz/v1"
api_key_env: "TOGETHER_API_KEY"
Unset (the default) is unchanged behaviour: OPENAI_API_KEY for OpenAI-compatible adapters, ANTHROPIC_API_KEY for claude-* models. A misconfigured api_key_env resolves to no key rather than silently falling back to a real-but-wrong one.
YAML Configuration¶
Three-tier configuration override system: 1. Inline strings (highest priority) — in YAML 2. File paths — referenced from YAML 3. Code defaults — library defaults
Full Configuration File Example¶
# elfmem.yaml
llm:
model: "claude-sonnet-4-6"
temperature: 0.0
max_tokens: 512
timeout: 30
max_retries: 3
base_url: null
api_key_env: null # e.g. "TOGETHER_API_KEY" for non-Anthropic/OpenAI providers
# Per-call model override (optional)
process_block_model: null # Use model above if null
embeddings:
model: "text-embedding-3-small"
dimensions: 1536
timeout: 30
base_url: null
api_key_env: null
memory:
# Lifecycle
inbox_threshold: 10
curate_interval_hours: 40.0
search_window_hours: 200.0
vector_n_seeds_multiplier: 4
max_inbox_per_run: 5 # bounds one consolidate()/dream() call (ADR 0007)
# Quality thresholds
self_alignment_threshold: 0.70
near_dup_exact_threshold: 0.95
near_dup_near_threshold: 0.90
# Graph
edge_score_threshold: 0.45
edge_degree_cap: 5
edge_prune_threshold: 0.10
edge_reinforce_delta: 0.10
# Scoring
top_k: 5
curate_reinforce_top_n: 5
# Outcome scoring
outcome_prior_strength: 2.0
outcome_reinforce_threshold: 0.5
penalize_threshold: 0.20
penalty_factor: 2.0
lambda_ceiling: 0.050
prompts:
# Inline override (None = use library default)
process_block: null # Combined block analysis prompt
# File-based override
process_block_file: null # Path to custom process_block prompt
# Custom tag vocabulary
valid_self_tags: null # null = use library defaults
Removed in v2 (ADR 0009, ADR 0010) — a config that still sets these has them silently ignored, not an error: memory.prune_threshold (block-level archival trigger, retired — curate() no longer auto-archives, see elfmem review corpus), memory.contradiction_threshold / .contradiction_similarity_prefilter / .contradiction_top_k, llm.contradiction_model, prompts.contradiction / .contradiction_file. memory.similarity_edge_threshold was always a misnomer for edge_score_threshold, corrected above.
Minimal Configuration File¶
Local Ollama Setup¶
# config-ollama.yaml
llm:
model: "llama3.2"
base_url: "http://localhost:11434"
temperature: 0.0
max_tokens: 512
embeddings:
model: "nomic-embed-text"
dimensions: 768
base_url: "http://localhost:11434"
Custom Prompts (Inline)¶
# config-custom-prompts.yaml
llm:
model: "gpt-4o-mini"
prompts:
process_block: |
You are analyzing a memory block for a research assistant.
Agent Identity:
{self_context}
Block:
{block}
Return JSON with: alignment_score [0-1], tags [list], summary [string]
contradiction: |
Are these two research findings contradictory?
Finding A: {block_a}
Finding B: {block_b}
Return JSON: {"score": <0.0-1.0>}
Custom Prompts (File-Based)¶
# config-file-prompts.yaml
llm:
model: "claude-sonnet-4-6"
prompts:
process_block_file: ./prompts/my_analysis.txt
contradiction_file: ./prompts/my_contradiction.txt
# Custom tag vocabulary
valid_self_tags:
- "self/identity"
- "self/value"
- "self/preference"
- "self/goal"
- "self/constitutional"
- "self/skill"
Per-Call Model Overrides¶
# Use a stronger model for block analysis specifically; everything else uses model above
llm:
model: "gpt-4o-mini"
process_block_model: "gpt-4" # Override for block analysis
Substrate: where memory actually lives¶
Two storage modes, and the difference is which copy is believed when they disagree:
false (default) |
true |
|
|---|---|---|
| source of truth | the SQLite database | .elfmem/memory/**.md |
| the database is | everything | a derived index, rebuildable |
undo for forget()/edit() |
none | git history |
| rebuild command | n/a | elfmem index rebuild (files + .elfmem/ledger/) |
Do not set this by hand on a database that already has content. Turning it on does not move anything — it only changes which copy is trusted, so a project whose files are empty or stale would start believing the wrong one. Use the migration, which exports, verifies, and only then offers the flip:
See the README's Substrate migration section for the full flow, the preflight checks, and the rollback path.
Editing it by hand is reasonable in exactly one case: a brand-new project
with no blocks yet, where there is nothing to migrate and nothing to lose.
Commit .elfmem/memory/ and .elfmem/ledger/ either way — under file
authority they are your data, not build output.
MCP Server Setup¶
Starting the Server¶
Using CLI¶
# With env vars
export ELFMEM_DB=/data/agent.db
export ELFMEM_CONFIG=./config/elfmem.yaml
elfmem serve
# With arguments
elfmem serve --db /data/agent.db --config ./config/elfmem.yaml
Programmatically¶
from elfmem.mcp import main
# Start with defaults and env var config
main(db_path="/data/agent.db")
# Start with explicit config path
main(db_path="/data/agent.db", config_path="./elfmem.yaml")
Integration with Claude Desktop¶
Create .claude/claude.json:
{
"mcpServers": {
"elfmem": {
"command": "elfmem",
"args": ["serve", "--db", "$HOME/agent.db", "--config", "$HOME/elfmem.yaml"],
"env": {
"OPENAI_API_KEY": "${OPENAI_API_KEY}",
"ANTHROPIC_API_KEY": "${ANTHROPIC_API_KEY}",
"GROQ_API_KEY": "${GROQ_API_KEY}"
}
}
}
}
Integration with Other MCP Clients¶
Configuration Methods¶
Priority Order (Highest → Lowest)¶
-
Command-line arguments
-
Environment variables
-
Config file values (YAML)
-
Code defaults (library built-ins)
model: "claude-haiku-4-5-20251001"embeddings.model: "text-embedding-3-small"embeddings.dimensions: 1536
Resolution Flow¶
Command-line arg
↓ (if not provided)
Env var (ELFMEM_*)
↓ (if not set)
YAML config value
↓ (if not specified)
Library default
MemorySystem.from_config()¶
The Python API offers flexible configuration:
from elfmem.api import MemorySystem
# 1. From YAML file
system = await MemorySystem.from_config(
"agent.db",
config="./elfmem.yaml"
)
# 2. From env vars (ELFMEM_*)
system = await MemorySystem.from_config("agent.db")
# 3. From inline dict
system = await MemorySystem.from_config(
"agent.db",
config={
"llm": {"model": "gpt-4o-mini"},
"embeddings": {"model": "text-embedding-3-small"}
}
)
# 4. From pre-built ElfmemConfig object
from elfmem.config import ElfmemConfig
cfg = ElfmemConfig.from_yaml("./elfmem.yaml")
system = await MemorySystem.from_config("agent.db", config=cfg)
# 5. From environment variables only
system = await MemorySystem.from_env("agent.db")
Examples¶
Example 1: OpenAI (Quickest)¶
Setup:
# 1. Install with CLI extra
pip install 'elfmem[cli]' # or: uv add 'elfmem[cli]'
# 2. Set API key (already installed, OpenAI reads OPENAI_API_KEY)
export OPENAI_API_KEY=sk-...
# 3. Set database
export ELFMEM_DB=~/.elfmem/agent.db
# 4. Start
elfmem serve
All defaults work: Claude Sonnet embeddings via OpenAI.
Example 2: Anthropic with Custom Config¶
Setup:
# 1. Install
pip install 'elfmem[cli]'
# 2. Set API key
export ANTHROPIC_API_KEY=sk-ant-...
# 3. Create config
cat > elfmem.yaml <<EOF
llm:
model: "anthropic/claude-opus-4-6"
temperature: 0.0
max_tokens: 1024
embeddings:
model: "text-embedding-3-small"
EOF
# 4. Start
export ELFMEM_DB=./agent.db
export ELFMEM_CONFIG=./elfmem.yaml
elfmem serve
Example 3: Ollama (Self-Hosted)¶
Setup:
# 1. Install Ollama, pull models
ollama pull llama3.2
ollama pull nomic-embed-text
# 2. Start Ollama server (default: http://localhost:11434)
ollama serve
# 3. Create config (in another terminal)
cat > ollama-config.yaml <<EOF
llm:
model: "llama3.2"
base_url: "http://localhost:11434"
embeddings:
model: "nomic-embed-text"
dimensions: 768
base_url: "http://localhost:11434"
memory:
top_k: 5
EOF
# 4. Start server
export ELFMEM_DB=./agent.db
elfmem serve --db $ELFMEM_DB --config ./ollama-config.yaml
Example 4: Docker Deployment¶
Dockerfile:
FROM python:3.12-slim
WORKDIR /app
RUN pip install 'elfmem[cli,mcp]'
# Create data directory
RUN mkdir -p /data
ENTRYPOINT ["elfmem", "serve"]
CMD ["--db", "/data/agent.db"]
docker-compose.yml:
version: "3.8"
services:
elfmem:
build: .
environment:
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
ELFMEM_CONFIG: /etc/elfmem/config.yaml
OPENAI_API_KEY: ${OPENAI_API_KEY}
volumes:
- ./data:/data
- ./elfmem.yaml:/etc/elfmem/config.yaml:ro
ports:
- "5173:5173" # MCP port
ollama: # Optional: self-hosted LLM/embeddings
image: ollama/ollama
ports:
- "11434:11434"
volumes:
- ./ollama-data:/root/.ollama
Run:
Example 5: Development Setup¶
Project structure:
my-agent/
├── .env # Private secrets (git-ignored)
├── .env.example # Template (committed)
├── config/
│ ├── elfmem-dev.yaml # Development config
│ └── elfmem-prod.yaml # Production config
├── prompts/
│ ├── process_block.txt
│ └── contradiction.txt
└── data/
└── agent.db # SQLite database
.env
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
ELFMEM_DB=./data/agent.db
ELFMEM_CONFIG=./config/elfmem-dev.yaml
.env.example
config/elfmem-dev.yaml
llm:
model: "claude-haiku-4-5-20251001" # Fast & cheap for dev
max_tokens: 512
embeddings:
model: "text-embedding-3-small"
memory:
inbox_threshold: 5 # Consolidate faster in dev
curate_interval_hours: 1.0
config/elfmem-prod.yaml
llm:
model: "claude-opus-4-6" # Best quality for production
max_tokens: 1024
embeddings:
model: "text-embedding-3-large"
memory:
inbox_threshold: 50
curate_interval_hours: 24.0
Start development:
Verification¶
Check Configuration is Applied¶
# List current status (includes config snapshot)
elfmem status --db $ELFMEM_DB
# JSON output for scripting
elfmem status --db $ELFMEM_DB --json | jq .
Verify LLM/Embedding Connection¶
# Test a simple remember-recall cycle
elfmem remember "Test memory" --db $ELFMEM_DB
elfmem recall "test" --db $ELFMEM_DB --top-k 1
# Check tokens used (embedded in status if token tracking enabled)
elfmem status --db $ELFMEM_DB
Debug Environment¶
# Check what ELFMEM_* env vars are set
env | grep ELFMEM
# Check if API keys are available to LiteLLM
export OPENAI_API_KEY=sk-test
Common Scenarios¶
"Model not found" error¶
Check:
# 1. Verify env var or config
echo $ELFMEM_LLM_MODEL
# 2. Verify API key
echo $ANTHROPIC_API_KEY | head -c 10
# 3. Verify LiteLLM supports it
"Embedding dimensions mismatch" error¶
Fix:
# Verify dimensions match the model:
# - text-embedding-3-small: 1536
# - text-embedding-3-large: 3072
# - nomic-embed-text (Ollama): 768
embeddings:
model: "nomic-embed-text"
dimensions: 768 # Must match!
"Connection refused to Ollama"¶
Check:
# 1. Is Ollama running?
curl http://localhost:11434/api/tags
# 2. Verify base_url in config
# 3. Check firewall/networking
"json: unsupported value: NaN" from Ollama (bge-m3 / other embedding models)¶
Symptom: elfmem's embedding adapter raises an InternalServerError: 500 and the message contains failed to encode response: json: unsupported value: NaN. The vector returned by Ollama contains NaN floats, which JSON can't serialise.
Cause: Ollama's flash-attention path produces NaN on some embedding models (reported with bge-m3).
Fix: disable flash attention before starting Ollama:
Or set it permanently in your shell rc / systemd unit.High token usage¶
Tune in config:
Summary¶
Quickest start:
Production setup:
export ELFMEM_DB=/data/agent.db
export ELFMEM_CONFIG=/etc/elfmem/config.yaml
export ANTHROPIC_API_KEY=sk-ant-...
elfmem serve
Local development (Ollama):
All three approaches work with the same codebase. Choose based on your deployment model.