Nerve uses two YAML config files:
config.yaml— Template settings (version controlled)config.local.yaml— Secrets and personal overrides (gitignored)
Values in config.local.yaml are deep-merged on top of config.yaml.
Unknown keys are ignored but logged as warnings at startup (and shown by
nerve doctor) so typos don't fail silently.
nerve commands locate the config directory via a waterfall, so they work
from any working directory:
--config-dir/-cflagNERVE_CONFIG_DIRenvironment variable- The current directory, if it contains
config.yamlorconfig.local.yaml - The pointer file
~/.nerve/config_dir(written bynerve initand on daemon start) - The current directory (fresh-install fallback)
nerve doctor reports which directory was used and how it was found.
| Key | Type | Default | Description |
|---|---|---|---|
workspace |
path | ~/nerve-workspace |
Path to workspace directory |
timezone |
string | America/New_York |
Local timezone for scheduling |
deployment |
string | server |
server (bare metal) or docker. Set during nerve init; determines whether CLI commands run directly or proxy to docker compose. |
Note: The mode (personal vs worker) is not a config field — it's determined at
nerve inittime and expressed through which workspace templates, cron jobs, and memory categories are active. There's nomodekey in config.
| Key | Type | Default | Description |
|---|---|---|---|
agent.model |
string | claude-opus-4-8 |
Primary model for conversations |
agent.cron_model |
string | claude-sonnet-4-6 |
Model for cron jobs (cheaper) |
agent.max_turns |
int | 50 |
Max agentic turns per request |
agent.max_concurrent |
int | 4 |
Max concurrent agent sessions |
agent.prompt_rewrite.enabled |
bool | true |
Offer the first-prompt rewrite feature in the web UI (per-user toggle lives in the composer) |
agent.prompt_rewrite.model |
string | "" |
Model for prompt rewriting (empty = agent.model, the chat model) |
agent.prompt_rewrite.max_tokens |
int | 1024 |
Max tokens for the rewritten prompt |
agent.prompt_rewrite.timeout_seconds |
float | 45.0 |
Rewrite API call timeout |
Prompt rewrite: when the ✨ toggle in the composer is on, the first prompt of a new chat is rewritten by a fast model to better express intent. The result is previewed (editable) and only sent after explicit approval — the user can always send the original instead. Trivial or already-clear prompts are sent unchanged without a preview.
Note: The engine uses a can_use_tool callback (not bypassPermissions) so that interactive tools (AskUserQuestion, ExitPlanMode, EnterPlanMode) can pause mid-turn for user input. All other tools are auto-approved. See sdk-sessions.md for details.
| Key | Type | Default | Description |
|---|---|---|---|
gateway.host |
string | 0.0.0.0 |
Bind address |
gateway.port |
int | 8900 |
Port number |
gateway.ssl.cert |
path | - | SSL certificate path |
gateway.ssl.key |
path | - | SSL private key path |
| Key | Type | Default | Description |
|---|---|---|---|
telegram.enabled |
bool | true |
Enable Telegram bot |
telegram.bot_token |
string | - | Bot token from @BotFather |
telegram.dm_policy |
string | pairing |
pairing (allowlist + one-time pairing codes) or open (anyone — dangerous) |
telegram.allowed_users |
list[int] | [] |
Telegram user IDs allowed to DM the bot |
telegram.stream_mode |
string | partial |
partial (edit msgs) or full |
With dm_policy: pairing (the default), the bot only talks to users in
allowed_users and rejects everyone else. To authorize a user without
editing config files:
- Run
nerve pairon the server — it prints a one-time 6-digit code (valid 1 hour). On a fresh install with noallowed_users, a code is also generated automatically at startup and printed to the log. - Send the bot
/pair <code>from the Telegram account to authorize. - The user ID is appended to
telegram.allowed_usersinconfig.local.yamland takes effect immediately.
An unauthorized /start gets a reply with the sender's numeric ID and
pairing instructions (rate-limited); all other messages from unauthorized
users are ignored.
| Key | Type | Default | Description |
|---|---|---|---|
quiet_start |
string | 02:00 |
HH:MM — start of quiet period (local timezone) |
quiet_end |
string | 08:00 |
HH:MM — end of quiet period (local timezone) |
Sources pull data from external services on a schedule. See sources.md for full details.
Common fields (available on all sources):
| Key | Type | Default | Description |
|---|---|---|---|
sync.<source>.enabled |
bool | true |
Enable/disable this source |
sync.<source>.schedule |
cron/interval | varies | Fetch frequency (crontab or interval like 2h) |
sync.<source>.processor |
string | agent |
agent (LLM review), memorize (direct memU), notify (channel forward), none |
sync.<source>.batch_size |
int | 50 |
Max records per fetch cycle |
sync.<source>.prompt_hint |
string | "" |
Extra instructions for the agent prompt |
sync.<source>.model |
string | "" |
Override model (empty = agent.cron_model) |
sync.<source>.condense |
bool | false |
LLM-condense long records via memory.fast_model before processing |
Telegram-specific:
| Key | Type | Default | Description |
|---|---|---|---|
sync.telegram.api_id |
int | - | Telethon API ID (from my.telegram.org) |
sync.telegram.api_hash |
string | - | Telethon API hash |
sync.telegram.schedule |
cron | */5 * * * * |
Fetch frequency |
sync.telegram.exclude_chats |
list[int] | [] |
Chat IDs to skip |
sync.telegram.monitored_folders |
list | [] |
Telegram folder names to filter |
Gmail-specific:
| Key | Type | Default | Description |
|---|---|---|---|
sync.gmail.accounts |
list | [] |
Gmail accounts to sync |
sync.gmail.schedule |
cron | */15 * * * * |
Fetch frequency |
sync.gmail.keyring_password |
string | - | gog keyring password |
GitHub-specific:
| Key | Type | Default | Description |
|---|---|---|---|
sync.github.schedule |
cron | */15 * * * * |
Fetch frequency |
| Key | Type | Default | Description |
|---|---|---|---|
memory.recall_model |
string | claude-sonnet-4-6 |
Model for recall routing |
memory.memorize_model |
string | claude-sonnet-4-6 |
Model for extraction & preprocessing |
memory.fast_model |
string | claude-haiku-4-5-20251001 |
Model for categorization, date resolution, knowledge filtering |
memory.embed_model |
string | (empty) | Embedding model (only used when openai_api_key is set, e.g. text-embedding-3-small) |
memory.semantic_dedup_threshold |
float | 0.85 |
Cosine similarity threshold for semantic deduplication (0 to disable) |
memory.knowledge_filter |
bool | false |
Post-extraction LLM filter that deletes generic knowledge items (extra Haiku API call per memorize) |
memory.categories |
list | [] |
Seed categories — each entry has name and description fields. Used for semantic routing when memorizing and recalling facts. nerve init populates mode-appropriate defaults (personal: relationships, finances, health, etc.; worker: patterns, procedures, approvals, etc.). |
xmemory.ai is an optional schema-backed memory layer that runs alongside memU — it never replaces it. Activated only when both xmemory.api_key and xmemory.instance_id are set (put them in config.local.yaml); otherwise it is completely inert (no SDK calls, zero overhead). The instance and its schema are created out of band on xmemory's side.
When active:
- The
memorizetool dual-writes: memU (as always) plus an asyncwrite_asyncto xmemory. Failures on the xmemory side never fail the tool. memory_recallappends xmemory's single synthesized answer (itsSINGLE_ANSWERread mode) to memU's N items, run concurrently so the dual lookup is one round-trip.- The memorization sweep (session-close / cron) stays memU-only — it does not go through the
memorizetool handler.
| Key | Type | Default | Description |
|---|---|---|---|
xmemory.api_key |
string | (empty) | xmemory bearer token (invite-only). Secret → config.local.yaml. |
xmemory.instance_id |
string | (empty) | The xmemory instance to bind. Both this and api_key are required to activate. |
xmemory.api_url |
string | https://api.xmemory.ai |
API base URL. |
xmemory.extraction_logic |
string | deep |
Write extraction mode: deep (accurate) or fast (high-volume). |
xmemory.timeout |
float | 60.0 |
Per-request timeout in seconds. |
Configuration for Docker deployment. Only relevant when deployment: docker.
| Key | Type | Default | Description |
|---|---|---|---|
docker.extra_mounts |
list[string] | [] |
Additional host:container mount pairs to add to docker-compose.yml. Example: ["~/code:/code", "~/projects:/projects"] |
The core Docker mounts (source code, ~/.nerve, workspace) are always included. GitHub CLI (~/.config/gh) and Gmail CLI (~/.config/gog) auth directories are mounted automatically if they exist on the host.
External MCP servers can be added via config without code changes or restarts. The agent picks up new servers on the next session creation, or immediately via the "Reload" button in the UI / mcp_reload tool.
Config uses a dict format so _deep_merge correctly overlays secrets from config.local.yaml:
# config.yaml — server definitions
mcp_servers:
filesystem:
type: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
remote-api:
type: http
url: https://mcp.example.com/v1# config.local.yaml — secrets merge on top
mcp_servers:
remote-api:
headers:
Authorization: "Bearer sk-secret-token"| Key | Type | Default | Description |
|---|---|---|---|
mcp_servers.<name>.type |
string | stdio |
Transport: stdio, sse, or http |
mcp_servers.<name>.enabled |
bool | true |
Enable/disable this server |
mcp_servers.<name>.command |
string | - | Command to run (stdio only) |
mcp_servers.<name>.args |
list | [] |
Command arguments (stdio only) |
mcp_servers.<name>.env |
dict | {} |
Environment variables (stdio only) |
mcp_servers.<name>.url |
string | - | Server URL (sse/http only) |
mcp_servers.<name>.headers |
dict | {} |
HTTP headers (sse/http only) |
The built-in nerve server (SDK type, in-process) is always present and cannot be overridden.
Nerve automatically discovers MCP servers from Claude Code's enabled plugins. Any plugin enabled in ~/.claude/settings.json is loaded via the SDK's --plugin-dir flag, so the CLI handles OAuth, credentials, and plugin lifecycle natively.
- No config needed — just enable a plugin in Claude Code and restart Nerve.
- OAuth works — the CLI uses cached tokens from
~/.claude/.credentials.json. - Auto-registered in UI — plugin MCP servers appear in the MCP Servers page on first tool invocation (type:
plugin). - No conflicts — Nerve-configured MCPs (from
config.yaml) and Claude Code plugin MCPs coexist; they use separate mechanisms (--mcp-configvs--plugin-dir).
| Key | Type | Default | Description |
|---|---|---|---|
auth.password_hash |
string | - | bcrypt hash for login |
auth.jwt_secret |
string | - | JWT signing secret |
| Key | Type | Description |
|---|---|---|
anthropic_api_key |
string | Anthropic API key (agent + memU chat). Not required when proxy is enabled. |
openai_api_key |
string | OpenAI API key (optional — enables vector-based memory search via embeddings; without it, LLM-based recall is used) |
brave_search_api_key |
string | Brave Search API key (optional) |
Optional local proxy that routes Anthropic API calls through Claude Code's OAuth authentication instead of a direct API key. When enabled, the API key is not required — all API calls go through the proxy at localhost.
| Key | Type | Default | Description |
|---|---|---|---|
proxy.enabled |
bool | false |
Enable CLIProxyAPI proxy |
proxy.port |
int | 8317 |
Proxy listen port |
proxy.host |
string | 127.0.0.1 |
Proxy bind address |
proxy.binary_path |
path | ~/.nerve/bin/cli-proxy-api |
Path to CLIProxyAPI binary (auto-downloaded if missing) |
proxy.auth_dir |
path | ~/.nerve/cli-proxy-auth |
Directory for OAuth token storage |
proxy.api_key |
string | sk-nerve-local-proxy |
Local auth key between Nerve and the proxy |
proxy.log_file |
path | ~/.nerve/proxy.log |
Proxy log file |
Setup:
# During nerve init, choose "Claude Code proxy" at the API configuration step.
# Or enable manually:# config.yaml
proxy:
enabled: true
port: 8317# Authenticate with Claude (one-time):
~/.nerve/bin/cli-proxy-api --claude-login --no-browser \
--config ~/.nerve/cli-proxy-config.yamlThe proxy binary is automatically downloaded from CLIProxyAPI on first start if not present. OAuth tokens are refreshed automatically.
| Key | Type | Default | Description |
|---|---|---|---|
sessions.archive_after_days |
int | 30 |
Auto-archive idle/stopped sessions older than this |
sessions.max_sessions |
int | 500 |
Max active (non-archived) sessions before cleanup |
sessions.cron_session_mode |
string | per_run |
per_run (unique session per cron run) or reuse (shared session per job) |
Opt-in nerve.db maintenance. Disabled by default. When enabled, a background
pass every interval_hours drops the verbose blocks/thinking JSON of old,
already-memorized messages (keeping the rendered content), prunes append-only
telemetry and file snapshots older than retention_days, and checkpoints the
WAL. This frees space inside the database but does not shrink the file on disk;
run nerve db vacuum once (with the daemon stopped) to reclaim it.
| Key | Type | Default | Description |
|---|---|---|---|
retention.enabled |
bool | false |
Master switch for the background retention pass |
retention.retention_full_days |
int | 30 |
Compact blocks/thinking of memorized messages older than this |
retention.retention_days |
int | 90 |
Prune telemetry and file snapshots older than this |
retention.interval_hours |
int | 24 |
How often the background pass runs |
Manual commands (run regardless of enabled):
nerve db prune [--dry-run]runs one pass immediately.--dry-runreports what would change without mutating.nerve db vacuumrewrites the file to reclaim freed pages. It takes a write lock, so stop the daemon first.
| Key | Type | Default | Description |
|---|---|---|---|
cron.system_file |
path | ~/.nerve/cron/system.yaml |
System cron jobs (managed by nerve init) |
cron.jobs_file |
path | ~/.nerve/cron/jobs.yaml |
User-defined custom cron jobs |