Configuration¶
Three places, each for one kind of value. Nothing personal is ever compiled into the engine.
| File | Holds | Committed? |
|---|---|---|
<vault>/.env |
Keys and per-machine settings on your laptop | No (gitignored) |
/etc/secondbrain/env |
The same, on a server (owned root:<app user>, mode 640) |
No |
<vault>/secondbrain.json |
Structured, non-secret settings for modules | Yes, to your private vault |
The app reads <vault>/.env at start (app/server/src/config.ts), validates every value, and refuses to start with a clear message if one is invalid. A value already in the process environment wins over the file.
Core settings¶
| Variable | Default | Purpose |
|---|---|---|
VAULT_ROOT |
The folder that holds .secondbrain/ |
Which vault to index |
DATABASE_URL |
postgres://localhost:5432/secondbrain |
The index database |
PORT |
5123 |
HTTP port (always bound to 127.0.0.1) |
LOG_LEVEL |
info |
debug, info, warn, error |
DEPLOYMENT |
local |
local (no login, loopback) or server (login required). See deployment.md |
SUMMARIES, SUMMARIES_M1_URL |
unset | SUMMARIES=on makes the deploy script install the summaries timer. SUMMARIES_M1_URL is the home server's Ollama address that the default chain tries first. See summaries.md |
B2_KEY_ID, B2_APP_KEY, B2_BUCKET, B2_S3_ENDPOINT |
unset | uploads: the bucket and its S3 endpoint. Presigned URLs are signed with this key; it never reaches the browser |
B2STORE_CRYPT_PASSWORD, B2STORE_CRYPT_PASSWORD2 |
unset | uploads: the rclone-crypt passwords when the vault part of the bucket is encrypted (the same ones b2store uses) |
UPLOADS_MAX_GB |
50 |
uploads: the largest single file the page accepts |
UPLOADS_PULL_TOKEN |
unset | uploads: bearer token (16+ characters) the second machine's puller sends |
MODULES |
empty | Optional features, comma separated: ring, agents, projects, org, board, usage, map, tv, voice, livestream, media, social, fund, chat, uploads. Unknown names are ignored |
OWNER_NAME |
owner |
Your name in the authenticator app and as a comment author |
DEV_LOG_PATH, DOCS_PATH |
unset | Vault-relative notes the nav bar links to; unset hides the link |
SCAN_INTERVAL_MINUTES |
0 |
Rescan and re-embed every N minutes; 0 means only when asked |
INDEXER |
on |
off on a machine that uses another machine's database: it never scans or embeds |
SECONDBRAIN_SETTINGS |
<vault>/secondbrain.json |
Where the structured settings live |
Search and the database page¶
| Variable | Default | Purpose |
|---|---|---|
TOGETHER_API_KEY |
unset | Embeddings for semantic search, and the database page's question-to-SQL box. Without it, semantic search falls back to Ollama or returns 503; keyword search always works |
OLLAMA_URL |
http://127.0.0.1:11434 |
Local embeddings, when the embeddings source's provider is ollama |
SQL_MODEL |
a Together-hosted model | The model that writes SQL for the database page. Queries run read-only |
Chat providers¶
Used by the chat module and by the embedder's optional contextual-enrichment step. A provider is offered in the app only when its key is set; which models each one lists comes from app/config/llm-presets.json plus the vault's chat.providers (below).
| Variable | Default | Purpose |
|---|---|---|
ANTHROPIC_API_KEY |
unset | Anthropic (Claude) |
OPENAI_API_KEY |
unset | OpenAI |
TOGETHER_API_KEY |
unset | Together AI (shared with embeddings and the database page) |
OPENROUTER_API_KEY |
unset | OpenRouter |
OLLAMA_URL |
http://127.0.0.1:11434 |
A local Ollama needs no key and is always offered |
OLLAMA_MACHINES |
unset | Connected computers that run Ollama: comma-separated name=url pairs, e.g. box1=http://192.0.2.10:11434,box2=http://192.0.2.11:11434. See below |
RERANK_URL, RERANK_API_KEY |
unset | A Cohere-compatible /rerank endpoint (Together, Cohere, Jina) for the chat's optional reranking step. Unset: the switch is off |
RERANK_MODEL |
Salesforce/Llama-Rank-V1 |
The reranker model at that endpoint |
Connected computers. Each name=url pair in OLLAMA_MACHINES adds a chat provider with id ollama-<name> and label Ollama ยท <name>. The name is [a-z0-9][a-z0-9_-]*; the url is the machine's Ollama base, with no /v1. The models are not listed in any file: the app asks the machine (GET <url>/api/tags, 2.5 s timeout) each time the settings modal opens, cached for about 20 s, and offers every model except embedding models (BERT family, or "embed" in the name). The first model is the default. A machine that does not answer stays in the list as (offline) and cannot be picked; it never slows or breaks the rest. Invalid pairs are skipped with one warning at start. The single-host ollama provider (OLLAMA_URL) is unchanged. The zen-agent harness reads the same variable the same way.
The Embeddings source's config (Sources page) takes contextualize (boolean), context_provider and context_model for the enrichment step; see rag.md.
Server and login¶
Required when DEPLOYMENT=server; the app refuses to start without them.
| Variable | Purpose |
|---|---|
PUBLIC_ORIGIN |
The browser-visible origin, for example https://brain.example.com. Every mutating request must carry it as Origin |
AUTH_PASSWORD_HASH, AUTH_TOTP_SECRET |
Printed by secondbrain auth-setup, which you run on your own machine |
B2_SERVE_URL |
Optional: a local b2store serve for big files kept only in object storage |
Deploy settings (DEPLOY_*) are read by deploy.sh only: see deployment.md.
Using another machine's database¶
| Variable | Where | Purpose |
|---|---|---|
SECONDBRAIN_DB_URL |
Vault .env |
The launcher connects to this database instead of a local one, and starts the app with INDEXER=off |
SECONDBRAIN_DB=local |
Environment | Force the local database for one run |
Module settings¶
| Variable | Module | Default | Purpose |
|---|---|---|---|
AGENT_HUB_URL, AGENT_HUB_TOKEN |
agents |
unset | The agent-hub service that serves Claude chats and the Telegram toggle. Unset: those tabs say so |
ZEN_AGENT_URL, ZEN_AGENT_TOKEN |
agents |
unset | The zen-agent serve process behind /api/zen (the zen-agent chat page). The token is the serve process's ZEN_SERVE_TOKEN. Unset: the page says so |
HERMES_HOME |
agents, archive |
~/.hermes |
Hermes's home on this machine |
ARCHIVE_SSH_HOST, REMOTE_HERMES_HOME, REMOTE_HERMES_USER |
archive | unset | Pull a server's Hermes history over ssh; unset skips the pull |
SB_LEDGER_DIR, SB_LEDGER_TOKEN |
agent ledger (on each machine, not the server) | the OS data folder, the token in ledger.json |
Where a machine's agent ledger keeps ledger.db and ledger.json, and a token that overrides the stored one. See the app README, "Agent ledger" |
PROJECTS_FEED_TOKEN |
projects |
unset | Read-only bearer token for GET /api/feed/projects (a kiosk panel). Unset: the route doesn't exist. deploy.sh generates one |
KNOWLEDGE_TOKEN |
any | unset | Read-only bearer token for an agent on the same machine (zen-agent's search_notes, search_everything, read_note). Opens GET /api/search, /api/search/global and /api/file/source and nothing else, only from loopback and never through a proxy (a request with a forwarding header, or a Host that is not 127.0.0.1, localhost or [::1], is refused). At least 32 characters; openssl rand -hex 24. Unset: the token does nothing |
ORG_OWNER |
org |
owner |
Your handle on the agent board (lowercase, a-z0-9-) |
ORG_VAULT_ASSIGNEE |
org |
vault-session |
The kanban assignee for vault work: a Claude Code session on your machine that no dispatcher spawns (lowercase, a-z0-9-). Cards on boards that still use the old slug m4 keep working |
ORG_SESSION_MAX_RUNS |
org |
3 |
Runs one remembered claude-code agent session carries before the next wake starts fresh. 1 never resumes. Not used by the zen-agent lane, whose wakes are always fresh |
BOARD_LEASE_MINUTES |
board |
60 |
How long a claimed card stays a lane's without a heartbeat before the board-reclaim sweep puts it back to ready |
BOARD_URL, BOARD_TOKEN |
board |
unset | On a machine that is not the server: cli-board and agents reach the board over HTTPS as the roster member the token names (cli-board token create <agent>). Unset: the database connection is used |
ORG_TASK_WAKE_RETRIES, ORG_TASK_WAKE_BACKOFF_MINUTES |
org |
2, 15 |
A card that keeps waking its bot waits 15, then 30 minutes between wakes, and after 2 re-wakes in 24 h is held until a person wakes it (wake-add --reason owner) |
ORG_TZ |
org |
UTC |
The local day for "one routine wakeup per day" and the dispatcher's quiet hours (an IANA zone name) |
ORG_DISPATCH |
org |
off |
on: the engine's dispatcher runs bots whose roster runtime is zen-agent (secondbrain org-dispatch; a timer on the server). off: wakeups are left to your own dispatcher |
ORG_DISPATCH_TICK_SECONDS, ORG_DISPATCH_PARALLEL |
org |
120, 2 |
How often the dispatcher looks, and how many bots run at once |
ORG_DISPATCH_QUIET |
org |
unset | Local hours with no wakes, HH-HH (e.g. 01-07) |
ORG_DISPATCH_DAILY_TOKENS |
org |
0 (off) |
Tokens all bots together may spend in one local day before the rest of the day's wakes are held |
ORG_ZEN_COMMAND |
org |
zen-agent |
The zen-agent command the dispatcher runs (on the PATH, or absolute) |
ZEN_SESSIONS_DIR |
usage |
<vault>/.zen/sessions |
Where zen-agent writes its traces; usage-sync reads them into the ledger |
CLASSIFY_PROVIDER |
org |
none |
The classify step's provider: openrouter (Jev through OpenRouter with OPENROUTER_API_KEY) or typesafe (with TYPESAFE_API_KEY). none: every decision falls through to the old path |
CLASSIFY_DAILY_TOKENS |
org |
2000000 |
Classifier input tokens per day; above it, decisions fall through |
TYPESAFE_API_KEY |
org |
unset | A direct TypeSafe key, when CLASSIFY_PROVIDER=typesafe |
RING_PULL_SSH_HOST, RING_PULL_DB |
ring |
unset, secondbrain |
Mirror a server's ring tables over ssh |
TTS_URL, TTS_MODEL |
voice |
http://127.0.0.1:8880, a Kokoro model |
A local OpenAI-compatible speech server. Never used on a server |
ROBOT_LAUNCH_SCRIPT |
none (local only) | unset | A script the "start robot" button runs. Unset: the route isn't mounted |
Vault sync¶
Per-machine settings for secondbrain-vault sync (sync.md); the shared policy lives in secondbrain.json, below.
| Variable | Default | Purpose |
|---|---|---|
SYNC_ROLE |
owner |
owner (you edit here; a conflict stops and alerts) or agent (a server; a conflict keeps both versions) |
SYNC_MACHINE |
the hostname | Name in sync commit messages and conflict copies |
SYNC_INTERVAL_MIN |
60 |
Minutes between runs, used by sync install |
SYNC_CONFLICTS |
by role | stop or both |
SYNC_ALERT |
desktop |
desktop, log, or an https:// webhook URL |
SYNC_STATE_DIR |
~/.local/state/secondbrain |
Where sync.json and sync.log are written and where the app reads the sync chip from |
Agent hooks¶
| Variable | Purpose |
|---|---|
DEV_LOG_INDEX |
The README the dev-log gate tells agents to link new entries from |
SB_GUARD_LOCAL |
An explicit path to guard.local.json |
secondbrain.json¶
Settings that don't fit on one env line. Every section is optional; a file that doesn't parse is reported and ignored, and the app uses empty defaults. vault-template/secondbrain.example.json has one of each.
| Key | Module | Holds |
|---|---|---|
goalsFile |
projects, map |
Your goals note: markdown tables of \| W1 \| Goal \| Measure \| Due \| Status \| |
missionFile |
map |
Your mission note |
map.divisionFolders |
map |
{ "division": ["folder", ...] }: which folders belong to which division when no project claims them |
map.missionFolders |
map |
Notes that sit at the centre of the map |
metrics[] |
map |
Numbers the map tracks. Each has key, label, unit (followers, subscribers, hours, usd, count), optional goal, target, division, headline, and either collect (type youtube, x, instagram or base-balance) or sum (a list of other metric keys) |
tv.episodesDir |
tv |
Where episodes live in the vault (default tv/episodes) |
tv.shows |
tv |
{ "slug": "Show name" } |
livestream.youtube, .twitch, .kick |
livestream |
Your channel handle on each platform |
fund.dir, fund.txService, fund.explorer |
fund |
Vault folder of the fund's state (default fund), the Safe Transaction Service URL and the block explorer for its chain (defaults: Base) |
projects.reportFile |
projects |
Where cli-projects render writes the markdown report (default projects.md) |
summaries.chain, summaries.minSteps, summaries.quietMinutes, summaries.maxInputChars |
usage |
Which providers write session summaries and in what order, the shortest session worth summarizing, how long a session must be quiet, and how much transcript is sent. Defaults in app/config/summaries.json; see summaries.md |
chat.defaultProvider |
chat |
The preset provider the chat starts on, when its key is set |
chat.systemPrompt |
chat |
Replaces the engine's base system prompt (still editable in the app's settings modal) |
chat.providers.<id> |
chat |
Per preset provider: models to add ([{ "id", "label" }]), another default model, another baseUrl |
sync.remote, sync.branch |
vault sync | What every machine syncs with (default origin, main) |
sync.agentOwned |
vault sync | Files only agent-role machines commit, e.g. a rendered report |
sync.embeddedRepos, sync.maxFileMB, sync.settleSeconds, sync.heartbeat |
vault sync | ignore or record nested repos' pointers; largest file to commit (20); recent-write wait (90); write .sync/<machine>.json with each commit (off). See sync.md |
classify.allow |
org |
Channel slugs and vault folder prefixes whose text may be sent to the classifier. Empty (the default): every decision falls through and nothing leaves the machine |
classify.model |
org |
provider/model for decisions whose schema pins none (default openrouter/typesafe/jev-1.14) |
classify.schemasDir |
org |
Vault folder of decision schemas, <decision>.json (default .agents/classify); the engine's defaults fill in what is missing |
dispatch.tiers |
org |
{ "flash-low" \| "flash-high" \| "mid" \| "strong": { "model", "thinking" } }: what the card-tier decision routes to. Unset tiers use zen-agent's own model |
dispatch.budgets |
org |
Per wake reason (dm, mention, handoff, task, routine, owner): { "turns", "tokens", "minutes" } |
dispatch.tools |
org |
Per wake reason: zen-agent tool sets (fs, shell, skills, knowledge, kanban, org, subagent) |
Example:
{
"goalsFile": "Areas/goals.md",
"map": { "divisionFolders": { "writing": ["Areas/blog"] } },
"tv": { "shows": { "weekly": "The weekly report" } },
"livestream": { "youtube": "example" }
}