Skip to content

Configuration

Configuration is handled by Annihilation.Config (lib/annihilation/config.ex). It loads TOML files, merges them with built-in defaults, and returns a flat map with atom keys.

# Auto-detect project root and load config
config = Annihilation.Config.load()
# Or specify the project root explicitly
config = Annihilation.Config.load("/path/to/project")
# Access built-in defaults
defaults = Annihilation.Config.defaults()

Annihilation.Config.detect_project_root/1 walks up the directory tree from the current working directory (or a specified start directory) looking for markers in this order:

  1. A directory containing .annihilation/ — this is the preferred marker
  2. A directory containing .git/ — fallback for repos without .annihilation/
  3. If neither is found at /, falls back to File.cwd!()
# Auto-detect from cwd
root = Annihilation.Config.detect_project_root()
# Detect from a specific starting point
root = Annihilation.Config.detect_project_root("/some/nested/dir")
LocationPurposePrecedence
Built-in defaultsHardcoded in @defaultsLowest
~/.annihilation/config.tomlGlobal user settingsMiddle
$PROJECT_ROOT/.annihilation/config.tomlProject overridesHighest

Project-local settings override global settings, which override built-in defaults. The merge is a shallow Map.merge/2 of the flattened TOML sections.

These defaults are defined in Annihilation.Config and are used when no TOML config overrides them:

%{
project_dir: ".annihilation",
db_path: "beads.db",
search_db_path: "search.db",
sessions_dir: "sessions",
question_timeout: 120_000, # 2 minutes before drifting
auto_create_correction_beads: true,
correction_bead_priority: 1,
recursion_max_depth: 3,
recursion_max_concurrent: 16,
recursion_fan_out_concurrency: 4,
recursion_child_timeout: 120_000
}

The loaded config map also includes a :project_root key set to the detected project root path.

# --- LLM Providers ---
[llm]
default_provider = "anthropic" # "anthropic" | "openai"
default_model = "claude-sonnet-4-20250514"
[llm.anthropic]
api_key_env = "ANTHROPIC_API_KEY" # env var name (not the key itself)
max_retries = 5
base_delay_ms = 1000
[llm.openai]
api_key_env = "OPENAI_API_KEY"
max_retries = 5
base_delay_ms = 1000
# --- TUI ---
[tui]
fps = 30
theme = "dark"
# --- Security ---
[security]
shell_allowlist = ["git", "mix", "elixir", "cat", "ls", "grep", "rg", "find"]
shell_blocklist = ["rm -rf /", "sudo", "curl | bash"]
file_read_outside_project = "ask" # "allow" | "ask" | "deny"
# --- Bursts ---
[burst]
question_timeout_ms = 120000 # 2 minutes before drifting
auto_create_correction_beads = true
correction_bead_priority = 1
# --- Memory ---
[memory]
half_life_days = 90
harmful_multiplier = 4
auto_reflect = true # run reflection after each burst
# --- Session ---
[session]
compaction_threshold = 0.8 # compact at 80% of context window
keep_recent_messages = 10
# --- Agent Defaults ---
[agent]
max_turns = 50
max_tokens = 4096

The config loader flattens nested TOML sections into a single-level map with atom keys. For example:

[burst]
question_timeout_ms = 120000
auto_create_correction_beads = true

becomes:

%{question_timeout_ms: 120000, auto_create_correction_beads: true}

This means keys across sections must be unique. If two sections define the same key name, the last one processed wins (map merge behavior).

VariableRequiredPurpose
ANTHROPIC_API_KEYYes (if using Anthropic)Anthropic API authentication
OPENAI_API_KEYYes (if using OpenAI)OpenAI API authentication

API keys are never stored in config files — always use environment variables. The TOML config references them by env var name (e.g., api_key_env = "ANTHROPIC_API_KEY"), not by value.

The full config module is at lib/annihilation/config.ex:

defmodule Annihilation.Config do
@defaults %{
project_dir: ".annihilation",
db_path: "beads.db",
search_db_path: "search.db",
sessions_dir: "sessions",
question_timeout: 120_000,
auto_create_correction_beads: true,
correction_bead_priority: 1,
recursion_max_depth: 3,
recursion_max_concurrent: 16,
recursion_fan_out_concurrency: 4,
recursion_child_timeout: 120_000
}
def defaults, do: @defaults
def load(project_root \\ nil) do
root = project_root || detect_project_root()
global = load_toml(Path.expand("~/.annihilation/config.toml"))
project = load_toml(Path.join([root, ".annihilation", "config.toml"]))
@defaults
|> Map.merge(global)
|> Map.merge(project)
|> Map.put(:project_root, root)
end
def detect_project_root(start_dir \\ File.cwd!()) do
cond do
File.dir?(Path.join(start_dir, ".annihilation")) -> start_dir
File.dir?(Path.join(start_dir, ".git")) -> start_dir
start_dir == "/" -> File.cwd!()
true -> detect_project_root(Path.dirname(start_dir))
end
end
end