All configuration lives in a single TOML file. Default location: ~/.denkeeper/denkeeper.toml.

Top-level keys

KeyTypeDefaultDescription
data_dirstring"~/.denkeeper"Base directory all default paths derive from. Overridden by DENKEEPER_DATA_DIR
max_toolsint50Combined ceiling on tools + plugins

[telegram]

KeyTypeDefaultDescription
tokenstringrequiredBot token from @BotFather
allowed_usersint[]requiredTelegram user IDs allowed to interact

[discord]

KeyTypeDefaultDescription
tokenstringrequiredDiscord bot token
allowed_usersstring[]requiredDiscord user snowflake IDs

[llm]

KeyTypeDefaultDescription
default_providerstring"openrouter"Name of the provider instance to use by default (must match a configured instance name)
default_modelstring"anthropic/claude-sonnet-4-20250514"Model identifier (format depends on provider)
cost_limit_softfloat0Soft cost limit per session in USD (warns but continues)
cost_limit_hardfloat1.0Hard cost limit per session in USD (stops generation)

[[llm.providers]]

Named provider instances. Multiple entries of the same type are allowed, enabling e.g. OpenAI + a local LM Studio endpoint simultaneously. Each instance is addressable by its unique name.

KeyTypeDescription
namestringUnique instance name (used in default_provider and per-agent llm_provider)
typestringProvider type: "anthropic", "openai", "openrouter", or "ollama"
api_keystringAPI key (required for all types except ollama)
base_urlstringAPI endpoint override (useful for Azure, vLLM, LM Studio, etc.)
organizationstringOpenAI organization ID (openai type only)
[[llm.providers]]
name = "openai"
type = "openai"
api_key = "sk-..."

[[llm.providers]]
name = "lmstudio"
type = "openai"
base_url = "http://localhost:1234/v1"
api_key = "lm-studio"

Legacy single-slot syntax ([llm.openai], [llm.anthropic], etc.) is still supported and auto-converted at startup. The two styles can coexist; an explicit [[llm.providers]] entry with the same name takes precedence.

[llm.openrouter] (legacy)

KeyTypeDefaultDescription
api_keystringrequiredOpenRouter API key

[llm.anthropic] (legacy)

KeyTypeDefaultDescription
api_keystringrequiredAnthropic API key (sk-ant-...)
base_urlstring"https://api.anthropic.com"API endpoint override

[llm.ollama] (legacy)

KeyTypeDefaultDescription
base_urlstring"http://localhost:11434"Ollama server URL

[llm.openai] (legacy)

KeyTypeDefaultDescription
api_keystringrequiredOpenAI API key
base_urlstring"https://api.openai.com/v1"API endpoint override (for Azure OpenAI, vLLM, LiteLLM, etc.)
organizationstringOpenAI organization ID (optional)

Compatible with any endpoint that speaks the OpenAI Chat Completions API format.

[[llm.fallback]]

KeyTypeDescription
triggerstring"cost_limit", "rate_limit", or "error"
actionstring"switch_provider", "switch_model", or "wait_and_retry"
providerstringTarget provider (for switch_provider)
modelstringTarget model (for switch_model)
scopestring"soft" or "hard" (for cost_limit) — which agent cost limit triggers the swap
max_retriesintMax retry count (for wait_and_retry)
backoffstring"exponential" (default) or "constant"

cost_limit rules consume the agent’s cost_limit_soft / cost_limit_hard (resolved via [[agents]] overrides or the global [llm] defaults). Legacy low_funds rules with a threshold field auto-migrate to cost_limit + scope = "soft" on load.

[session]

KeyTypeDefaultDescription
tierstring"supervised"Default permission tier: "autonomous", "supervised", "restricted"
approval_timeoutstring"5m"How long to wait for operator approval before timing out. Go duration string
approval_retriesint0Times to re-submit a timed-out approval before reporting failure to the LLM

Note that approval_timeout is the engine’s wait, distinct from the 24-hour TTL after which an unresolved approval request is expired and cleaned up.

[agent]

Defaults for agents that do not set their own directories.

KeyTypeDefaultDescription
persona_dirstring<data_dir>/agents/<name>Base persona directory
skills_dirstring<data_dir>/skillsGlobal skills directory

[[agents]]

