Configuration Reference -- cortex

July 30, 2026 · View on GitHub

Complete configuration reference. cortex uses compiled defaults, optional TOML config, the shared setup env file, and process environment overrides.

Configuration precedence

Precedence (highest to lowest):

  1. Environment variables (always win)
  2. ~/.cortex/.env (or $CORTEX_HOME/.env) when present
  3. config.toml in the working directory (partial configs supported -- missing fields keep defaults)
  4. Compiled defaults in src/config.rs

The setup env file is created and repaired by cortex setup. It is loaded automatically by installed CLI commands so cortex stats, cortex mcp, and cortex serve mcp see the same database path and runtime settings as the Docker Compose container. Explicit process environment variables still win.

config.toml

The TOML config file at the repo root is used for local development. It is not copied into the Docker image -- container deployments use defaults + env vars exclusively.

[syslog]
host = "0.0.0.0"
port = 1514
max_message_size = 8192

[storage]
db_path = "/data/cortex.db"
pool_size = 8
sqlite_page_cache_mb = 128
sqlite_mmap_mb = 256
heavy_read_concurrency = 1
wal_checkpoint_mb = 256
retention_days = 90
wal_mode = true
max_db_size_mb = 1024
recovery_db_size_mb = 900
min_free_disk_mb = 0      # 0 = free-disk guard disabled (default)
recovery_free_disk_mb = 0
cleanup_interval_secs = 60

[mcp]
host = "127.0.0.1"
port = 3100
server_name = "cortex"
allowed_hosts = ["cortex.example.com", "cortex.example.com:443"]
allowed_origins = ["https://cortex.example.com"]

[api]
# Always-on REST API token — required at server startup.
# api_token = "your-api-token"

[docker_ingest]
enabled = false
reconnect_initial_ms = 1000
reconnect_max_ms = 30000

[[docker_ingest.hosts]]
name = "edge-host-a"
base_url = "http://edge-host-a:2375"
allow_insecure_http = true

[llm]
enabled = true
max_concurrent = 1
max_per_action_concurrent = 1
max_invocations_per_minute = 3
max_invocations_per_hour = 30
failure_threshold = 3
cooldown_secs = 300
timeout_secs = 120
max_prompt_bytes = 1048576
max_output_bytes = 262144
background_enrichment_enabled = false

[llm.actions.ai_assess]
enabled = true

Bind host fields (CORTEX_RECEIVER_HOST and CORTEX_HOST) must be hostnames or IP addresses without : because their ports are configured separately. allowed_hosts / CORTEX_ALLOWED_HOSTS are RMCP Host-header allow-list entries and may include host:port values such as cortex.example.com:443. allowed_origins / CORTEX_ALLOWED_ORIGINS remain full browser origin URLs such as https://cortex.example.com.

Environment variables

Syslog listener (CORTEX_*)

VariableRequiredDefaultSensitiveDescription
CORTEX_RECEIVER_HOSTno0.0.0.0noListen host for UDP+TCP syslog (no port -- use separate setting)
CORTEX_RECEIVER_PORTno1514noListen port shared by UDP and TCP syslog listeners
CORTEX_MAX_MESSAGE_SIZEno8192noMax bytes per UDP datagram or newline-delimited TCP frame. Oversized frames are dropped.
CORTEX_MAX_TCP_CONNECTIONSno512noMaximum simultaneous TCP syslog connections
CORTEX_TCP_IDLE_TIMEOUT_SECSno300noIdle timeout per TCP read before closing inactive connections
CORTEX_BATCH_SIZEno100noEntries per batch flush to SQLite
CORTEX_FLUSH_INTERVALno500noBatch flush interval in milliseconds
CORTEX_WRITE_CHANNEL_CAPACITYno10000noInternal parsed-message queue capacity

TCP forwarders can keep a connection open and send multiple newline-delimited syslog frames. The size limit applies to each frame, not to the full TCP connection lifetime. An oversized newline-delimited frame is dropped and later bounded frames on the same connection can still be ingested. An oversized unterminated frame is dropped and the connection is closed because the listener cannot safely find the next frame boundary.

Docker log ingest (CORTEX_DOCKER_*)

Current deployments use the host-local cortex agent: each deployed agent reads Docker logs from unix:///var/run/docker.sock on its own host and forwards normalized rows into cortex. That is the recommended path because it does not expose a Docker API endpoint on the network.

