dirge reads an optional JSON config file named config.json from its config
folder:
- If
DIRGE_CONFIG_DIRis set:$DIRGE_CONFIG_DIR/config.json - Otherwise: the platform config directory joined with
dirge/config.json(for example$XDG_CONFIG_HOME/dirge/config.jsonon Linux) - Fallback:
$HOME/.config/dirge/config.json
A project may also ship a partial <project>/.dirge/config.json. It is
deep-merged on top of the global file: scalar fields override, while maps
(providers, mcp_servers, agents, slash_aliases) union key-by-key, so
a project can add or override a single entry without redeclaring the whole
map. Arrays are replaced wholesale, not merged — notably keybindings (a
list): a project keybindings entry wipes the global list rather than
extending it, so redeclare the full set if you also want the global bindings. Absent keys fall through to the global file. An
empty object (e.g. "providers": {}) is a no-op, not a wipe — there is no
syntax to clear a global map from a project config. CLI flags and env vars
still take precedence over both files.
All config keys are optional. CLI flags and their environment-backed values
(such as DIRGE_PROVIDER and DIRGE_MODEL) take precedence where both exist.
Example:
{
"provider": "openrouter",
"max_tokens": 8192,
"temperature": 0.7,
"context_window": 128000,
"reserve_tokens": 16384,
"keep_recent_tokens": 20000,
"compact_enabled": true,
"default_prompt": "code",
"default_permission_mode": "standard",
"show_tool_details": true,
"show_edit_diff": true,
"animations_enabled": true,
"show_reasoning": false,
"display": "left|main|right",
"tool_result_max_chars": 500,
"tool_result_max_lines": 4,
"providers": {
"openrouter": {
"model": "deepseek/deepseek-v4-flash"
},
"local-vllm": {
"provider_type": "openai",
"base_url": "http://localhost:8000/v1",
"api_key_env": "VLLM_API_KEY"
}
},
"permission": {
"*": "ask",
"rules": [
{ "op": "edit", "match": "**/*.rs", "effect": "allow" },
{ "op": "edit", "match": "**", "effect": "ask" },
{ "op": "execute", "match": "cargo test", "effect": "allow" },
{ "op": "execute", "match": "rm **", "effect": "deny" }
],
"external_directory": [
{ "match": "/tmp/**", "effect": "allow" },
{ "match": "/**", "effect": "ask" }
],
"doom_loop": "ask"
}
}Accepted top-level keys:
| Key | Type | Description |
|---|---|---|
provider |
string | Active provider alias. Built-ins are openrouter, openai, anthropic, gemini/google, deepseek, glm/zhipu, cerebras, opencode, kimi/kimi-code/moonshot, and ollama; any alias declared in providers is also accepted. Default: openrouter. See Providers and roles. |
auth |
string | Default authentication source for providers that don't set their own providers.<name>.auth: api-key (the implicit default), chatgpt (Codex/OpenAI login tokens), anthropic / claude-code (Anthropic Claude Code OAuth), or kimi (Kimi Code device OAuth). See Providers and roles. |
providers |
object | Map of provider alias → entry. The active model lives in providers.<active-provider>.model. Each role key below points at one of these aliases. See Providers and roles. |
review_provider |
string | Provider alias for the background session-review pass. Falls back to provider. |
escalation_provider |
string | Provider alias for the one-shot retry after repair-exhaustion / pre-write syntax failure. Falls back to provider (no-op when equal). |
summarization_provider |
string | Provider alias for context compaction. Falls back to provider; with Anthropic OAuth, configure a non-Anthropic-OAuth summarization provider for LLM compaction side calls. Reactive overflow can still use a local prune-only emergency fallback, but high-fidelity LLM summaries require this route. |
subagent_provider |
string | Provider alias for task tool subagents. Falls back to provider. |
subagent_dispatch_strategy |
string | Coordinated background dispatch mode: off (default), optional, or full. Coordination requires eligible readonly and readwrite agent profiles and runs only in the interactive TUI. See Coordinated Subagents. |
subagent_write_isolation |
string | Where coordinated read-write subagents run: auto (default), worktree, or serialize. Worktree isolation requires a confining Linux sandbox; auto otherwise falls back to one serialized writer in a clean parent checkout. See Coordinated Subagents. |
critic_provider |
string | Provider alias for the F6 in-loop critic (tier 3). When set, the verifier escalates to a bounded LLM critique at finalization on substantive runs (one call per run); it also judges the goal gate (--goal) and powers the diff-aware code reviewer (reviews the run's uncommitted diff, blocks on high/critical findings, advises on medium/low — also runnable on demand via /code-review). No fallback — unset means no critic, no reviewer, no goal gate, and no cost. |
critic_preamble |
string | Optional system-preamble override for the F6 in-loop critic. Replaces the built-in critic stance for every prompt. A prompt's critic_preamble frontmatter overrides this per-prompt; a critic: false frontmatter suppresses the critic entirely for that prompt. The goal gate is unaffected by either — it always judges under its own fixed preamble. Unset = built-in. |
code_review |
string | How the diff-aware code reviewer engages at finalization: off | advisory | blocking. advisory (default) runs the review in the background after the turn finalizes and surfaces all findings (high/critical included) as one non-blocking <system> notice — it never holds up the turn and never re-enters the loop, so a tight debug loop isn't interrupted. blocking runs it synchronously on the finalization path: high/critical findings re-enter the loop (fix or justify), medium/low advise. off disarms it entirely (no diff capture, no judge call, zero cost). Still requires critic_provider (the reviewer reuses its judge); a critic: false prompt also suppresses it. A prompt's code_review frontmatter overrides this per-prompt. |
verification_tiers |
string | Splits build/test commands into a fast tier (typecheck, lint, one targeted test) and a slow tier (full suite/build): off | advisory | blocking. off (default) is byte-identical to the untiered gate. advisory adds one mid-run nudge to run a fast check once three code edits pile up unverified, plus one finalization escalation when only the fast tier ever passed. blocking repeats that escalation (bounded). Unknown commands tier as slow, so an unrecognized one errs toward silence. See failure-ladder.md. |
progress_stall_threshold |
integer | Turn boundaries with no progress (no todo closed, no new file touched, no green check) before the loop asks the model what's blocking and whether to cut scope. Absent (default) disables the monitor entirely. Clamped to a minimum of 2. Also enables turn-budget notices at 60%/85% of max_agent_turns — those need a cap set to mean anything. Arms only after the first progress event, so an exploration phase is never nudged. See failure-ladder.md. |
progress_prologue_cap |
integer | Barren turn boundaries (or four times that many barren tool calls) before a run that has produced nothing at all gets one checkpoint pushing for the smallest possible first write. Default 24. This is an UPPER BOUND, a safety net against runaway reconnaissance — not an eager nudge — so never firing on normal work is the correct outcome. Only meaningful alongside progress_stall_threshold. See verification-discipline.md. |
verification_command |
string | The command whose pass is the only honest green — typically what CI enforces, e.g. cargo clippy --all-targets -- -D warnings. When set and that command hasn't passed, a result that would read VerifiedGreen becomes FastGreenOnly instead, so the full-suite escalation carries it. Matched by (program, subcommand) signature, so flag and env-prefix differences don't matter. Absent (default) leaves behaviour unchanged, including in off mode. See verification-discipline.md. |
safe_state_abort |
string | Third rung of the failure ladder — when a failure streak reaches twice the recovery-checkpoint threshold with unverified edits on the tree and a green point behind it, abort the approach and re-plan: off | advisory | auto. off (default). advisory injects the re-plan message and writes no files. auto restores the tree itself, but only after proving against git that the snapshot store covers every file changed since the green point — one uncaptured file (a sed -i, a formatter, any bash write) and it declines to advisory rather than leaving a half-reverted tree. See failure-ladder.md. |
publish_guard |
string | Once verification goes green, intercept commands that would DISCARD that work — rm of a file in the verified diff, git reset --hard, git checkout -- <path>, git clean -f, git stash, truncating redirects: off | advisory | blocking. off (default). advisory injects a model-visible warning naming the protected paths, at most twice per run. blocking suppresses the call before dispatch and returns an error naming what it would have destroyed. Ordinary editing of a protected file is never blocked — sessions keep working after green, and blocking rewrites would nag on every edit-test cycle. No override token: advisory and off are the escape hatches. See verification-discipline.md. |
claim_gate |
string | Fire when the final answer makes a claim the run's evidence does not support — a test count or named-gate result (4954 passed, clippy clean) while the verifier saw no build/test command, or an applied/fixed/changed claim while zero files moved: off | advisory | blocking. advisory (default) injects one model-visible nudge per run asking it to correct the claim or run the check. blocking re-enters up to three times. off disables it (byte-identical to the loop without the gate). Deterministic — no LLM, so it can't be talked out of it or invent an accusation. Quoted output and claims attributed elsewhere (CI reported ...) never fire. See verification-discipline.md. |
source_gate |
string | Fire when a comment ADDED to a source file this run asserts having consulted an external source (checked the X page, per the Y docs) while no webfetch/websearch tool ran: off | advisory | blocking. off (default) — opt-in, unlike claim_gate, because it inspects the diff rather than the final answer and has a larger false-positive surface. advisory injects one nudge per run; blocking re-enters up to three times. Deterministic — no LLM. Only lines added by THIS run count (a run-start baseline diff is subtracted), and RFC citations, bug IDs, spec sections, repo file references, and URLs the code fetches at runtime are excluded. Catches a fabricated citation written into the artifact, which a final-answer scanner structurally cannot see. See verification-discipline.md. |
completeness_gate |
string | Fire when the final answer states first-person work the model still intended to do (Next I'll wire up the tests, I still need to implement the retry path) while the run is finishing: off | advisory | blocking. advisory (default) injects one model-visible nudge per run asking it to do the work or record what is left and say it is stopping; blocking re-enters up to three times; off is byte-identical to the loop without the gate. Deterministic — no LLM. Armed by default because without a critic_provider nothing else asks whether the task is finished: every other always-on gate wants a specific mechanical fault (unrun edits, a failed last call, an unsupported claim, tracked todos), so a run that edits real files, runs a real check, claims nothing false and stops halfway hits none of them. Narrow by construction — the sentence needs a first-person forward marker AND a work verb AND no second-person address, so handoffs (you'll want to run the migration), self-fulfilled intent (I'll explain below) and quoted plans never fire. Yields to the todo gate when tracked todos are outstanding. See verification-discipline.md. |
open_issues_gate |
string | Fire at finalization when this session left tracked issues open: off (default) | advisory | blocking. Opt-in, unlike the other gates, because nagging about a backlog is intrusive. advisory emits one SystemNotice; blocking re-enters the loop (bounded) so the run cannot finish until the session's issues are closed or deferred. Session-scoped — issues belonging to other sessions never surface here. |
injection_scan |
string | How the ingestion-time prompt-injection scanner treats untrusted tool results (read, MCP, websearch): off | advisory (default) | block. advisory fences a positive hit with a warning; block additionally withholds the body when two or more high-severity findings are present. |
turn_envelope |
boolean | Re-evaluate the volatile session facts (cwd, OS, shell, git branch) every turn and append them as a <turn_envelope> block, instead of freezing them into the system prompt at agent construction. Default: true. Those four facts were captured once and never refreshed, so a cd, a git switch, or a worktree move left the model reading a world that no longer existed, and the only correction was rebuilding the whole cached prefix to update four lines. Appending at the tail costs no cache churn. Measured on two models at n=6: no regression on any metric, and it removed the blow-up runs on the model that was struggling. Set false to restore the frozen preamble. |
capability_projection |
boolean | Render the prompt's tool list from the tools actually registered for the turn, minus what the active prompt's deny_tools removes, instead of a hand-written Available tools: literal. Default: true. The static list cannot see deny_tools, so plan and review mode advertised write/edit/apply_patch/bash while refusing all four — a weaker model plans against the prompt, hits a refusal, and burns turns recovering. Set false to restore the literal list. |
prompt_leak_detect |
string | Detect a model that stops answering and starts reciting its own system prompt: off | advisory | blocking. off (default) does no detection and emits nothing. advisory records it and warns, leaving the turn untouched. blocking also stops consuming the stream, keeping the answer given before the recitation and discarding the rest. Method: SimHash-64 over sliding 16-content-word windows of the output against the same windows of the prompt, tripping only on 12 consecutive near-matches — roughly 55 words reproduced verbatim. The margin is smaller than it sounds: two ordinary sentences quoted word-for-word reach a run of 6, so do not lower the threshold without re-running the fixtures in agent_loop::prompt_leak. Off by default because the failure has not yet been observed in dirge; advisory exists so it can be measured before it is trusted. |
context_target |
integer | Working-context budget in tokens (default: 250000). The compaction decision treats the effective window as min(model_window, context_target), so models with a window below 250k use their own (e.g. 128k stays 128k) while larger models are capped. Set lower (e.g. 100000) for stricter folding on cost-sensitive routes. Floored at 16k. |
compaction_fold_threshold |
float | Fraction of the (budgeted) context window (0.3–0.75) at which history folds into a summary — and the durable checkpoint is written. Lower folds/checkpoints earlier, from more coherent context. Unset keeps the 0.75 default. Composes with context_target: the fold point is fraction × min(model_window, context_target). |
incremental_checkpoint |
bool | Refresh the durable session checkpoint in the background at 20%-of-window usage thresholds, without folding, so a resumed session recovers fresh state (adapted from MiMo-Code). Default true; set false to disable the background summary calls. Forced off in headless -p/--loop (nothing there persists it). |
file_excerpt_cap_tokens |
integer | Per-result token cap for read excerpts (default: 12000). Every tool result is head+tail truncated at the turn-end cap (3000 tokens, or 1000 once context passes 60%); a file excerpt gets this roomier allowance instead, because it is the material the next edit is written against and edit_lines anchors on line hashes that only survive while the rows are intact. Raise it for a codebase of large files; set it to 3000 to hold reads to the same cap as any other tool output. Floored at 3000, and the aggressive tier still overrides it. See Large file reads. |
agents |
object | Optional user-defined agent profiles, keyed by name. Each is a { prompt, model, deny_tools/allow_tools, reasoning, temperature, description, subagent } bundle activated at runtime with /agent <name>. Lowest-precedence source — .dirge/agents/*.md and ~/.config/dirge/agents/*.md override same-named entries. Absent = no profiles (opt-in). |
max_tokens |
integer | Maximum response tokens. Default: 8192. |
max_agent_turns |
integer | Maximum agent turns per response. Default: 100. |
temperature |
number | Model sampling temperature in 0.0–2.0. --temperature CLI flag overrides this. Values outside the range are clamped with a stderr warning. |
no_tools |
boolean | Disable all tools. Default: false. |
no_context_files |
boolean | Disable loading global/project AGENTS.md and CLAUDE.md context files. Default: false. |
no_skills |
boolean | Disable on-disk skill discovery: no skill catalog in the preamble and no skill tool. Discovery normally spans ~/.claude, ~/.opencode, ~/.agents, ~/.dirge /skills plus every project ancestor, and honours neither DIRGE_CONFIG_DIR nor DIRGE_DATA_DIR — so this is the lever for reproducible runs (scripts/loop-ab.sh sets it). Default: false. |
skill_anchor_interval |
integer | Restate loaded skills' declared anchor: sections every N turn boundaries. 0 (default) is off. Preserving an anchor through a compaction happens whenever a skill declares one; this is the other half, for skills whose own spec says the anchor must RECUR. Every fire costs tokens, so the rate is yours to set. Note the interval is a FLOOR, not a schedule: the boundary chain returns one nudge and correctness nudges outrank this one, so the effective rate is lower whenever the run is also being steered. |
context_window |
integer | Advertised model context window — feeds the status line and the legacy token-reserve path (the compaction fold decision uses context_target, not this). Resolved as: providers.<name>.context_window → this key → built-in per-model table → 128000. Set it per provider when more than one is configured; this top-level key applies to all of them, so correcting one model's window here corrupts the rest. The table usually decides, e.g. Claude Sonnet/Opus 4.x, DeepSeek-V4, GPT-4.1, Gemini-2.5-Flash, and Llama-4 = 1M (Gemini-2.5-Pro = 2M); base Claude and o3 = 200k; GPT-4o, DeepSeek-R1, and older 128k models = 128k. A model neither lookup knows warns once and falls back to 128000. |
reserve_tokens |
integer | Tokens to reserve before compaction is triggered. Default: 16384. |
thinking_budget_tokens |
integer | Absolute cap on one turn's reasoning trace, in estimated tokens, enforced on dirge's side of the stream — the provider-side budget is a request, and a locally-served model honours it only as far as its template does. Crossing it truncates the trace and disables thinking for the rest of the task. Absent (default) derives a cap per turn instead. |
keep_recent_tokens |
integer | Approximate recent-token budget kept verbatim during compaction. Default: 20000. |
compact_enabled |
boolean | Enable automatic conversation compaction. Default: true. |
compact_tool_schemas |
string | Trim tool schemas to breadcrumbs on a small context window: auto (default) | on | off. auto engages at or below a 48k resolved window. Each tool's description is cut to its first sentence and each parameter's to a clause; names, types, enums and required-ness are untouched, so the model keeps everything it needs to form a well-formed call and loses only the prose about when to prefer one tool over another. No tool is dropped — a model that cannot see a tool cannot ask for it, and that failure is silent and reads as incapability. The full surface is ~16k tokens with the built-ins alone and ~32.6k with MCP servers loaded, the latter larger than a 32k window in its entirety. |
dynamic_tool_search |
boolean | Ship only tool_search + a small always-on toolset per request; the model loads more tools on demand via tool_search(query). The always-on set covers the core loop (read, write, edit, bash, grep, glob, list_dir) plus write_todo_list and task_status, so only the long tail (MCP, semantic, spec, debug) needs discovery. ~30% token savings on MCP-heavy sessions. Default: false. |
code_mode_rubric |
boolean | Append the "code mode" rubric to the system prompt: for a bulk/fan-out of similar tool calls (roughly 10+ items), write ONE bash script that returns only the distilled result instead of N per-item calls, keeping raw output out of context. Default: false. See code-mode-rubric.md for the measured effect. |
context_depth_reminder_threshold |
integer | Consecutive same-file tool calls before a single mid-turn reminder restates the active task + touched files. Opt-in; unset (default) disables. Recommended value: 8. |
phased_workflow_enabled |
boolean | Enable the /plan phased workflow (explore → plan → implement → reviewer-runs-code loop). Master kill-switch — /plan is inert unless this is true. Default: false. See agent-loop.md. |
phased_workflow_max_review_cycles |
integer | Reviewer-runs-code fix-cycle budget for /plan: how many times a NEEDS_FIX verdict re-runs the implementer before stopping. Default: 2. |
phased_workflow_plan_approval |
boolean | Pause /plan after the plan phase and prompt to approve / edit / cancel before any code is written. On edit you type feedback and the plan is regenerated for another review. Default: false (implement immediately). See agent-loop.md. |
permission |
object | Permission rules; see the permission config notes below. |
restrictive |
boolean | Select restrictive permission mode. Overridden by accept_all/yolo if those are also true. |
accept_all |
boolean | Select accept mode, equivalent to --accept-all. Overridden by yolo if true. |
yolo |
boolean | Select yolo mode, auto-approving all operations. |
sandbox |
bool / string / object | Sandbox bash commands. true/false, a mode string ("off", "bwrap", "microvm"), or an object — see Sandbox configuration. Default: false. |
default_permission_mode |
string | Permission mode when no mode boolean/CLI flag is set. Use standard, restrictive, accept, or yolo. |
show_tool_details |
boolean | Show tool-result output in the TUI. Default: true. |
show_edit_diff |
boolean | Show colorized diff output for edit tool results (- red, + green, @@ cyan). Default: true. |
animations_enabled |
boolean | Enable TUI animations (avatar face toggling, spinner repaint timer). Default: true. Set to false to reduce terminal flicker and CPU usage; the avatar freezes to a static face. |
show_reasoning |
boolean | Show the model's thinking/reasoning by default, instead of having to press Ctrl+O each turn. Default: false. |
keyboard_enhancement |
boolean | Enable the terminal's enhanced keyboard (kitty) protocol so distinct chords like Shift+Enter reach the input editor (Shift+Enter then inserts a newline via insert_newline instead of submitting). Only takes effect on terminals that advertise support (kitty, Ghostty, WezTerm, foot, rio, …); a harmless no-op elsewhere — use Alt+Enter or Ctrl+J there. Default: true. Set false to disable if your terminal misbehaves. |
desktop_notifications |
object | Optional OS-level desktop notifications. Off when absent. Set { "enabled": true } to notify on completed runs and prompts waiting for input. Backed by notify-rust on macOS, Linux, and Windows. |
max_sessions |
integer | How many of the most-recent prior sessions in the same project (same working dir) to mine for Up-arrow / Ctrl+F command history, seeded ahead of the current session's prompts. Default: 3. Set 0 to keep recall to the current session only. See Command history. |
display |
string | Preferred startup pane layout: a |/,/space-separated subset of left, main, right (e.g. "main|right", "main"). The main pane is always shown; this picks which side panels appear. Override at runtime with /display. Default: automatic (side panels shown at ≥152 cols). |
tool_result_max_chars |
integer | Hard ceiling on characters per tool result. Default: 500. Combined with tool_result_max_lines (lines applied first; chars trim what's left). |
tool_result_max_lines |
integer | Body lines shown inside a tool chamber before collapsing to ↓ N more lines (Ctrl+O to expand). Default: 4. Press Ctrl+O to re-print the most recent collapsed result in full. edit, apply_patch, question, task, and task_status are exempt (their body IS the value). |
default_prompt |
string | Prompt name to activate on startup. Default: code. |
theme |
string | UI color theme. phosphor (default — 80s CRT green-on-black), plain (pre-theme white/cyan), or any <name>.theme.json file in the config dir. See themes.md. |
tools |
object | Per-tool settings. tools.websearch / tools.webfetch (bool, default true) gate the two web tools, but only as one operand of an OR: a tool is registered when its config value is true or its env var (WEBSEARCH_ENABLED / WEBFETCH_ENABLED; truthy = true or 1) is set — so an explicit config false is re-enabled by the env var, not dropped. tools.bash_output_inline_max_bytes, tools.webfetch_output_inline_max_bytes, and tools.task_output_inline_max_bytes (integer, default 8192 each) cap how much of a bash / webfetch / task result is returned inline versus relayed to ~/.dirge/transient/<pid>/ with a head/tail summary and a read-tool hint. |
memory |
object | Long-term memory retrieval tuning. See Hybrid memory retrieval below. Absent = the builtin BM25 store. |
memory_graduation |
boolean | Recurrence-weighted salience graduation for the memory curator: detect near-duplicate entries and boost the representative's salience. Default true. Non-destructive — it never merges or deletes; the LLM curator pass still owns dedup. |
mcp_servers |
object | MCP server map when compiled with the mcp feature. When omitted, defaults to a single Exa Web Search server; see below. |
acp_servers |
object | ACP server config map when compiled with the acp feature. See the ACP section below. |
editor_open_command |
string | Opt-in editor follow-along: a command template with {path} and {line} placeholders (e.g. "zed {path}:{line}", "code --goto {path}:{line}"). When set, dirge opens files it reads or edits in this external GUI editor, detached and non-blocking — the editor "follows along" like Zed's AI panel. None (unset) disables the feature entirely. |
Desktop notifications are off unless desktop_notifications.enabled is set.
dirge uses notify-rust on macOS, Linux, and Windows: a system notification
banner on macOS (sent through the Terminal sender), a freedesktop D-Bus
notification on Linux (needs a running notification daemon), and a WinRT toast
on Windows. On macOS the banner is attributed to Terminal and on Windows to
PowerShell, since dirge doesn't register its own notification identity.
{
"desktop_notifications": {
"enabled": true,
"on_completion": true,
"on_input_required": true
}
}When enabled, omitted on_completion / on_input_required keys default to
true. on_completion fires only when a run actually returns to idle, not when
an automatic follow-up, reviewer pass, loop iteration, or queued interjection
continues immediately.
Two keys control how large the live context grows before history is folded into a summary (compaction):
context_target— the working-context budget in tokens (default250000). The decision treats the effective window asmin(model_window, context_target), so a model whose window is under the budget uses its own (128k stays 128k) while models advertising 1M+ are held to the budget rather than folding on a fraction of their full window. Context quality degrades gradually past ~100k and varies by model, so 250k is a middle ground — set it lower (e.g.100000) for smaller local models or cost-sensitive routes (see the README for the reasoning). Floored at 16k; a value above the model's real window is a no-op.compaction_fold_threshold— the fraction of that budget at which the fold (and durable checkpoint) fires, clamped to0.3–0.75(default0.75). Lower folds earlier, from more coherent context.
The fold point — the size the context reaches before compaction kicks in — is the product of the two:
fold_point = compaction_fold_threshold × min(model_window, context_target)
Examples below assume a large-window model (≥250k) so the budget is what
binds; on a smaller model the fold point is fraction × model_window instead.
| Goal | context_target |
compaction_fold_threshold |
Folds at |
|---|---|---|---|
| Default | unset (250k) | unset (0.75) | ~188k |
| Smaller, tighter context | 60000 |
unset (0.75) | ~45k |
| Same budget, fold earlier | unset (250k) | 0.5 |
~125k |
| Both | 80000 |
0.6 |
~48k |
{
"context_target": 80000,
"compaction_fold_threshold": 0.6
}Set compact_enabled to false to disable automatic compaction entirely.
(The separate context_window / reserve_tokens keys feed the status line and
the older token-reserve path; the budget above is what the fold decision uses.)
Two separate mechanisms shrink what the model sees of a file, and they have different knobs.
The turn-end result cap. Every tool result is head+tail truncated before
each send: 3000 tokens normally, 1000 once estimated context passes 60%. The
model that called the tool sees the full result on that turn; later turns see
the truncation. A read excerpt is held to file_excerpt_cap_tokens (default
12000) instead, because it is the material the next edit is written against and
edit_lines anchors on line hashes that only survive while rows are intact.
{
"file_excerpt_cap_tokens": 40000
}Raise it if you work in a codebase of large files and see the agent re-reading
the same one; set it to 3000 to hold reads to the same cap as any other tool
output. It is floored at 3000, and the aggressive (>60% context) tier still
overrides it — a roomier allowance is worth nothing if the request stops
fitting. Truncation always cuts on a line boundary, so surviving rows keep
their <n> <hash>: prefixes and stay usable as edit_lines anchors.
Prompt compression. The [compression] engine windows verbose tool
output (logs, diffs, grep dumps). It does not touch source files, file
excerpts, or your own messages. Those exemptions have overrides, but they exist
for A/B testing the change that introduced them and there is no reason to turn
them on:
| Key | Default | Effect when set |
|---|---|---|
enabled |
true |
false disables compression entirely — request bodies go out untouched. |
preset |
"dirge" |
Named profile. "safe" / "lossless" are also output-neutral; "agent", "aggressive", "auto", "rag", "code" enable lossy stages and directives that alter the model's output. |
trim_user_text |
false |
Let windowing touch your own messages too. |
window_code |
false |
Let windowing fold and window code and file excerpts. |
header |
"explicit" |
"legacy" restores the pre-0.21.10 elision-header wording. |
verbatim |
true |
false ignores read(verbatim=true)'s compression opt-out. |
To turn it off for one run without editing config, pass --no-compression or
set DIRGE_COMPRESSION=0 (off, false, no, and disabled also work).
DIRGE_COMPRESSION_PRESET overrides preset the same way. Precedence is
most-local-wins: the CLI flag beats the env var, which beats the config file.
If you need a guarantee that what you are looking at is byte-for-byte what is
on disk, call read with verbatim: true — that result is exempt from
compression entirely.
By default the memory tool's search is BM25 (keyword) only — exact on
paths, error codes, and identifiers, but blind to paraphrase. Opt into hybrid
dense+BM25 retrieval to also recover semantically-related entries, fused with
Reciprocal Rank Fusion. It needs an OpenAI-compatible embeddings endpoint.
{
"memory": {
"hybrid_retrieval": true,
"embed_url": "https://api.openai.com/v1/embeddings",
"embed_model": "text-embedding-3-small",
"embed_api_key_env": "OPENAI_API_KEY"
}
}| Key | Type | Description |
|---|---|---|
hybrid_retrieval |
boolean | Turn on dense+BM25 fusion. Default false. |
embed_url |
string | OpenAI-compatible /v1/embeddings endpoint. Required for hybrid; if unset, retrieval stays BM25. |
embed_model |
string | Embedding model id. Default text-embedding-3-small — set it when pointing at a non-OpenAI endpoint. |
embed_api_key_env |
string | Name of the env var holding the API key (the key itself is never stored in config). Omit for a keyless local endpoint. |
verbatim_pre_recall |
boolean | Each turn, auto-search memory on the verbatim user message and inject the hits as a supplemental context note (separate from the frozen system-prompt snapshot — it never changes the cached prefix). Surfaces relevant memory the agent wouldn't think to look up. Works with BM25 or hybrid. Default false. |
confirm_writes |
boolean | Require human confirmation before any memory add is stored. See Confirming memory writes. Default false. |
dirge decides what is worth remembering on its own. The agent writes mid-session whenever it judges something memorable, and after an idle session the background review and memory curator — forked LLM runners — write more. All of it lands in the system prompt of every later session in the project, and under global scope, of every project.
confirm_writes puts a human in that loop:
{ "memory": { "confirm_writes": true } }An add is then queued rather than stored. A queued entry is inert — it is not
in the prompt snapshot, not in memory view, and not in memory search — and the
model is told plainly that it is not yet stored, so it does not treat the fact as
durable. Review them with:
/memory review
which opens the queue in $EDITOR. The file is the desired final state: delete
a block to reject it, edit the text to reword it, or type a new block to record
something the model never noticed. Saving stores exactly what is in the file;
quitting without saving (:cq) leaves the queue untouched for next time. Accepted
entries go in through the normal write path, so a memory you typed is
indistinguishable from one the agent proposed.
Only add is gated. replace, supersede and remove act on entries you already
accepted, and supersede usually fires because you just corrected the agent —
asking you to confirm your own correction would be noise.
A notice appears when the queue is non-empty, both after the post-session passes
and once at startup. Headless -p has no one to ask, so entries simply queue there
and wait for your next interactive session; nothing is auto-accepted and nothing is
lost.
Safe by default and on failure: with hybrid_retrieval off, or the endpoint
unset/unreachable/timed out, search silently falls back to BM25 — it never
errors. Embeddings are computed at search time (the first search of a session
embeds all active entries; later searches only embed the query) and cached for
the session.
Cost note: enabling hybrid_retrieval and verbatim_pre_recall together
means roughly one embeddings API call per agent turn (pre-recall searches the
verbatim message every turn, and with hybrid each search embeds the query). On
a paid endpoint that adds up over a long session; on a local endpoint it's just
latency.
Providers are declared once in the providers map and referenced by alias from
the role-assignment keys — so each role can run on a different model:
{
"provider": "deepseek",
"review_provider": "glm",
"escalation_provider": "anthropic",
"subagent_provider": "glm",
"providers": {
"deepseek": {
"model": "deepseek-v4-pro"
},
"glm": {
"model": "glm-4.6"
},
"anthropic": {
"model": "claude-opus-4-5"
},
"ollama": {
"provider_type": "openai",
"base_url": "http://127.0.0.1:11434/v1",
"model": "llama3.1"
}
}
}Each providers entry accepts:
| Field | Description |
|---|---|
provider_type |
Built-in backend to use: openrouter, openai, openai-responses, anthropic, gemini, deepseek, glm, cerebras, opencode, kimi, ollama, or custom. Optional — defaults to the entry's alias when that alias matches a built-in name. openai speaks the Chat Completions API (/v1/chat/completions); openai-responses speaks the Responses API (/v1/responses) — see below. |
base_url |
Endpoint base URL (for custom / self-hosted endpoints). |
model |
Model name for this provider. |
api_key |
Literal key or ${ENV_VAR} interpolation. Takes precedence over api_key_env. |
api_key_env |
Name of the env var holding the API key. |
auth |
Authentication mode: api-key (default), chatgpt for Codex/OpenAI login tokens, anthropic / claude-code for Anthropic Claude Code OAuth, or kimi for Kimi Code (Moonshot) device OAuth. |
allow_insecure |
Allow http:// URLs (plaintext). Default false; only enable for local-only proxies. |
context_window |
Context window for this provider's model, in tokens. Takes precedence over the top-level context_window and the built-in model table, and is the key to reach for when more than one provider is configured — the top-level one applies to all of them. Every compaction threshold divides by this number, so a wrong one folds a context with room to spare or fails to fold one without. Needed for a self-hosted or locally-served model, whose name is often a file path the model table cannot match. |
stream_chunk_timeout_secs |
Per-provider streaming chunk timeout override. |
effort |
Default reasoning effort for this provider's model: off, minimal, low, medium, high, xhigh, or max. xhigh and max are distinct tiers on OpenAI and Anthropic (xhigh for long-horizon agentic work, max for unconstrained capability); providers that lack an xhigh tier fold xhigh up to max (GLM-5.3 takes low/high/max; DeepSeek has no xhigh but does accept medium) or to high (Cerebras takes low/medium/high). Providers that don't accept a given level collapse it to the nearest one they do — e.g. GLM-5.3 takes only low/high/max, so medium/minimal go out as high/low. A /effort session override takes precedence over this. Unrecognized values are warned and ignored. |
multimodal |
Override for whether this provider/model accepts image input (gates the Ctrl+V image-paste UX). true/false forces it either way; omit to auto-detect from the model name and provider type. Set true to enable pasting into a local vision model (e.g. Ollama llama3.2-vision) behind a generic provider type. |
headers |
Extra HTTP request headers. A value that is exactly ${ENV_VAR} is replaced by that environment variable (an unset variable is a hard error); any other value, including one with ${…} embedded in surrounding text, is sent literally. |
options |
Free-form per-provider model options; currently honors temperature. |
The aliases on the left of the map become the values you write in role-assignment keys.
provider_type: "openai" sends requests to the Chat Completions endpoint
(/v1/chat/completions). Some OpenAI-compatible endpoints only expose the
newer Responses API (/v1/responses) — e.g. an OAuth proxy that mirrors what
OpenAI itself serves for GPT-5+. Set provider_type: "openai-responses" to
target that endpoint instead:
{
"providers": {
"gpt5-proxy": {
"provider_type": "openai-responses",
"model": "gpt-5.6",
"base_url": "http://127.0.0.1:8639/v1",
"api_key": "${MY_PROXY_KEY}",
"allow_insecure": true
}
}
}It behaves like openai in every other respect (default model, API-key auth,
base_url handling) — only the request shape and endpoint differ. The api key
is sent as a bearer token; unlike auth: chatgpt this uses no OAuth/Codex
login, so it works against any plain /v1/responses server. (allow_insecure
is honored here since no OAuth bearer is involved.)
Copilot has no built-in provider_type; it does not need one. Its endpoint is
OpenAI-compatible, so it is reached with the generic openai /
openai-responses types and a base_url of https://api.githubcopilot.com.
Get a token from the GitHub CLI:
gh auth login
export GH_API_KEY="$(gh auth token)"Then point one or more provider entries at Copilot. Different Copilot models
sit behind different APIs, so the provider_type is per model — the Responses
API for the ones that require it, Chat Completions for the rest:
{
"provider": "grok45",
"critic_provider": "luna56",
"summarization_provider": "luna56",
"approval_provider": "luna56",
"providers": {
"grok45": {
"provider_type": "openai-responses",
"base_url": "https://api.githubcopilot.com",
"api_key": "${GH_API_KEY}",
"model": "grok-4.5",
"multimodal": true
},
"luna56": {
"provider_type": "openai",
"base_url": "https://api.githubcopilot.com",
"api_key": "${GH_API_KEY}",
"model": "gpt-5.6-luna",
"multimodal": true
}
}
}api_key is expanded at use time, so the token stays in the environment rather
than in the config file. Which models you can select depends on your Copilot
plan, and gh auth token returns a token tied to your gh login — re-export
it after re-authenticating.
Recipe contributed by @dubchord, verified against Copilot Enterprise (GH #698).
Cerebras needs no providers entry. Export its API key and select the built-in:
export CEREBRAS_API_KEY="..."
dirge --provider cerebras # defaults to gemma-4-31bThe built-in sends OpenAI-compatible Chat Completions requests to
https://api.cerebras.ai/v1. To pin another Cerebras model, add only the
model override; keep the secret in CEREBRAS_API_KEY:
{
"provider": "cerebras",
"providers": {
"cerebras": {
"model": "gpt-oss-120b"
}
}
}Set providers.cerebras.base_url to an HTTPS proxy or custom Cerebras
endpoint. Dirge does not read a separate CEREBRAS_BASE_URL variable:
{
"provider": "cerebras",
"providers": {
"cerebras": {
"model": "gemma-4-31b",
"base_url": "https://cerebras-proxy.example.com/v1"
}
}
}Reasoning and image support depend on the model. For gemma-4-31b, Dirge
sends top-level reasoning_effort values (low, medium, or high) and
accepts image input. It clamps lower and higher Dirge reasoning levels to that
three-value set and omits the field when reasoning is off. Dirge treats
gpt-oss-120b and zai-glm-4.7 as text-only. This integration does not expose
other Cerebras-specific request options.
Run dirge auth openai to authorize OpenAI through the browser OAuth flow and
persist a local OAuth refresh token. Dirge prints an OpenAI authorization URL
and waits for the browser redirect on http://localhost:1455/auth/callback.
This is the preferred ChatGPT/Codex subscription login path.
For headless environments, run dirge auth openai --device-code to use the
older device-code flow. Before using that mode, enable device-code auth in
ChatGPT Codex security settings. Dirge prints the OpenAI verification URL and
user code; the user code is part of the interactive login UX, but you should
not share it with anyone.
The credential store lives in the Dirge data directory, not the repository or program directory:
- Linux default:
~/.local/share/dirge/auth.json - Override:
$DIRGE_DATA_DIR/auth.json
Successful login persists across Dirge sessions. Delete auth.json or revoke
the OpenAI authorization if you want to force a new login.
For the canonical openai provider with no configured base_url, a fresh
stored OAuth credential is treated as subscription auth and is preferred before
API-key billing. Explicit auth: "chatgpt" also uses this fresh Dirge-managed
OpenAI OAuth credential before falling back to legacy ~/.codex/auth.json
storage, so rerunning dirge auth openai is enough to recover from a stale
Codex login file. OpenAI-compatible aliases and providers with a custom
base_url keep normal API-key behavior. If no fresh OAuth credential exists,
Dirge uses the usual API-key sources: explicit CLI keys, key files/stdin, config
api_key, config api_key_env, and provider environment variables. If the
OAuth/Codex request reports subscription quota or model-access exhaustion, Dirge
asks before switching that request to API-key billing.
Troubleshooting:
- Browser callback port is busy: stop the process using port 1455 and rerun
dirge auth openai, or usedirge auth openai --device-codein a headless environment. OpenAI device-code auth is not enabledor a 404 from the user-code endpoint: enable device-code auth in ChatGPT Codex security settings and rerundirge auth openai --device-code.- Timeout: complete approval in the browser and rerun the command.
- Corrupt auth store: fix or remove
auth.json, then rerundirge auth openai.
To use a Claude Pro/Max subscription token instead of an Anthropic API key, run:
dirge auth anthropicComplete the browser login. dirge listens on http://localhost:53692/callback,
exchanges the PKCE code, and writes credentials to
~/.claude/.credentials.json in the same shape as Claude Code. Then configure
the Anthropic provider to use OAuth:
{
"provider": "anthropic",
"providers": {
"anthropic": {
"auth": "anthropic",
"model": "claude-sonnet-4-5"
}
}
}Aliases claude-code, claude_code, and claude are accepted for the same
auth mode. ANTHROPIC_OAUTH_TOKEN can also provide a raw access token for
smoke tests, but persisted credentials are preferred because dirge can refresh
expired tokens before rebuilding the Anthropic client.
To use a Kimi membership's managed coding API instead of a raw API key, run:
dirge auth kimi # alias: dirge auth kimi-codeThis is a device-code flow (there is no browser/localhost variant): Dirge
prints a verification URL and user code, you authorize in the browser, and
Dirge polls until the login completes. The credential is stored under the
kimi key in the same $DIRGE_DATA_DIR/auth.json used by the other OAuth
logins.
Then select the provider (a stored login is also auto-detected when no provider/env key is configured):
{
"provider": "kimi",
"providers": {
"kimi": {
"model": "k3"
}
}
}Models: k3 (default), kimi-for-coding, and kimi-for-coding-highspeed,
all OpenAI-compatible chat completions against
https://api.kimi.com/coding/v1 with a 262144-token context window. Lower
membership tiers without K3 access should use --model kimi-for-coding.
Kimi access tokens live only 15 minutes; Dirge refreshes them automatically (before client construction and mid-session) and persists the rotated token bundle, so a long session does not die on token expiry.
Environment overrides:
KIMI_CODE_API_KEY— static bearer used instead of the OAuth login (not refreshable).KIMI_CODE_BASE_URL— API base URL override (https only).KIMI_CODE_OAUTH_HOST/KIMI_OAUTH_HOST— OAuth host override (defaulthttps://auth.kimi.com).
Aliases kimi-code and moonshot are accepted for the provider name and
the auth mode. The separate Kimi Platform API (api.moonshot.ai) is not
this provider — use a custom provider with an API key for that.
dirge asks each provider to cache the stable part of every request (the system
prompt and tool definitions, plus the conversation history where the provider
supports it) so it is billed once instead of on every turn. Cached input reads
at roughly a tenth of the normal rate, which makes the cache hit ratio the main
cost lever on a long session. /cache reports that ratio along with the
cumulative read and write totals.
Nothing needs configuring for this to work. Most providers cache the longest matching prefix by themselves; where an explicit breakpoint is required (Anthropic directly, and the Anthropic, Qwen and Gemini routes through OpenRouter) dirge places one.
The one knob is how long an Anthropic cache entry lives:
{
"prompt_cache": { "ttl": "1h" }
}"1h" (the default) or "5m". The tradeoff is in the write price: a 1h write
costs 2x the base input rate against 1.25x for 5m, while reads are a tenth
either way. A read refreshes a 5m entry, so a session that keeps working stays
warm on 5m and never pays the premium. What 1h buys is the idle gap: if more
than five minutes pass between turns, a lapsed 5m entry means re-writing the
whole prefix on the next turn rather than just that turn's delta. Pick "5m"
if your sessions run continuously, keep "1h" if you leave dirge sitting while
you read or work elsewhere. DIRGE_PROMPT_CACHE_TTL overrides the setting at
runtime.
| Key | Used for | Falls back to |
|---|---|---|
provider |
Default / main loop | (none — required) |
review_provider |
Background session-review pass | provider |
escalation_provider |
One-shot retry after repair-exhaustion / pre-write syntax failure | provider (no-op when equal) |
summarization_provider |
Context compaction side calls (required for LLM compaction when provider uses Anthropic OAuth) |
provider when safe |
subagent_provider |
task tool subagents |
provider |
critic_provider |
F6 in-loop critic (tier 3) + diff-aware code reviewer (/code-review) + goal-gate judge (--goal) |
none (off) |
When a role's provider equals provider (either explicitly or by fallback), no
duplicate client is constructed and the feature has zero overhead — escalation
routes, for example, simply don't fire because they'd be a no-op anyway.
Migration note: dirge no longer reads the legacy top-level
model,custom_providers, orreview_modelkeys — starting a session with any of those at the root fails fast with a migration hint. Movemodelinside the active provider's entry,custom_providers.<name>entries directly intoproviders, andreview_modelinto the entry referenced byreview_provider.
Permission actions are lowercase strings: allow, ask, or deny. rules
is an ordered list read top-to-bottom; last match wins. Each rule has:
op— the operation class it governs (NOT a tool name). One of:read,edit,execute,network,mcp,memory,skill,agent,meta, or*(any).editcovers write/edit/apply_patch — they're one operation, so one rule governs all three.match— a glob. Read/edit use path-style globs (*is one path segment,**spans directories); execute/network/mcp use shell-style (*matches anything including/, trailing*makes args optional). The*(any) op uses shell-style too, since it can match commands and MCP keys as well as paths. MCP patterns match the full keymcp_tool:{server}:{tool}.effect—allow,ask, ordeny.tool(optional) — narrow the rule to a single concrete tool name (e.g."grep") instead of the whole op.
Use "*" for the default action, external_directory (also a rules
list, op defaults to *) for absolute-path rules outside the working
directory, and doom_loop for the retry-loop hard-deny (set to allow
to disable it). dirge always installs its built-in safe bash allow/deny
rules and a read-only/memory/skill/in-cwd-write allow set beneath your
rules; your rules override them.
MCP tools default to ask for ALL servers — they execute external code
(the server's implementation, plus whatever filesystem / network / API
effects it has), and silent default-allow let entire query sequences run
before any prompt fired. To re-enable silent allow for a trusted server:
{
"permission": {
"rules": [
{ "op": "mcp", "match": "mcp_tool:lattice:*", "effect": "allow" }
]
}
}Or accept once at the alert and pick "allow always" for the same session-allowlist effect.
standard(default): every rule inpermissionis consulted; tools without matching rules fall back to*(defaultallow).restrictive: likestandard, but any tool whose rule resolves toallowvia the*fallback (no explicit allow rule matched) is converted toask. Explicitallowrules still allow. Explicitdenyrules still deny.accept(equivalent to--accept-all): auto-allows tools whose targets resolve inside the working directory; tools touching paths outside still consultexternal_directoryrules.yolo(equivalent to--yolo): bypasses every check. Use with caution.
CLI precedence (high → low): --yolo > --accept-all > --restrictive >
default_permission_mode config > standard.
When compiled with MCP support, mcp_servers accepts command-based and URL-based
servers:
{
"mcp_servers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
"env": {}
},
"semantic-index": {
"command": "my-indexer",
"args": ["--repo", "/work/other-project"],
"allow_external_paths": true
},
"remote-search": {
"url": "https://example.com/mcp",
"headers": {
"authorization": "Bearer token"
}
}
}
}If mcp_servers is omitted (null) and the mcp feature is enabled, dirge
adds a default Exa Web Search MCP server at https://mcp.exa.ai/mcp with the
x-api-key header set to EXA_API_KEY when that environment variable is set.
Set "mcp_servers": {} to disable all MCP servers.
By default an MCP tool call whose JSON arguments name a path resolving outside
the working directory is refused with a clear error — matching the trust model
of dirge's built-in file tools (read / write / edit anchored to cwd).
The check scans top-level args fields named path, file_path, file,
directory, dir, cwd, and the paths array.
Some MCP servers legitimately need broader scope: a semantic indexer pointed
at a sibling repo, a project-wide search tool, a backup utility. Set
"allow_external_paths": true on that one server's config (both Command and
Url variants accept it; default false) to skip the cwd guard for tools
from THAT server only.
The flag is path-scoped and narrow:
- It only bypasses the cwd-external-path check.
- It does NOT bypass
mcp_tooldeny rules, promptdeny_toolsfrontmatter, doom-loop detection, the sandbox, or--yolo/--restrictivemode logic — every other gate runs unchanged. - It applies per-server: enabling it on
semantic-indexdoes not affectfilesystemor any other server in the same config.
Pair it with a tight mcp_tool rule for layered control, e.g.:
{
"mcp_servers": {
"semantic-index": {
"command": "indexer",
"allow_external_paths": true
}
},
"permission": {
"rules": [
{ "op": "mcp", "match": "mcp_tool:semantic-index:*", "effect": "allow" },
{ "op": "mcp", "match": "mcp_tool:semantic-index:write_file", "effect": "deny" }
]
}
}Per-prompt deny_tools frontmatter (see "Prompt restrictions" below) applies
to MCP tools too. The deny gate matches against three names for each MCP tool
call:
- the raw tool name as exported by the MCP server (e.g.
edit,write_file), - the qualified
mcp_tool:<server>:<name>, - the umbrella
mcp_tool(denies every MCP tool from every server).
So a plan-mode prompt that ships deny_tools: [edit, write, apply_patch, bash]
also blocks any MCP server that exports a tool named edit / write /
apply_patch / bash. Use mcp_tool as a blanket deny when in doubt about
what an MCP server might expose.
The Janet plugin system runs INSIDE the trust boundary. Plugin hooks
(on-tool-start, on-tool-end) can mutate tool inputs, block tool calls,
and replace tool outputs with arbitrary text. They cannot, however, bypass
the permission checker (check_perm* runs inside the inner tool, after the
plugin pre-hook). If you load third-party plugins, treat them with the same
care you'd give to executing third-party code in your shell — the plugin's
trust level effectively equals the user's. There is no sandboxing.
Plugins are discovered from ~/.config/dirge/plugins/ and
./.dirge/plugins/ and load automatically. The optional plugins object
toggles them by name — the directory name (multi-file plugins) or the
.janet file stem (single-file plugins):
{
"plugins": {
"backpressured": { "enabled": true, "auto_start": true },
"nrepl": { "enabled": false }
}
}| Field | Default | Effect |
|---|---|---|
enabled |
true |
Whether to load the plugin. false skips it entirely. |
auto_start |
false |
Passed to the plugin via harness/plugin-config; a plugin that supports it self-engages at startup (e.g. backpressured runs its loop without the keyword). |
A plugin with no entry — or no plugins block at all — is enabled and not
auto-started, so existing setups load every plugin exactly as before.
Plugin authors: read your own settings in load-time code with
(harness/plugin-config), which returns @{:enabled bool :auto-start bool}
(or nil). The host sets it just before your files load and clears it
after, so capture it at the top level — not from a shared hook, where it
would reflect the last plugin loaded.
The sandbox key accepts three forms:
bwrapruns each bash command inside bubblewrap (Linux only; needs thebwrapbinary). The working directory is bound read-write; the rest of the filesystem is read-only.microvmruns commands in a full microVM (requires thesandbox-microvmbuild feature).image,cpus, andmemory_mibapply only to this mode.- A legacy nested form
{"mode": "microvm", "microvm": {"image", "cpus", "memory_mib"}}is still accepted; out-of-rangecpus/memory_mibare now a config error rather than silently wrapping. - A top-level
microvm_imagekey (e.g."local://dirge-microvm:alpine") is deprecated but still honoured as a fallback. Prefersandbox.imageabove.
dirge applies a per-chunk read deadline to streaming LLM responses so a
silently-dropped TCP connection (which reqwest can't always detect) doesn't
freeze the agent. The default is 5 minutes (300s) — well above any
legitimate reasoning gap from Claude 3.7 extended thinking, GPT-5 thinking,
or large-tool-output processing. Bump it if you see false-positive
stream chunk timed out errors in the middle of a turn.
Resolution order (first hit wins):
providers.<name>.stream_chunk_timeout_secs— per-provider override- top-level
stream_chunk_timeout_secs— applies to every provider 300sdefault
Provider name matching is case-insensitive (anthropic matches
--provider Anthropic).
{
"stream_chunk_timeout_secs": 300,
"providers": {
"anthropic": { "stream_chunk_timeout_secs": 900 },
"ollama": { "stream_chunk_timeout_secs": 60 },
"my-vllm": {
"provider_type": "openai",
"base_url": "http://localhost:8000/v1",
"api_key_env": "VLLM_API_KEY",
"stream_chunk_timeout_secs": 1200
}
}
}Every other per-operation timeout is named in one place — the timeouts
block — and installed process-wide at startup. Each field is in seconds;
omitted fields keep their built-in default. (The streaming chunk timeout
above is the one exception with richer per-provider precedence;
timeouts.stream_chunk_secs acts as its global fallback.)
| Field | Default | What it bounds |
|---|---|---|
stream_chunk_secs |
300 | Per-chunk read deadline for a streaming LLM response (fallback for the per-provider key above) |
request_establish_secs |
300 | Deadline for establishing a streaming request — the connection/handshake and the wait for the first response event. Distinct from stream_chunk_secs, which only guards gaps between chunks once the stream is live; a connection that stalls during the handshake never produces a first chunk. A timeout here is retried automatically. Generous by default so it only fires on a genuine stall; lower it for faster failure or raise it if a provider legitimately delays the first response. |
tool_call_gap_secs |
60 | Stall window while a tool call is mid-assembly in the stream. A timeout here is retried automatically when no response text has been emitted yet (the partial tool call is discarded and the request restarted); if text was already shown, the partial is kept to avoid a duplicated response. Raise it only if your provider legitimately pauses longer than 60s between tool-call deltas. |
mcp_call_secs |
120 | Total budget for one MCP tool call, including reconnect + retry |
mcp_init_secs |
10 | MCP server initialize handshake |
lsp_request_secs |
30 | Any non-initialize LSP request |
lsp_initialize_secs |
45 | LSP initialize handshake |
bash_secs |
120 | Default bash tool timeout when the call omits one. A DEFAULT, not a bound — the model may pass its own timeout and that wins, up to bash_max_secs |
bash_max_secs |
3600 | Ceiling on a foreground bash timeout, including one the model asked for. Kept separate from bash_secs because raising the timeout for a genuinely long command (a full test suite) is correct and clamping every request to the default would break it; this only has to sit above anything real. Lower it to hold commands shorter. Foreground only — a backgrounded shell with no timeout still runs until it exits or is killed |
{
"timeouts": {
"mcp_call_secs": 60,
"lsp_initialize_secs": 90,
"bash_secs": 300
}
}VSCode-style overrides. keybindings is an array of
{ "key": "<chord>", "command": "<command>" }; each entry layers over the
built-in defaults, so you only list what you want to change. One array
covers BOTH the global "command" keys (scroll, chat nav, …) and the
input-editor keys (cursor motion, kill-ring, history, …) — each entry
routes to the right one by its command name.
{
"keybindings": [
{ "key": "ctrl-t", "command": "toggle_reasoning" },
{ "key": "ctrl-shift-k", "command": "kill_subagent" },
{ "key": "ctrl-r", "command": "none" },
{ "key": "alt-a", "command": "cursor_line_start" },
{ "key": "ctrl-x ctrl-s", "command": "scroll_to_top" }
]
}key— a chord, or a whitespace-separated sequence of chords for an emacs-style binding (e.g.ctrl-x ctrl-s). A chord is case-insensitive,-or+separated, modifiers before the key. Modifiers:ctrl,alt(a.k.a.meta/option),shift. Keys: a single character,f1–f12, or a named key (enter,esc,tab,backspace,delete,insert,space,up/down/left/right,home,end,pageup/pgup,pagedown/pgdn). Examples:ctrl-t,pageup,ctrl-shift-x,f5,ctrl-x ctrl-s.command— one of the global or input commands below, ornone(alsounbind) to disable the default binding on that chord (clears it from both contexts).- Binding a command to a new chord adds it (the default chord still works unless you separately unbind it). Binding a chord that already has a default replaces it.
| Command | Default | Action |
|---|---|---|
toggle_reasoning |
ctrl-r |
Show/hide reasoning tokens |
expand |
ctrl-o |
Expand buffered thinking / reprint last collapsed tool result |
scroll_page_up |
pageup |
Scroll chat up one page |
scroll_page_down |
pagedown |
Scroll chat down one page |
scroll_to_top |
ctrl-home |
Jump to top of chat |
scroll_to_bottom |
ctrl-end |
Jump to bottom of chat |
next_chat |
ctrl-n |
Next subagent chat window |
prev_chat |
ctrl-p |
Previous subagent chat window |
close_chat |
ctrl-x |
Close the active chat window |
kill_subagent |
ctrl-k |
Kill the focused subagent |
drop_queue |
alt-x |
Drop queued interjections (without cancelling the run) |
cycle_prompt |
shift-tab |
Cycle the active prompt layer to the next available prompt |
| Command | Default | Action |
|---|---|---|
cursor_line_start |
ctrl-a, home |
Cursor to start of line |
cursor_line_end |
ctrl-e, end |
Cursor to end of line |
cursor_left |
ctrl-b, left |
Cursor one character left |
cursor_right |
right |
Cursor one character right |
word_left |
alt-b, alt-left |
Cursor one word left |
word_right |
alt-f, alt-right |
Cursor one word right |
delete_char_back |
ctrl-h |
Delete character before cursor |
delete_char_forward |
ctrl-d |
Delete character at cursor (forward) |
kill_to_line_end |
ctrl-k |
Kill to end of line |
kill_to_line_start |
ctrl-u |
Kill to start of line |
kill_word_back |
ctrl-w |
Kill word before cursor |
delete_word_back |
alt-backspace |
Delete word before cursor |
delete_word_forward |
alt-d |
Delete word after cursor |
yank |
ctrl-y |
Paste from the kill-ring |
yank_pop |
alt-y |
Cycle the kill-ring at the last yank |
history_prev |
ctrl-p |
Previous history entry |
history_next |
ctrl-n |
Next history entry |
reverse_search |
ctrl-f |
Reverse-i-search over history |
line_up |
up |
Up one line (then history) |
line_down |
down |
Down one line (then history) |
undo |
ctrl-z |
Undo the last edit |
external_editor |
ctrl-g |
Open the current buffer in $EDITOR |
insert_newline |
shift-enter, alt-enter, ctrl-j |
Add a line instead of submitting |
insert_newline is how you write a multi-line prompt. shift-enter only
reaches dirge on terminals that report it distinctly (see the
keyboard_enhancement option above), while alt-enter and ctrl-j work
everywhere. Plain enter always submits and is not rebindable.
Some chords serve both contexts (e.g. ctrl-k is kill_subagent and
kill_to_line_end, ctrl-n is next_chat and history_next). The
global command only fires in its situation — kill_subagent only when the
input box is empty, chat nav only with more than one chat window — so the
editor handler gets the key the rest of the time.
A key may be a sequence like ctrl-x ctrl-s. After the first chord the
footer shows the pending prefix (ctrl-x-) and waits; the next chord
completes (or aborts) the sequence. Esc or Ctrl+G cancels a
pending prefix. By default a pending prefix waits indefinitely (emacs
style); set "chord_timeout_ms": <n> at the top level of the config to
auto-cancel it after n milliseconds of inactivity. Sequences fire for
global commands only; a sequence bound to an input command is rejected
with a startup warning. Binding a sequence whose first chord is also a
single-key command disables that single-key binding (the sequence wins) —
you'll see a warning.
Notes:
- Always fixed (never rebindable): the cancel/interrupt gesture Ctrl+C / Esc (the panic button) and intrinsic editing — typing a character, Backspace, Delete, Enter to submit, Ctrl+J (insert newline), and Tab completion. Binding a global command to one of these chords shadows the intrinsic behavior while active.
- Plugins can also add and override bindings; user config always wins over a plugin. See plugins.md.
- Unrecognized chords or unknown commands are skipped with a warning on startup; the rest of the config still loads.
Pressing Up in the input box recalls previous prompts, and Ctrl+F
opens a reverse-i-search over them. By default the recall pool is the
current session's prompts. Set top-level max_sessions to an integer
N (default 3) to additionally mine the N most-recent prior
sessions in the same project (matching working_dir) for their user
prompts. Those older prompts are seeded ahead of the current session's
own, so Up starts from your newest command and walks back through earlier
conversations in the project.
The scan is scoped to the same project and excludes the current
conversation's own compaction-fold rotations, so a fold doesn't double its
prompts into history. Set "max_sessions": 0 to keep recall limited to
the current session. Synthetic turns (system-reminder wrappers, mid-turn
steering, auto-continue markers) never enter history.
Rename a built-in slash command, or give it a short alias, with the
top-level slash_aliases map. The key is what you type (with or without a
leading /); the value is the built-in command it runs (again with or
without a leading /). Arguments you type after the alias are passed
through to the target.
{
"slash_aliases": {
"exit": "quit",
"q": "/quit",
"cls": "/clear"
}
}With the above, /exit, /q, and /cls all run /quit or /clear. The
alias is resolved once before dispatch, so it inherits its target's
behavior (e.g. an alias for /quit works while the agent is running).
- Aliases don't replace the built-in — both names work unless they collide (an alias key that matches a built-in shadows it).
- A leading
/on either side is optional and normalized. - A target that isn't a known built-in produces a startup warning (likely a typo) but is still passed through — it may resolve to a plugin command. Plugin-command targets can't be validated ahead of time.
- An empty alias key is ignored (with a warning); it would otherwise make
a bare
/run the target. - Configured aliases are listed under
slash aliasesin/help.
| Variable | Purpose |
|---|---|
EXA_API_KEY |
API key for the built-in websearch tool and the default Exa MCP server. Without this the websearch tool emits a startup warning and is not registered. |
DIRGE_WEBFETCH_ALLOW_PRIVATE |
Set to 1 (or any non-empty value) to allow webfetch to call private / loopback IPs. By default webfetch enforces SSRF protection — it refuses localhost, 127.x, 10.x, 172.16-31.x, 192.168.x, and link-local addresses. Override only in trusted local-dev contexts; never set this in production environments that touch attacker-influenced URLs. |
WEBSEARCH_ENABLED / WEBFETCH_ENABLED |
Force-enable the corresponding tool when not enabled via tools.* config. Useful in container builds where you set the toggle once via env rather than per-config-file. |
DIRGE_PROMPT_CACHE_TTL |
5m or 1h, overriding [prompt_cache] ttl. See Prompt caching for the cost tradeoff. |
When compiled with the lsp feature (default-on), dirge spawns language
servers on demand to surface compile errors in tool output. The lsp config
key accepts three forms:
// Default-on, built-in commands for rust/typescript/pyright/clojure-lsp.
{ "lsp": true }
// Off entirely. Same as the --no-lsp CLI flag.
{ "lsp": false }
// Default-on with per-server overrides.
{
"lsp": {
"rust": {
"command": ["rust-analyzer"],
"env": { "RA_LOG": "rust_analyzer=debug" },
"initialization": { "cargo": { "buildScripts": { "enable": true } } }
},
"typescript": { "disabled": true }
}
}Per-server fields (all optional):
| Field | Type | Description |
|---|---|---|
command |
string[] | argv to launch the server. Replaces the built-in default. |
extensions |
string[] | Replaces the server's built-in extension list. |
extend_extensions |
string[] | Appends to the built-in list (deduped). e.g. route .janet to clojure-lsp without re-listing clj/cljs/cljc/edn/bb. Accepts extendExtensions too. |
env |
object | extra env vars for the child process. |
initialization |
object | sent as initializationOptions in the LSP initialize request. |
disabled |
boolean | true removes the server entirely. |
Example — make clojure-lsp also handle Janet files (keeps the built-in Clojure extensions):
{ "lsp": { "clojure-lsp": { "extend_extensions": ["janet"] } } }CLI flag: --no-lsp (overrides the config; same effect as lsp: false).
| Server id | Default command |
|---|---|
rust |
rust-analyzer |
typescript |
typescript-language-server --stdio |
pyright |
pyright-langserver --stdio |
clojure-lsp |
clojure-lsp |
gopls |
gopls |
jdtls |
jdtls |
clangd |
clangd |
ruby-lsp |
ruby-lsp |
bash-language-server |
bash-language-server start |
dafny |
dafny server --verify-on change |
swift |
sourcekit-lsp |
cmake |
cmake-language-server |
mojo |
mojo-lsp-server |
Servers are spawned lazily on first file touch and cached per (workspace_root, server_id) pair. Concurrent agent tool calls for the same file deduplicate so dirge never races two rust-analyzer processes against one workspace.
- The
extensionsoverride is currently ignored. The claimed-extensions list lives in the staticbuiltin_servers()registry atsrc/lsp/server.rs. Adding new extensions today requires editing that file. Follow-up.
When compiled with the acp feature, dirge can act as an ACP agent server.
The following config keys are available:
| Key | Type | Description |
|---|---|---|
acp_servers |
object | Named ACP server configurations (see below) |
dirge's ACP runs over stdio only; the acp_host / acp_port
keys that earlier docs mentioned have been removed from the CLI
and config in favor of editors driving the agent via stdio.
ACP server configs (in acp_servers) support two transport types:
{
"acp_servers": {
"tcp-server": {
"host": "127.0.0.1",
"port": 7243,
"api_key": "optional-key"
}
}
}When --acp is passed without --acp-host, dirge runs in stdio mode
(the editor spawns it as a subprocess). With --acp-host, it listens on TCP.