`odek uses a layered configuration system with convention over configuration — opt-in files and environment variables, no mandatory setup.
Each layer overrides the one below it. Unset fields inherit from the layer below:
0. ~/.odek/secrets.env ← Auto-loaded into process environment on startup
1. ~/.odek/config.json ← Global defaults (shared across projects)
2. ./odek.json ← Project-specific overrides
3. ODEK_* env vars ← Runtime/environment overrides
4. CLI flags ← Explicit invocation (highest priority)
Layer 0 is unique: it does not hold config fields directly. Instead it injects
KEY=VALUE pairs into the process environment so they're available for:
- Layer 1–2
${VAR}substitution in config files - Layer 3
ODEK_*env var lookups (e.g.ODEK_API_KEY) - Legacy fallbacks like
DEEPSEEK_API_KEY/OPENAI_API_KEY
Shared across all projects:
{
"model": "deepseek-v4-flash",
"base_url": "https://api.deepseek.com/v1",
"api_key": "${ODEK_API_KEY}",
"thinking": "",
"max_iterations": 90,
"sandbox": false,
"interaction_mode": "engaging",
"no_color": false,
"no_agents": false,
"max_tool_parallel": 4,
"tool_progress": "all",
"tool_progress_cleanup": true,
"system": ""
}Same schema as global. Only set the fields you want to override:
{
"model": "gpt-4o",
"base_url": "https://api.openai.com/v1",
"max_iterations": 30
}Both files are optional. Missing files are silently ignored. String values support ${VAR} environment variable substitution — useful for API keys without plaintext storage.
Auto-loaded on every odek invocation before any config file or env var is read.
Each KEY=VALUE line is injected into the process environment via os.Setenv.
ODEK_API_KEY=sk-...
GITHUB_TOKEN=ghp_...
Rules:
- File format:
KEY=VALUE— one per line, noexportkeyword needed - Blank lines and
#comments are skipped - Existing env vars are NOT overwritten — if
ODEK_API_KEYis already in the environment, the file is ignored for that key - Missing/unreadable file is silently ignored (not an error)
- Permissions: keep
0600(chmod 600 ~/.odek/secrets.env)
This lets you keep secrets out of config files entirely:
// ~/.odek/config.json — no plaintext secrets
{
"model": "deepseek-v4-flash",
"api_key": "${ODEK_API_KEY}" // ← resolved from secrets.env at runtime
}Every config knob has a ODEK_* counterpart:
| Variable | Maps to | Type |
|---|---|---|
ODEK_MODEL |
--model |
string |
ODEK_BASE_URL |
--base-url |
string |
ODEK_API_KEY |
config files only | string |
ODEK_THINKING |
--thinking |
string |
ODEK_MAX_ITER |
--max-iter |
int |
ODEK_SANDBOX |
--sandbox |
bool |
ODEK_INTERACTION_MODE |
--interaction-mode |
string |
ODEK_NO_COLOR |
--no-color |
bool |
ODEK_NO_AGENTS |
--no-agents |
bool |
ODEK_SYSTEM |
--system |
string |
ODEK_SKILLS_LEARN |
skills.learn |
bool |
ODEK_PROMPT_CACHING |
prompt_caching |
bool |
ODEK_TOOL_PROGRESS |
tool_progress |
string (all|new|verbose|off) |
ODEK_SANDBOX_IMAGE |
--sandbox-image |
string |
ODEK_SANDBOX_NETWORK |
--sandbox-network |
string |
ODEK_SANDBOX_READONLY |
--sandbox-readonly |
bool |
ODEK_SANDBOX_MEMORY |
--sandbox-memory |
string |
ODEK_SANDBOX_CPUS |
--sandbox-cpus |
string |
ODEK_SANDBOX_USER |
--sandbox-user |
string |
ODEK_MAX_TOOL_PARALLEL |
max_tool_parallel |
int |
ODEK_API_KEY → DEEPSEEK_API_KEY → OPENAI_API_KEY
When a model emits multiple tool calls in one response (tool_calls array with N entries), odek executes them concurrently in goroutines bounded by a semaphore.
| Field | Default | Env var | Description |
|---|---|---|---|
max_tool_parallel |
4 |
ODEK_MAX_TOOL_PARALLEL |
Max concurrent tool calls per iteration. 0 = default 4. Set to 1 for sequential execution. |
I/O-bound tools (read_file, search_files, shell) benefit most — latency drops from sum(latencies) to max(latency).
Approval gate: When an approver is configured and the LLM returns multiple tool calls, a single batch approval prompt is shown before any tool executes. If approved, all tools run in parallel. If denied, no tools run.
The skills section controls the skill system:
{
"skills": {
"max_auto_load": 3,
"max_lazy_slots": 5,
"learn": true,
"llm_learn": true,
"llm_curate": true,
"import": {
"max_size_bytes": 1048576,
"timeout_seconds": 5,
"require_https": false
},
"curation": {
"staleness_days": 90,
"auto_prune": false,
"auto_curate": true,
"skip_threshold": 1,
"skip_reset_days": 30
},
"auto_save": {
"enabled": true,
"require_llm": true,
"max_per_run": 3
}
}
}| Field | Env var | Default | Description |
|---|---|---|---|
max_auto_load |
— | 3 | Max skills injected into system prompt on start |
max_lazy_slots |
— | 5 | Max skills loaded per user input via trigger matching |
learn |
ODEK_SKILLS_LEARN |
true |
Enable skill learning mode (detects patterns, suggests skills). On by default |
llm_learn |
— | true |
Use LLM to enrich detected patterns. Template-only — set via odek init, not parsed from JSON at runtime |
llm_curate |
— | true |
Use LLM for curation quality assessment. Template-only — set via odek init, not parsed from JSON at runtime |
dirs |
— | [] | Extra skill directories beyond ~/.odek/skills and ./.odek/skills |
import.max_size_bytes |
— | 1048576 (1MB) | Max size for fetched skill content |
import.timeout_seconds |
— | 5 | HTTP timeout for skill URI fetch |
import.require_https |
— | false | Reject http:// URIs when true |
curation.staleness_days |
— | 90 | Days without use before flagging as stale |
curation.auto_prune |
— | false | Auto-delete stale skills on curate (no prompt) |
curation.auto_curate |
— | true | Run auto-curation after sessions (merge, dedup, prune) |
curation.skip_threshold |
— | 1 | Times a skill must be skipped before permanent suppression |
curation.skip_reset_days |
— | 30 | Days after which a skip expires (re-allows suggestion) |
auto_save.enabled |
— | true | Auto-save quality skill suggestions without prompting |
auto_save.require_llm |
— | true | Only auto-save if LLM enhancement was applied |
auto_save.max_per_run |
— | 3 | Max skills to auto-save per session |
The memory section controls the persistent memory system (see docs/MEMORY.md):
{
"memory": {
"enabled": true,
"facts_limit_user": 1500,
"facts_limit_env": 2500,
"buffer_lines": 20,
"buffer_enabled": true,
"merge_on_write": true,
"extract_on_end": true,
"llm_search": true,
"llm_extract": true,
"llm_consolidate": true,
"merge_threshold": 0.7,
"add_threshold": 0.3
}
}| Field | Default | Description |
|---|---|---|
enabled |
true | Enable memory system entirely |
facts_limit_user |
1500 | Max chars for user.md fact file |
facts_limit_env |
2500 | Max chars for env.md fact file |
buffer_lines |
20 | Max turn summaries in session buffer |
buffer_enabled |
true | Enable the turn-level buffer |
merge_on_write |
true | Use go-vector RP similarity to auto-merge related entries |
extract_on_end |
true | Extract durable facts via LLM at session end (≥3 turns) |
llm_search |
true | Use LLM to rank episode search results by relevance |
llm_extract |
true | Use LLM for end-of-session fact extraction |
llm_consolidate |
true | Use LLM to merge related fact entries |
merge_threshold |
0.7 | go-vector cosine threshold for auto-merge (0.0–1.0) |
add_threshold |
0.3 | go-vector cosine threshold for auto-add (0.0–1.0) |
The subagent section controls task decomposition and parallel sub-agent execution (see docs/SUBAGENTS.md):
{
"subagent": {
"max_concurrency": 3,
"timeout_seconds": 120,
"max_iterations": 15
}
}| Field | Default | Description |
|---|---|---|
max_concurrency |
3 | Max sub-agents running in parallel (max 8) |
timeout_seconds |
120 | Default timeout per sub-agent (overridden by --timeout) |
max_iterations |
15 | Default max think→act cycles per sub-agent (overridden by --max-iter) |
This section is optional. Omitted fields inherit sensible defaults.
Note: The
subagentsection is currently read only fromodek.jsonby theodek subagentcommand in test code. Runtime values (max_concurrency,timeout_seconds) are hardcoded in productionodek run/odek serve. This may be wired up fully in a future release.
Connect to external MCP servers and expose their tools to the agent. Any MCP server that works with Claude Code works with odek — same config format.
{
"mcp_servers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp"]
},
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
}| Field | Description |
|---|---|
command |
The executable to run |
args |
Optional command-line arguments |
env |
Optional environment variable overrides (empty string removes from env) |
Tools are registered as <server_name>__<tool_name> (e.g., playwright__navigate)
and are available in odek run, odek repl, odek continue, and odek serve.
See docs/MCP.md for detailed instructions.
The telegram section configures the Telegram bot integration and the --deliver flag.
{
"telegram": {
"bot_token": "8610437446:AAElHFJ...",
"allowed_users": [8592463065],
"allowed_chats": [],
"poll_interval": 1,
"poll_timeout": 30,
"max_msg_length": 4096,
"session_ttl_hours": 24,
"log_level": "info",
"log_file": "",
"default_chat_id": 8592463065
}
}| Field | Env var | Default | Description |
|---|---|---|---|
bot_token |
ODEK_TELEGRAM_BOT_TOKEN |
— (required) | Telegram bot API token from @BotFather |
allowed_users |
— | all | Restrict bot to specific user IDs |
allowed_chats |
— | all | Restrict bot to specific chat IDs |
poll_interval |
— | 1 | Seconds between poll cycles |
poll_timeout |
— | 30 | Long-poll timeout (1-60 seconds) |
max_msg_length |
— | 4096 | Max characters per message |
session_ttl_hours |
— | 24 | Hours before inactive session expires |
log_level |
— | info | Log level: debug, info, warn, error |
log_file |
— | stderr | Log file path (empty = stderr) |
default_chat_id |
— | 0 | Required for --deliver — numeric chat ID where odek run --deliver sends results. Get this from your bot's update or use a tool like @userinfobot. |
The --deliver flag on odek run sends the agent's final response to the configured
default_chat_id as a plain text message. This enables cron-based scheduled agent
workflows — no daemon needed.
# Run an agent task and deliver the result to Telegram
odek run --deliver "Check the CI pipeline status"
# Works with task text first too
odek run "Daily summary" --deliverSee docs/TELEGRAM.md for full cron setup instructions.
Controls how per-tool progress messages appear inside the Telegram bot during agent runs. Independent from interaction_mode — you can have engaging terminal output with minimal Telegram progress, or verbose terminal with rich progress bubbles.
{
"tool_progress": "all",
"tool_progress_cleanup": true
}| Value | Behavior | Use case |
|---|---|---|
"all" (default) |
Single editable progress bubble with smart previews — e.g. 📝 read_file: "main.go". Includes edit throttling (1.5s), tool dedup (×N counter for repeated same-tool), and automatic flood-control fallback |
General use — shows what the agent is doing without spamming the chat |
"new" |
Same as "all" but only updates when the tool name changes. Consecutive read_file calls produce one line; a shell call starts a new line |
Long-running agents with repetitive tool chains (e.g. reading 50 files in batch) |
"verbose" |
Raw tool arguments as separate messages. Each tool call sends a new message with full JSON args; on completion the result is sent as a new message ✅ (size) |
Debugging — see exactly what the agent passes to each tool |
"off" |
No per-tool progress messages at all. Only the initial "🤔 Looking into that..." and final answer are shown | Privacy-sensitive contexts or users who prefer zero noise |
Default: true. Controls whether the progress message bubble is deleted after the agent's final answer arrives:
true— delete the progress bubble (clean chat, no stale tool traces)false— keep the progress bubble as a breadcrumb of what the agent did
The progress system is an evolving single message that gets edited in-place (similar to an animated status). Each tool call adds a line like:
📝 read_file: "main.go"
💻 shell: "npm test"
📝 read_file: "utils.go" (×3)
Key behaviors:
- Smart previews — instead of showing raw JSON args, the system extracts meaningful context: filename for file tools, the command text for shell, URL for browser, query text for memory/search tools, audio filename for transcribe
- Edit throttling — edits are rate-limited to one every 1.5 seconds to avoid hitting Telegram's flood control limits. Rapid tool chains don't produce 429 errors
- Tool dedup — when the same tool runs consecutively (common with parallel batch tools like
batch_read), identical lines are collapsed into a(×N)counter instead of repeating N times - Flood control fallback — if an edit message fails with "flood" or "retry after", the system automatically switches to sending new messages instead of editing. This prevents the bot from becoming unresponsive under heavy load
- Content reset — when the agent calls
send_messagemid-run to send an interim message, the progress bubble resets below that content, keeping the chat timeline in correct order
Create a config file template:
# Local project config (./odek.json)
odek init
# Global config (~/.odek/config.json)
odek init --global
# Overwrite existing file
odek init --force# Set API key via secrets.env (recommended — keeps secrets out of config files)
echo 'ODEK_API_KEY="sk-..."' >> ~/.odek/secrets.env
chmod 600 ~/.odek/secrets.env
# Global config (model and other settings only, no secrets)
echo '{"model": "deepseek-v4-flash"}' > ~/.odek/config.json
odek run "list files"
# Per-project override
echo '{"max_iterations": 30}' > ./odek.json
odek run "quick status"
# Env var override for one-off
ODEK_SANDBOX=true odek run "run untrusted script"
# Enable skill learning via env var
ODEK_SKILLS_LEARN=true odek run "set up CI"
# Sub-agent config (project-level)
echo '{"subagent": {"max_concurrency": 5, "timeout_seconds": 300}}' > ./odek.json
# CLI flag always wins
odek run --model gpt-4o --base-url https://api.openai.com/v1 "task"