CORTEX_DOCKER_* is the legacy central pull compatibility mode. It lets the cortex server pull stdout/stderr logs from explicit remote Docker Engine HTTP endpoints. Older deployments commonly placed docker-socket-proxy in front of those endpoints, but that is no longer the default homelab path.

Set CORTEX_DOCKER_HOSTS to a comma-separated list of hostnames. Each hostname becomes http://<host>:2375 with insecure HTTP allowed — use only on trusted private networks.

CORTEX_DOCKER_HOSTS=squirts,tootie,dookie

CORTEX_DOCKER_HOSTS_FILE (path to a legacy [[hosts]] TOML file) is still accepted as a fallback when CORTEX_DOCKER_HOSTS is not set.

VariableRequiredDefaultSensitiveDescription
CORTEX_DOCKER_INGEST_ENABLEDnofalsenoEnable legacy central pull Docker log ingestion
CORTEX_DOCKER_HOSTSyes, if Docker ingest is enabled(none)noComma-separated hostnames — each becomes http://<host>:2375
CORTEX_DOCKER_RECONNECT_INITIAL_MSno1000noInitial reconnect delay after host stream failure
CORTEX_DOCKER_RECONNECT_MAX_MSno30000noMaximum reconnect delay after repeated failures

If the legacy central pull endpoint is a docker-socket-proxy, minimum recommended permissions are:

CONTAINERS=1
EVENTS=1
PING=1
VERSION=1
POST=0

CONTAINERS=1 exposes the broader read-only Docker container API to every client that can reach that endpoint. Bind it on a trusted private network, firewall it so only cortex can connect, or put it behind authenticated TLS. Hosts using plain http:// must set allow_insecure_http = true in the hosts file; otherwise config validation rejects them.

Docker log ingest is tested by path: host-local agent parity tests cover deployed-agent Docker streaming, while the legacy central pull path has a mocked Docker HTTP fixture. For a full legacy integration run, set CORTEX_DOCKER_INGEST_ENABLED=true against a disposable Docker-compatible HTTP fixture, emit a unique container stdout/stderr line, and verify it with search or tail. Container stream rows identify their source as docker://<host>/<container>/<stream>. Container lifecycle events such as create, start, restart, die, stop, destroy, rename, and oom identify their source as docker-event://<host>/<container>/<action> and use facility=docker.

Managed File-Tail Sources

Cortex can tail local log files directly and ingest appended lines through the same writer/enrichment path as syslog, Docker, and OTLP. Sources are stored in <data-dir>/file-tails.json, where <data-dir> is the parent directory of CORTEX_DB_PATH.

Use this for logs that do not naturally reach journald or container stdout, such as SWAG nginx access/error logs, SWAG fail2ban logs, Authelia file logs, and AdGuard query logs.

In Docker, mount a host log tree read-only at /file-tail-root:

CORTEX_FILE_TAIL_LOG_VOLUME=/mnt/user/appdata

Registered paths must be absolute, existing, non-symlink regular files under CORTEX_FILE_TAIL_ALLOWED_ROOTS. The documented safe default is the dedicated /file-tail-root mount. To opt into broader roots, mount those directories read-only and set an explicit comma-separated allowlist, for example CORTEX_FILE_TAIL_ALLOWED_ROOTS=/file-tail-root,/var/log,/logs. Sensitive Cortex mounts such as /data, /cortex-home, /home/cortex/.ssh, and /home/cortex/workspace are always rejected. REST management requires both the normal API bearer and X-Cortex-Admin-Token: $CORTEX_API_ADMIN_TOKEN; MCP management requires cortex:admin.

cortex ingest filetail add /file-tail-root/swag/log/nginx/access.log \
  --facility local4

cortex ingest filetail add /file-tail-root/swag/log/nginx/error.log \
  --id swag-error \
  --facility local4 \
  --severity warning

cortex ingest filetail add /file-tail-root/swag/log/fail2ban/fail2ban.log \
  --facility local5

cortex ingest filetail add /file-tail-root/authelia/logs/authelia.log \
  --facility local5

cortex ingest filetail add /file-tail-root/adguard/var/data/querylog.json \
  --id adguard-query \
  --facility local6