KeyTypeDefaultDescription
namestringrequiredUnique agent name (one must be "default")
descriptionstringAgent description
persona_dirstringPath to persona files
skills_dirstringOverride the global skills directory. Agent-specific skills in <persona_dir>/skills/ are always loaded regardless, and override global skills by name
adaptersstring[]Adapter bindings (e.g., ["telegram"], ["telegram:12345"])
llm_providerstringOverride default provider (must match a configured provider instance name)
llm_modelstringOverride default model
session_tierstringOverride default permission tier
timezonestringIANA name, e.g. "Australia/Sydney". Precedence: agent > api.timezone > UTC. Does not affect cron evaluation, which stays on api.timezone and is restart-only
cost_limit_softfloatPer-agent soft cost limit in USD (overrides global)
cost_limit_hardfloatPer-agent hard cost limit in USD (overrides global)
max_context_messagesint50Most recent messages sent to the LLM. Older history is dropped from the request, not deleted
max_tool_roundsint50Tool-call rounds per turn, not calls — one round may fan out to many parallel calls
browser_url_allowliststring[]Overrides the global browser allowlist for this agent. Supports *.example.com
auto_approve_toolsstring[]Tool names auto-approved for this agent without human sign-off (config scope). Declared in TOML only — cannot be created or removed at runtime; re-applied wholesale on config reload. Names not matching an advertised tool are warned about and kept
[[agents.fallback]]table arrayPer-agent fallback rules. When non-empty these replace the global [[llm.fallback]] rules rather than merging with them

Supervisor

KeyTypeDefaultDescription
supervisorstringName of another agent that auto-reviews tool calls before they reach you (supervised tier only; supervisor must be autonomous or restricted, not itself supervised)
supervisor_timeoutstring"30s"Max wait for the supervisor’s LLM review. Go duration format (30s, 1m, 90s). On timeout, falls through to human approval
supervisor_context_messagesint5Number of recent conversation messages passed to the supervisor as context
supervisor_body_excerpt_lenint500Max characters of skill body included in the review prompt
supervisor_tool_desc_lenint200Max characters of tool description included in the review prompt

Post-turn reviewer

A headless per-agent engine that reviews after a turn and can append persona memory or report skill improvements. Distinct from the supervisor above — it reviews after the fact rather than gating tool calls, and is capability-reduced rather than approval-gated.

KeyTypeDefaultDescription
reviewer_modelstringModel used for post-turn review. Empty disables post-turn review for this agent
reviewer_providerstringProvider for the reviewer; inherits the agent’s llm_provider when empty
review_max_iterationsint6Tool-call rounds allowed to the reviewer
review_timeoutstring"2m"Max duration of a review pass
nudge_memory_intervalint0User turns between memory-review nudges. 0 = disabled
nudge_skill_intervalint0Tool-call rounds between skill-review nudges. 0 = disabled

[[channels]]

Named routing endpoints that decouple sessions from a 1:1 agent–adapter binding. A channel points at one agent and may bind several adapters, which lets one conversation be shared across them. When no [[channels]] are declared, Denkeeper synthesizes them from each agent’s adapters list.

KeyTypeDefaultDescription
namestringrequiredUnique channel name. Conversation ID is chan:{name}
agentstringrequiredAgent that handles messages on this channel
adaptersstring[]Adapter bindings, same format as [[agents]] adapters. Empty means the channel is reachable only via /session or the API
deliverystring"single"How scheduled messages are delivered: "single" (first specific binding) or "broadcast" (all specific bindings)
session_modestring"persistent""persistent" keeps one conversation per channel; "ephemeral" starts a fresh one per interaction (conversation ID chan:{name}:{unix_nano}). Cross-adapter ephemeral channels are rejected at validation

Users switch channels at runtime with /session <name>; the selection is persisted. Resolution priority is: active /session override > specific binding > wildcard binding > legacy agent-adapter fallback.

[memory]

KeyTypeDefaultDescription
db_pathstring"<data_dir>/data/memory.db"SQLite database path
retention_daysint90How long conversations are kept. 0 = unlimited
max_conversationsint10000Cap on stored conversations. 0 = unlimited
cleanup_intervalstring"1h"How often retention is enforced
persona_memory_char_limitint2200Cap on MEMORY.md size in characters. 0 in TOML falls back to this default — there is currently no way to request an unlimited cap
persona_user_char_limitint1375Cap on USER.md size in characters. 0 in TOML falls back to this default — there is currently no way to request an unlimited cap

