lecode reads TOML config only (config.toml):
- Global:
~/.config/lecode/config.toml(the directory is overridable with theLECODE_CONFIG_DIRenv var). Auto-created with commented defaults on first run. - Project:
.lecode/config.toml, found by walking from the cwd up to the git root. Deep-merged over the global config: dicts merge recursively, scalars and lists replace. - CLI flags apply on top of both (
--model,--provider,--base-url,--api-key, …).
Unknown keys produce startup warnings. schema_version = 1 is the current
on-disk schema; older files are migrated forward automatically.
The API key resolution chain is: --api-key > provider env var
(OPENROUTER_API_KEY, OPENAI_API_KEY, or [custom_providers.*].api_key_env)
[llm].api_keyin the config file. If you store the key in the file, keep it owner-only (chmod 600);lecode --setupdoes that for you.
| field | default | meaning |
|---|---|---|
provider |
"openrouter" |
openrouter or a [custom_providers] name |
model |
"deepseek/deepseek-v4-flash" |
default model id |
api_key |
unset | provider key (env vars preferred) |
base_url |
unset | override the provider's endpoint |
thinking |
"medium" |
none | low | medium | high |
connect_timeout_s |
10.0 |
HTTP connect timeout |
read_timeout_s |
300.0 |
HTTP read (streaming) timeout |
auth_policy |
"auto" |
auto | required | none — fail when no key found |
tls_verify |
true |
set false for self-signed endpoints |
The model catalog is fetched live from the provider's /models endpoint at
startup (context windows, pricing, modalities). When the fetch fails the
catalog is empty — models lose pricing/modality annotations and costs report
as unknown until the provider reports usage — and nothing is cached on disk. Plain
OpenAI-shaped /models responses (id only) get a 128k
default context window and zeroed pricing.
| field | default | meaning |
|---|---|---|
style |
"minimal" |
minimal | rich base prompt |
custom |
unset | replaces the base prompt entirely |
persona |
unset | persona name (prompts/personas/<name>.md) appended in rich style |
| field | default | meaning |
|---|---|---|
enabled |
true |
auto-compact when approaching the context window |
buffer_tokens |
20000 |
headroom kept below the window |
on_overflow |
"continue" |
continue | pause when even compaction can't fit |
mid_turn_threshold |
unset | absolute token count that triggers compaction on tool-loop rounds after the first (instead of window − buffer) |
| field | default | meaning |
|---|---|---|
max_turns |
500 |
hard turn budget per prompt |
context_window |
200000 |
assumed model window for compaction |
turn_cooldown_ms |
0 |
sleep between turns |
tool_idle_timeout_s |
300 |
abort a turn after this much provider silence |
subagent_model |
unset | model for subagents (/model-subagent); inherits main |
| field | default | meaning |
|---|---|---|
enabled |
{} |
per-tool on/off, e.g. enabled = { bash = false } |
allowlist |
[] |
when non-empty, only these tools are registered |
| field | default | meaning |
|---|---|---|
collapse_thinking |
true |
collapse reasoning blocks in the feed |
show_welcome |
true |
show the welcome cheat-sheet on startup |
hidden_models |
[] |
model ids hidden from /models |
no_color |
false |
disable colored output |
The theme is fixed: one dark, Kalvad-purple palette ("kalvad"). There is no theme selection, no theme files, and no color overrides.
mode is one of yolo (default; everything allowed) or readonly (read-class
tools allowed, everything else denied). Legacy mode values (standard,
restrictive, planwrite, guarded) still load but are coerced to yolo
with a deprecation warning. Rules live under [permissions.rules] as
allow / ask / deny tables mapping tool names to pattern lists:
[permissions]
mode = "yolo"
[[permissions.rules.allow.bash]]
pattern = "git status"
[[permissions.rules.ask.write]]
pattern = "*.py"
[[permissions.rules.deny.bash]]
pattern = "rm -rf*"
kind = "glob" # "glob" (default) or "regex"Last match wins within a table; deny rules are unbypassable (even in yolo),
and ask rules still prompt. Read-class tools (read, grep, find_files,
list_dir, lsp_diagnostics, memory_read, memory_search,
task, and Exa/context7/grep.app MCP tools) are the only tools allowed in
readonly. A 3rd identical consecutive call escalates Allow → Ask, the 4th
is denied (doom-loop guard).
Sound notifications (afplay/paplay/aplay, terminal bell fallback) and desktop notifications (osascript on macOS, notify-send on Linux).
| field | default | meaning |
|---|---|---|
enabled |
true |
master switch (/notifications on|off toggles per session) |
volume |
0.5 |
0.0–1.0 (afplay only) |
sound |
true |
sound channel (player or bell) |
desktop |
true |
desktop-notification channel |
on_finish / on_error / on_approval |
true |
per-event toggles (both channels) |
Lifecycle status events as JSON datagrams over a Unix socket — for status bars and tmux integration.
| field | default | meaning |
|---|---|---|
enabled |
false |
emit start / stop / git-conflict events |
socket_path |
<config_dir>/lecode.sock |
datagram destination |
Payload: {"event", "session", "ts", …}; failures are dropped silently.
| field | default | meaning |
|---|---|---|
enable_exa |
true |
auto-configure Exa web search when EXA_API_KEY is set |
enable_context7 |
false |
auto-configure the context7 docs server |
[mcp.servers.<name>] entries:
| field | default | meaning |
|---|---|---|
transport |
"stdio" |
stdio | http (streamable HTTP) | sse (legacy) |
command / args / env |
— | stdio: argv and extra environment |
url / headers |
— | http/sse: endpoint and extra headers (static bearer tokens go here) |
auth |
— | http/sse: "oauth" enables the interactive OAuth 2.1 flow; credentials in <config_dir>/mcp-auth/ (0600) |
timeout_s |
30.0 |
per-call timeout |
enabled |
true |
disabled servers are skipped |
See mcp.md.
| field | default | meaning |
|---|---|---|
enabled |
true |
spawn language servers, append diagnostics to write/edit |
[lsp.servers.<lang>] overrides the built-in registry (command,
file_patterns) or defines a new language. Diagnostics are fail-open: LSP
trouble never blocks the agent.
| field | default | meaning |
|---|---|---|
enabled |
true |
persistent markdown memory + the four memory_* tools |
max_bytes |
32768 |
injection cap for MEMORY.md |
See memory.md.
Post-task reviewer: after every completed task, a second model compares the request with the result and gives feedback in the feed.
| field | default | meaning |
|---|---|---|
enabled |
false |
review every finished task + /pierre |
model |
unset | reviewer model id; /pierre on requires one, different from the main model |
Event name → list of shell commands:
[hooks]
PreToolUse = ["./ci/check-tool.sh"]
Stop = ["say done"]Events: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest,
PermissionResult, UserPromptSubmit, Stop, SessionStart, SessionEnd,
SubagentStart, SubagentEnd, PreCompact, PostCompact, Interrupt,
Notification. Hooks can only narrow permission verdicts; only PreToolUse
and UserPromptSubmit deny-verdicts are enforced. See hooks.md.
[custom_providers.<name>]— extra OpenRouter-compatible providers:
[custom_providers.local]
base_url = "http://localhost:11434/v1"
api_key_env = "LOCAL_API_KEY" # optional
auth_policy = "none" # auto | required | none
# headers = { X-Team = "infra" }Opt-in Sentry error reporting + OpenTelemetry metrics. Requires the
telemetry extra (uv tool install 'lecode[telemetry]'); every failure mode
is fail-open (telemetry never breaks the agent).
| field | default | meaning |
|---|---|---|
enabled |
false |
master switch |
sentry_dsn |
unset | Sentry (or GlitchTip) DSN for error reports |
otlp_endpoint |
unset | OTLP/HTTP base URL for metrics (e.g. http://localhost:4318) |
export_interval_s |
60.0 |
metric export interval |
service_name |
"lecode" |
OTel service.name resource |
environment |
"dev" |
Sentry/OTel environment tag |
Metrics: lecode.turns, lecode.turn.duration_s, lecode.tokens.input,
lecode.tokens.output, lecode.cost_usd, lecode.tool_calls,
lecode.tool.duration_s (all tagged by model / tool / stop reason). Sentry
receives provider and tool errors with send_default_pii = false.