The source id and tag derive from the lowercased file name. With no --host, the source gets the stable synthetic owner file-tail-<id>; Cortex never uses the server or container hostname for mounted files. Pass --host when the path belongs to a real host that should participate in host filters and correlation. --from-start ingests existing file contents on first open. The default starts at EOF so adding a source does not backfill a large historic log unexpectedly. After a source is running, Cortex checkpoints dev/inode/offset in file-tails.json, resumes from that cursor on restart, reopens on rename/create rotation, seeks back to 0 after truncation, and bounds each line by CORTEX_MAX_MESSAGE_SIZE. Per-row metadata stores file_tail_id, tag, and path_basename; full paths are visible only through the admin management surface. The runtime reconciles enabled sources periodically and after CLI/REST/MCP mutations.

MCP server (CORTEX_*)

VariableRequiredDefaultSensitiveDescription
CORTEX_HOSTno127.0.0.1noHTTP listen host for MCP endpoint (loopback by default; non-loopback binds require CORTEX_TOKEN, OAuth, or the trusted-gateway pair)
CORTEX_PORTno3100noHTTP listen port for MCP endpoint
CORTEX_TOKENno(none)yesBearer token for /mcp auth. Generate: openssl rand -hex 32. When unset, auth is disabled.
CORTEX_ALLOWED_HOSTSno(none)noExtra comma-separated Host header values for RMCP Host validation
CORTEX_ALLOWED_ORIGINSno(none)noExtra comma-separated browser origins for RMCP Origin validation

Non-MCP API (CORTEX_API_*)