[log]

KeyTypeDefaultDescription
levelstring"info""debug", "info", "warn", "error"
formatstring"text""text" or "json"

[voice]

KeyTypeDefaultDescription
stt_providerstringSpeech-to-text provider ("openai")
tts_providerstringText-to-speech provider ("openai")
tts_voicestring"alloy"Voice name
auto_voice_replyboolfalseReply with voice when user sends voice

[voice.openai]

KeyTypeDefaultDescription
api_keystringrequiredOpenAI API key for STT/TTS

[web]

Built-in web_search and web_fetch tools. Restart-only — [web] settings are not hot-reloaded.

KeyTypeDefaultDescription
enabledbooltrueEnable web search/fetch tools for agents

[web.search]

KeyTypeDefaultDescription
providerstring"duckduckgo"Search backend: "duckduckgo" or "tavily"
api_keystringProvider API key (required for Tavily)
max_resultsint5Number of search results to return

[web.fetch]

KeyTypeDefaultDescription
timeoutstring"30s"HTTP request timeout
max_size_bytesint5242880Raw response body size limit (5 MB)
max_response_charsint8000Characters of converted Markdown returned per web_fetch call (max 100000); longer pages paginate via start_index. Each pagination round re-reads the full conversation context, so 24000–32000 usually serves a whole article in one call. Also editable in the Server Config dashboard page
user_agentstring"Denkeeper/1.0 (+https://denkeeper.io)"HTTP User-Agent header
respect_robots_txtboolfalseCheck robots.txt before fetching
respect_agents_txtboolfalseCheck agents.txt before fetching

[web.fetch.jina]

KeyTypeDefaultDescription
enabledboolfalseEnable Jina Reader as a fallback fetcher for JS-heavy pages

[script]

Bounds for the in-process run_javascript tool, which runs short ES5.1 snippets against a JSON input in a fresh sandboxed VM per call. There is no network, no filesystem, and no require. Disabled entirely in the restricted tier.

KeyTypeDefaultDescription
enabledbooltrueEnable run_javascript
timeoutstring"2s"Per-call wall-clock limit
max_output_charsint16000Result length cap (truncates)
max_input_bytesint262144Accepted input payload cap, 256 KiB (rejects)
max_concurrentint4Simultaneous VM executions across all agents. Negative = unlimited
max_concurrent_per_agentint0Additional per-agent cap so one agent cannot monopolize the global pool. 0 = off

There is no per-VM heap cap. max_concurrent bounds the memory multiplier but is not a hard ceiling — lower it if you run on constrained hardware.

[skills]

KeyTypeDefaultDescription
max_bytesint1048576Cap on a single persisted skill file, frontmatter + body (1 MiB). Negative = unlimited

Skill content is written verbatim, so without a bound an authorized caller could exhaust disk. The cap is enforced on every write surface (REST, config MCP, external MCP).

[browser]

Containerized browser automation. Off by default.

KeyTypeDefaultDescription
enabledboolfalseEnable browser automation
imagestring"ghcr.io/temikus/denkeeper-browser:latest"Browser plugin container image
memory_limitstring"512m"Container memory limit
cpu_limitstring"1"Container CPU limit
profile_dirstring"data/browser-profiles"Per-agent profile directory, relative to data_dir
session_ttlstring"10m"Idle session close timeout
max_pagesint5Concurrent pages per agent

[browser.url_allowlist]

KeyTypeDefaultDescription
domainsstring[]Domains the browser may navigate to. Empty = unrestricted. Supports *.example.com

Individual agents can narrow this further with browser_url_allowlist on [[agents]].

[costs]

KeyTypeDefaultDescription
default_rate_per_1k_tokensfloat0Fallback rate (USD per 1K tokens) when a model is in neither the bundled registry nor your overrides. 0 records $0.00 and logs a warning

Denkeeper ships a pricing registry covering roughly 70 models. Lookup priority is: provider-reported cost > registry exact match > registry longest-prefix match > default_rate_per_1k_tokens > $0 with a warning. The winning source is recorded as the pricing_source telemetry attribute, so you can tell a real price from a fallback.

[costs.model_prices.<model>]

Override or add pricing for a model. Rates are USD per million tokens.

KeyTypeDefaultDescription
inputfloatInput token rate
outputfloatOutput token rate
cached_inputfloat0Cached-input rate. 0 means “same as input
[costs.model_prices."my-org/custom-model"]
input = 3.0
output = 15.0
cached_input = 0.3

[audit]

KeyTypeDefaultDescription
enabledbooltrueEnable audit logging
retention_daysint30How long audit events are kept. 0 = unlimited
cleanup_intervalstring"1h"How often retention is enforced
buffer_sizeint1000Capacity of the in-memory event buffer. Emission never blocks an agent turn: events past a full buffer are dropped with a warning log

Events are queryable via GET /api/v1/audit and the dashboard’s Audit Log page.

[eval]

KeyTypeDefaultDescription
auditstring"full"How much of a dry-run or eval turn reaches the audit log. "full" records it like a live turn; "summary" keeps only lifecycle events and errors
max_concurrentint2Eval samples running at once, process-wide across all runs
max_cost_per_runfloat2.0Default USD ceiling for one eval run, overridable per run. There is no uncapped value
default_kint3Samples per (task, variant) pair, giving the objective metrics something to average over
completeness_floorfloat0.8Fraction of expected samples that must succeed before a run’s scorecard is called conclusive
win_thresholdfloat0.55Blinded-pair judge win rate a candidate must reach to be called an upgrade
gate_rejected_rate_ppfloat2.0Largest tolerated rise in the rejected tool-call rate, in percentage points
gate_rounds_pctfloat20Largest tolerated rise in mean tool-call rounds per task, in percent
gate_cost_pctfloat25Largest tolerated rise in mean cost per task, in percent

The last four are the decision rule, and it is deliberately asymmetric: the three gates can declare a downgrade on their own (a failed gate needs no judge to reject a candidate) or report that nothing regressed, but calling a candidate an upgrade also requires the judge win-rate to reach win_threshold. A judge’s preference can never override an objective regression. GET /api/v1/eval/runs/{id}/summary returns the gate table with each value, delta, threshold and pass/fail alongside a one-line reason, plus a per-category breakdown so a candidate that wins on chat while regressing on tool-heavy tasks is visible rather than averaged away.

An eval run is bounded twice, by spend (max_cost_per_run) and by rate (max_concurrent); both are always in force. Writing 0 for any of these keys is indistinguishable from omitting it and yields the default — set a real value to change one. A run that hits its cost cap stops dispatching new samples, lets the in-flight ones finish, and keeps its partial results rather than discarding them.