The plain JSON API is always on: it is mounted under /api/* on the same HTTP listener and requires its own bearer token. The server fails to start (on the serve mcp path) when no token is configured; cortex setup repair generates one if missing.

VariableRequiredDefaultSensitiveDescription
CORTEX_API_TOKENyes(none)yesBearer token for /api/* routes — required at startup
CORTEX_API_ADMIN_TOKENfor REST admin mutations(none)yesExtra token sent as X-Cortex-Admin-Token for admin REST mutations, including /api/file-tails, /api/sessions/prune-checkpoints, /api/db/integrity/background, /api/db/checkpoint, /api/db/vacuum, and /api/db/backup. The normal API bearer is still required.

Headless Gemini assessment (CORTEX_HEADLESS_*, CORTEX_LLM_*)

cortex sessions assess is local-only and starts the Gemini CLI in an isolated temporary HOME. It copies Gemini auth files from the configured source HOME, installs the bundled frustration-assessment skill into that isolated HOME, disables MCP servers/hooks/context-file loading, and parses Gemini's stream-json output so only assistant text is emitted.

VariableRequiredDefaultSensitiveDescription
CORTEX_HEADLESS_GEMINI_CMDnogemininoGemini CLI executable path or command name
CORTEX_HEADLESS_GEMINI_MODELnogemini-3.1-flash-lite-previewnoDefault model for cortex sessions assess; --model on the CLI overrides this
CORTEX_HEADLESS_GEMINI_HOMEno$HOMEmaybeSource home containing .gemini auth files to copy into the isolated runtime HOME
CORTEX_LLM_COMPLETION_TIMEOUT_SECSnonoDeprecated, no longer takes effect. Previously an independent timeout for the Gemini assessment subprocess; now superseded end-to-end by [llm].timeout_secs. Setting this var logs a tracing::warn! deprecation notice at call time but has no effect.

LLM invocation guard (CORTEX_LLM_*, [llm])

Shared by every LLM-backed assessment feature (ai_assess today). LlmRunner (src/app/llm_runner.rs) enforces global/per-action concurrency limits, per-action rate limits, a consecutive-failure circuit breaker, a per-invocation timeout, prompt/output byte caps, a global + per-action kill switch, and writes every invocation attempt — including denials — to the llm_invocations audit table (see the llm_invocations MCP action / GET /api/sessions/llm-invocations / cortex sessions llminvocations).

Behavior change: prior to this release, cortex sessions assess had no concurrency guard — multiple overlapping invocations ran in parallel. As of this release it routes through LlmRunner, whose defaults (max_concurrent=1, max_per_action_concurrent=1) mean a second concurrent assess call is now REJECTED with a concurrency-limited error instead of running alongside the first. Raise [llm].max_concurrent / [llm].max_per_action_concurrent if your workflow depends on concurrent assessments.

VariableRequiredDefaultSensitiveDescription
CORTEX_LLM_ENABLEDnotruenoGlobal kill switch — false denies every LLM invocation immediately (still audited with status disabled)

Fields without a dedicated env override (max_concurrent, max_per_action_concurrent, max_invocations_per_minute, max_invocations_per_hour, failure_threshold, cooldown_secs, timeout_secs, max_prompt_bytes, max_output_bytes, background_enrichment_enabled, [llm.actions.<name>].enabled) are set via config.toml's [llm] section — see the sample above. Defaults match the sample TOML block; per-action tables (e.g. [llm.actions.ai_assess]) default to enabled = true when omitted, except background_enrich, which is gated separately by background_enrichment_enabled (default false) regardless of its own table.

Storage (CORTEX_*)

VariableRequiredDefaultSensitiveDescription
CORTEX_DB_PATHno/data/cortex.dbnoPath to SQLite database file
CORTEX_POOL_SIZEno8noSQLite connection pool size (must be > 0); reads get pool_size - 1 permits
CORTEX_SQLITE_PAGE_CACHE_MBno128noTotal SQLite page-cache budget across the pool; divided by pool_size before PRAGMA cache_size
CORTEX_SQLITE_MMAP_MBno256noBounded SQLite mmap size; resident mapped pages may still count toward cgroup memory
CORTEX_HEAVY_READ_CONCURRENCYno1noShared service-layer limiter for SQLite-heavy reads
CORTEX_WAL_CHECKPOINT_MBno256noWAL size threshold for bounded PASSIVE checkpoint attempts
CORTEX_RETENTION_DAYSno90noDays to retain logs and llm_invocations audit rows before automatic hourly purge (0 = keep forever for both)

Storage budget (CORTEX_*)

VariableRequiredDefaultSensitiveDescription
CORTEX_MAX_DB_SIZE_MBno1024noSoft limit for logical DB size in MB; breach deletes oldest logs (0 = disable)
CORTEX_RECOVERY_DB_SIZE_MBno900noCleanup target after DB-size breach (must be < max)
CORTEX_MIN_FREE_DISK_MBno0noMinimum free disk space in MB — disabled by default. A breach blocks writes; it does not delete data.
CORTEX_RECOVERY_FREE_DISK_MBno0noHysteresis target before writes resume after a free-disk breach (must be > min when enabled)
CORTEX_CLEANUP_INTERVAL_SECSno60noStorage budget enforcement interval in seconds (minimum 5)
CORTEX_CLEANUP_CHUNK_SIZEno2000noRows deleted per chunk during enforcement (1 to 1,000,000)
CORTEX_ERR_FLOOR_WINDOW_HOURSno24noerr+ rows received within this window are protected from disk-pressure deletion (0 = disable floor)
CORTEX_ERR_FLOOR_PER_SOURCE_CAPno10000noMax protected err+ rows per source IP within the floor window (0 = disable floor)

Logging

VariableRequiredDefaultSensitiveDescription
RUST_LOGnoinfonoRust tracing filter directive. Examples: debug, cortex=debug,tower_http=info, trace

Docker / container

VariableRequiredDefaultSensitiveDescription
CORTEX_UIDno1000noContainer user ID
CORTEX_GIDno1000noContainer group ID
CORTEX_FILE_TAIL_GROUPnoadmnoExtra container group added by Compose so the non-root cortex user can read group-owned log files mounted at /file-tail-root; set a numeric gid if your host log-reader group differs
CORTEX_RECEIVER_PORTno1514noHost-side syslog port mapping
CORTEX_PORTno3100noHost-side MCP port mapping
CORTEX_MCP_BINDno127.0.0.1noHost interface the MCP port is published on (loopback by default; set 0.0.0.0 only with CORTEX_TOKEN)
CORTEX_DATA_VOLUMEnocortex-datanoNamed Docker volume for /data
CORTEX_HOME_VOLUMEno~/.cortexnoShared cortex home (inventory cache, setup env) mounted at /cortex-home
CORTEX_SSH_VOLUMEno~/.cortex/sshnoDedicated SSH key dir mounted read-only at /home/cortex/.ssh for inventory collection. Never point at ~/.ssh — see docs/SECURITY.md
CORTEX_VERSIONnocurrent releasenoImage tag pulled by docker-compose.prod.yml (kept in version canon)
DOCKER_NETWORKnocortexnoExternal Docker network name

Storage budget behavior

The two guards behave differently:

  1. DB-size guard (enabled by default): when logical DB size exceeds max_db_size_mb, the oldest logs are deleted in chunks until size drops below recovery_db_size_mb. err+ rows inside the floor (err_floor_window_hours × err_floor_per_source_cap) are excluded from the deletable set.
  2. Free-disk guard (disabled by default): low free disk is an external condition cortex cannot fix by deleting its own data, so a breach blocks new writes instead of self-trimming. Writes resume once free disk rises above recovery_free_disk_mb (hysteresis prevents oscillation).
  3. Write blocking: if DB-size cleanup cannot recover enough space (e.g. no more deletable logs), the batch writer also blocks new writes until storage becomes healthy.
  4. Enforcement interval: checked every cleanup_interval_secs seconds (default 60). The age-based retention purge is separate and runs hourly.

Set both max_db_size_mb and min_free_disk_mb to 0 (with their recovery targets at 0) to disable all storage enforcement.

SQLite migration upgrades

Startup creates missing schema objects automatically. Small migrations are expected to complete quickly, but heavyweight migrations on a populated database can hold SQLite's write lock before syslog listeners and /health are ready. The server logs an operator-visible Migration N: starting ... message before such work and a completion message with elapsed time.

One-time auto_vacuum conversion VACUUM: the first startup against a database that is not yet auto_vacuum=INCREMENTAL runs a full VACUUM to convert it. This is logged loudly at startup and can take minutes on large databases — treat it like a heavy migration (backup first, expect /health and the listeners to be unavailable until it completes). It runs once; subsequent startups skip it.

For populated databases, treat heavy migrations as a planned upgrade step:

  1. Stop or quiet high-volume senders if packet loss is unacceptable.
  2. Take a WAL-safe backup with scripts/backup.sh or sqlite3 /data/cortex.db ".backup /data/syslog-pre-upgrade.db".
  3. Start the upgraded container or binary and watch docker compose logs -f cortex or the relevant service log for migration start/completion lines.
  4. Wait for curl -sf http://localhost:3100/health to succeed.
  5. Run cortex stats --json or mcporter call ... action=stats and confirm total_logs, storage metrics, and write_blocked match expectations.

If a migration must be abandoned, stop the new process before changing files, restore the WAL-safe backup, and restart the previous image or binary. See RELEASE.md for the current deploy gate checklist.

Validation rules

  • CORTEX_POOL_SIZE must be > 0
  • CORTEX_SQLITE_PAGE_CACHE_MB must be > 0 and its derived KiB-per-connection value must fit SQLite's signed PRAGMA range
  • CORTEX_SQLITE_MMAP_MB derived bytes must fit SQLite's signed PRAGMA range
  • CORTEX_HEAVY_READ_CONCURRENCY must be > 0
  • CORTEX_WAL_CHECKPOINT_MB must be > 0
  • recovery_db_size_mb must be > 0 and < max_db_size_mb when DB size guard is enabled
  • recovery_free_disk_mb must be > 0 and > min_free_disk_mb when free-disk guard is enabled
  • cleanup_interval_secs must be >= 5
  • cleanup_chunk_size must be between 1 and 1,000,000
  • err_floor_per_source_cap must be > 0 when err_floor_window_hours is set
  • CORTEX_API_TOKEN is required for the server to start (/api/* is always mounted)
  • Bind host fields (CORTEX_RECEIVER_HOST, CORTEX_HOST) must not contain a colon (port is a separate setting)
  • CORTEX_ALLOWED_HOSTS values may include host:port to match reverse-proxy Host headers
  • CORTEX_DOCKER_HOSTS must contain at least one hostname when Docker ingest is enabled
  • Docker ingest host names must be unique

Plugin deployment

cortex serve mcp runs as a daemon (syslog listener + HTTP MCP server), so the plugin connects via HTTP -- not stdio.

When installed as a Claude Code plugin, users are prompted for:

FieldSensitiveDescription
server_urlnoBase server URL (e.g. https://cortex.example.com)
api_tokenyesBearer token used by the plugin MCP client; enforced by the server unless no_auth=true
no_authnoExplicit no-auth mode; non-loopback server binds also require CORTEX_TRUSTED_GATEWAY_NO_AUTH=true
is_servernoWhether this host owns the Docker Compose deployment

These values are interpolated into plugins/cortex/mcp.json via ${user_config.*} syntax. See plugin/CONFIG.md for details.

.env.example conventions

  • Group variables by section with comment headers
  • Required variables first within each group
  • No actual secrets -- use descriptive placeholders
  • See .env.example at the repo root for the full template