Dry-run turns persist nothing — no messages, telemetry, or memory — and execute only idempotent tools; everything else returns a suppressed marker. "full" is the default because a preview that is audited like a live turn is easier to trust; the resulting noise is handled by marking rather than by recording less. Preview events are attributed to a pseudo-agent ({name}#dryrun / {name}#eval:{variant}) and carry source = dryrun/eval, so the Audit Log page’s “Previews” toggle can filter them out of both the event list and the statistics.

[api]

KeyTypeDefaultDescription
enabledbooltrueEnable the REST API server and web dashboard
listenstring":8080"Bind address
tlsboolfalseEnable HTTPS
cert_filestringTLS certificate path
key_filestringTLS private key path
cors_originsstring[]Allowed CORS origins
rate_limitfloat0Max requests/sec per API key
websocket_enabledbooltrueEnable the WebSocket endpoint (GET /api/v1/ws)
websocket_max_connectionsint0Maximum concurrent WebSocket connections (0 = unlimited)
websocket_replay_buffer_ttlstring"5m"How long to buffer events for replay after a client disconnects
external_urlstringPublicly-reachable base URL (used for OAuth callback URLs; defaults to http(s)://<listen>)
timezonestring"UTC"IANA timezone used for cron evaluation and as the fallback for agents without their own timezone. Cron evaluation is restart-only
login_rate_limitint5Failed password logins allowed per window per IP
login_rate_windowstring"15m"Window for login_rate_limit
onboarding_dismissedboolfalseSet by the dashboard when the onboarding checklist is dismissed
wizard_completedboolfalseSet by the dashboard when the setup wizard finishes

[api.mcp_server]

Exposes this Denkeeper instance as an MCP server, so an external MCP client (another agent, an IDE) can drive its agents, skills, schedules, and audit log. Opt-in.

KeyTypeDefaultDescription
enabledboolfalseEnable the MCP server endpoint
transportstring"streamable""streamable" or "sse" (legacy)
session_timeoutstring"30m"Idle session cleanup duration
chat_timeoutstring"2m"Maximum time for a single chat tool call
statelessboolfalseDisable session tracking

Note the direction: [tools.*] is Denkeeper connecting outward to MCP servers; this section is Denkeeper being one.

[[schedules]]

KeyTypeDefaultDescription
namestringrequiredUnique schedule name
typestringrequired"system" or "agent"
schedulestringrequiredCron expression, interval, or named schedule
skillstringSkill to invoke
agentstring"default"Target agent
session_tierstring"supervised"Permission tier for this schedule
channelstringDelivery channel (e.g., "telegram:12345")
tagsstring[]Freeform labels
enabledbooltrueEnable/disable without removing
session_modestring"shared""shared" reuses the target channel’s existing conversation history; "isolated" starts a fresh conversation with no prior context for each run. Note: schedules created via POST /schedules default to "isolated" instead — the default differs by creation path

[plugins.*]

KeyTypeDefaultDescription
typestringrequired"subprocess" or "docker"
commandstringrequiredPlugin binary path (subprocess) or Docker image (docker)
argsstring[]Command-line arguments
envmapEnvironment variable overrides
capabilitiesstring[]required["tools"]
memory_limitstringDocker container memory limit (e.g., "256m")
cpu_limitstringDocker container CPU limit (e.g., "0.5")
networkstring"none"Docker network mode ("none", "bridge", etc.)
volumesstring[]Docker bind mounts

Subprocess plugins run as child processes with direct MCP stdio. Docker plugins run in hardened containers with --cap-drop ALL, --read-only, --security-opt no-new-privileges, and --network none by default.

[security]

KeyTypeDefaultDescription
trusted_keysstring[]Paths to PEM-encoded Ed25519 public key files
allow_unsignedbooltrueAllow unsigned subprocess plugin binaries

When allow_unsigned = false, all subprocess plugin binaries must have a valid Ed25519 signature from one of the trusted keys.

[kv]

KeyTypeDefaultDescription
max_keys_per_agentint1000Maximum keys per agent
max_value_bytesint65536Maximum value size in bytes (64 KB)
cleanup_intervalstring"1h"Background cleanup interval for expired keys

Per-agent key-value storage with optional TTL. Exposed as Config MCP tools (kv_get, kv_set, kv_delete, kv_list, kv_set_nx). Useful for locks, counters, caches, and cross-session coordination.

[sandbox]

KeyTypeDefaultDescription
runtimestring"docker"Sandbox backend: "docker" or "kubernetes"

Selects the runtime backend for sandboxed (Docker-type) plugins.

[sandbox.kubernetes]

KeyTypeDefaultDescription
namespacestring"denkeeper-sandboxes"Kubernetes namespace for sandbox Pods
kubeconfigstringPath to kubeconfig file (empty uses in-cluster config)
runtime_classstringRuntimeClassName for gVisor or Kata Containers

The Kubernetes backend creates ephemeral Pods with init-container network isolation, dropped capabilities, read-only root filesystem, and Pod Security Admission labels. Supports both in-cluster (ServiceAccount) and out-of-cluster (kubeconfig) authentication.

[mcp]

Global settings that apply to all MCP tool servers.

KeyTypeDefaultDescription
request_timeout_secsint30Per-request timeout for MCP calls (0 = no timeout)
auto_restartbooltrueAutomatically restart crashed stdio servers
max_restart_attemptsint3Consecutive failures before disabling a server
restart_cooldownstring"5m"Duration a server must stay connected to reset the failure counter
drain_timeoutstring"35s"How long teardown waits for in-flight tool calls before forcing the transport closed
url_allowliststring[]Allowed hostnames/wildcards for SSE tool server URLs (empty = all non-blocked hosts)
health_fail_thresholdint3Consecutive health-probe failures before a health_fail audit event fires for remote (sse/http) servers. Stdio servers always emit on the first failure
init_retry_attemptsint5Retries for a server’s initial connection attempt
init_retry_backoffstring"2s"Base backoff duration between initial-connection retries
sse_keep_alive_secsint15TCP keepalive interval for SSE connections (overridable per server)
env_passthroughstring[]Extra parent-process environment variable names forwarded to stdio server subprocesses, on top of the built-in non-secret allowlist. DENKEEPER_* and other denylisted names are always blocked, even here

Removing, disabling, restarting or reconfiguring a tool server tears it down in two phases. The server stops being offered immediately — its tools leave the advertised set and new calls are refused — and only then does Denkeeper wait for the calls already running to finish, up to drain_timeout, before closing the transport. The server reports status draining while it waits. A window that expires forces the close and records a forced_close event in the audit log with the number of calls that were still running. The default sits just above the 30s per-tool-call timeout, so a call that was going to succeed gets to.

[tools.*]

KeyTypeDefaultDescription
transportstring"stdio"Transport type: "stdio" (subprocess) or "sse" (remote HTTP/SSE)
commandstringrequired for stdioMCP server command (stdio only)
argsstring[]Command arguments (stdio only)
envmapEnvironment variables; supports ${NAME} placeholder expansion (stdio only)
urlstringrequired for sseRemote server URL (SSE only, must be http/https)
headersmapHTTP headers sent with SSE requests (SSE only)
request_timeout_secsint0Per-server timeout override (0 = use global [mcp] value)
authstring""Authentication method: "" (none) or "oauth" (OAuth 2.1, SSE only)
client_idstringOAuth2 client ID (optional; some servers use dynamic registration)
client_secretstringOAuth2 client secret (optional; must be set together with client_id)
scopesstring[]OAuth2 scopes to request (optional)
idempotentboolfalseMemoize identical calls to this server’s tools within one message turn (identical name+args returns the cached result instead of re-executing). Only set on servers whose tools are all read-only — a cached write is a silently dropped side effect
idempotent_toolsstring[]Per-tool memoization opt-in for servers that mix read and write tools; union with idempotent
trust_annotationsboolfalseAlso memoize tools this server marks read-only via the MCP readOnlyHint annotation. Annotations are self-declared by the server — enable only for servers you trust to describe their tools honestly

SSE security: SSRF protection blocks localhost, link-local (169.254.x.x), and cloud metadata endpoints. ${NAME} placeholders in url and headers are resolved from environment but secrets matching DENKEEPER_*_SECRET, DENKEEPER_*_PASSWORD*, and related patterns are denied. URL and header values are redacted in API responses.

Tools can also be added and removed at runtime via the REST API (tools:write scope) or the Config MCP server (tool_add/tool_remove). Runtime changes are persisted to the TOML config file.

[otel]

KeyTypeDefaultDescription
enabledboolfalseEnable OpenTelemetry instrumentation
traces_endpointstringOTLP HTTP endpoint for trace export (e.g. "http://localhost:4318")
service_namestring"denkeeper"Service name for the OTel resource

Env overrides: DENKEEPER_OTEL_ENABLED sets enabled, DENKEEPER_OTEL_TRACES_ENDPOINT sets traces_endpoint.

When enabled, Prometheus metrics are exposed at GET /metrics (no auth required). Traces are only exported when traces_endpoint is set.

[api.auth]

KeyTypeDefaultDescription
password_hashstringbcrypt hash from denkeeper passwd CLI
session_secretstringHex-encoded AES-256 key (64 hex chars). Generate with openssl rand -hex 32
session_max_agestring"24h"Session cookie lifetime

Env override: DENKEEPER_API_AUTH_SESSION_SECRET sets session_secret.

[api.auth.oidc]

KeyTypeDefaultDescription
enabledboolfalseEnable OIDC SSO
issuerstringOIDC provider issuer URL (e.g. "https://accounts.google.com")
client_idstringOAuth2 client ID
client_secretstringOAuth2 client secret
redirect_urlstringCallback URL (e.g. "https://denkeeper.example.com/auth/callback")
scopesstring[]["openid","email","profile"]OAuth2 scopes
allowed_emailsstring[]Email allowlist. Required non-empty when enabled. Case-insensitive.

Env overrides: DENKEEPER_OIDC_CLIENT_ID sets client_id, DENKEEPER_OIDC_CLIENT_SECRET sets client_secret.

Requires email_verified: true claim from the OIDC provider. Uses Authorization Code flow with PKCE (S256).