Skip to content

Configuration reference

Mjolnir keeps per-user configuration in config.toml. Workspaces, sessions, prompt history, per-session resource choices, drafts, and read markers live in Mjolnir’s state database instead; they are not fields in this file.

Open Settings with prefix+s to add or edit agent profiles, SSH and EC2 connections, projects, runtime overrides, and interface options. The command palette (prefix+:) also provides Manage agent profiles, Manage machines, and Manage runtimes. No setup command or file editing is required. Detect profiles on the Agent Profiles page can import existing agent accounts for review.

Standard local targets are supplied automatically: localhost, Podman, Docker, and Apple container on macOS. Saved entries override their defaults. The new, resume, and move target pickers check every target in the background, including SSH connectivity and AWS credentials and launch templates. Pending or failed checks block Next and show the reason; press prefix+shift+r to recheck after fixing it. Checks do not start stopped services, and launch performs a final preflight.

Settings refuses to remove or rewrite configuration used by an active session; add an alternative entry or stop the session first. Global defaults and profile enablement remain editable. mj setup reruns automatic agent and repository discovery; direct config.toml editing also remains available.

If an active session references a missing profile, bundle, or target, Mjolnir still opens and marks that session as needing configuration repair. Select it and press Enter for repair details, its retained transcript, or Settings. The web session menu also provides repair guidance. Restore the named entry in Settings and retry. Detect profiles can rediscover installations, but cannot reconstruct an arbitrary deleted bundle or custom target. Other sessions remain accessible, and configuration diagnostics do not change the affected session’s stored lifecycle state.

See Profiles and harnesses, Workspaces and bundles, and Targets for the concepts behind these fields.

The default paths follow the operating system’s configuration and data directories:

FileLinux (default XDG directories)macOS
Configuration~/.config/mjolnir/config.toml~/Library/Application Support/mjolnir/config.toml
State database~/.local/share/mjolnir/mj.sqlite3~/Library/Application Support/mjolnir/mj.sqlite3

Set MJ_CONFIG_DIR to replace the configuration directory; Mjolnir appends config.toml to it. MJ_DATA_DIR replaces the data directory.

Pass --instance <name> (short -i, or MJ_INSTANCE=<name>) to run a fully isolated copy: configuration, database, daemon, and logs move under instances/<name> inside the default directories. For --instance dev:

FileLinuxmacOS
Configuration~/.config/mjolnir/instances/dev/config.toml~/Library/Application Support/mjolnir/instances/dev/config.toml
State database~/.local/share/mjolnir/instances/dev/mj.sqlite3~/Library/Application Support/mjolnir/instances/dev/mj.sqlite3

On macOS, configuration and data share the same instance directory under Application Support. Quote paths containing spaces when using them in a shell. Each instance runs its own daemon, so parallel instances never share sessions. The name may only use ASCII letters, digits, ., -, and _. Explicit MJ_CONFIG_DIR/MJ_DATA_DIR still take precedence over the instance directories.

Every current file starts with the required schema version:

version = 18

The top-level keys are listed below. The legacy [mailbox] enabled switch is also accepted when reading and is saved under [github.watch] enabled.

KeyTOML typeRequiredDefaultPurpose
versionintegeryesnoneConfiguration schema version; use 18.
sessions_sidestring enumno"left"Place the Sessions sidebar on the left or right.
show_stopped_sessionsbooleannoignoredRetired. It is accepted when reading configuration files but has no effect and is omitted on the next save. Suspended sessions are listed only in the resume dialog.
spinnerstring enumno"scan"Activity animation: scan, pulse, wave, bars, shimmer, or globe.
themestring enumno"midnight"Terminal color palette: auto, midnight, light, darcula, high-contrast, high-contrast-light, colorblind-dark, colorblind-light, catppuccin-mocha, catppuccin-latte, campbell, one-half-dark, one-half-light, solarized-dark, solarized-light, terminal-dark, terminal-light, or mono (no colors). auto asks the terminal for its background color at startup and uses midnight on a dark background and light on a light one, or midnight when the terminal does not answer. The colorblind palettes keep failures distinct from success and warnings with red-green or blue-yellow color blindness. The Campbell, One Half, and Solarized palettes match the Windows Terminal schemes of the same name; terminal-dark and terminal-light use the terminal’s own background and 16 ANSI colors, with a neutral gray for secondary text. A non-empty NO_COLOR environment variable selects mono regardless of this setting.
phonetablenodefault [phone] valuesBrowser and desktop viewer settings.
advancedtablenodefault [advanced] valuesOptional terminal display settings.
notifytablenodefault [notify] valuesHow the terminal dashboard reports sessions that need you.
reviewtablenodefault [review] valuesIndependent turn-review settings.
continuationtablenoenabledAutomatic continuation of already-requested unfinished work; also requires Jev.
jevtablenoenabledHosted turn classification, continuation checks, and semantic help search.
sessionwikitablenodefault [sessionwiki] valuesFull-text session index and automatic archiving.
keystablenodefault [keys] valuesThe prefix key and every command’s key bindings.
profilestable of named tablesnoemptyNamed harness accounts and homes.
bundlestable of named tablesnoemptyNamed repository sets for managed targets.
githubtablenoApp absent; watcher enabledGitHub App credentials and event watcher settings.
machinestable of named tablesnoemptyNamed hosts sessions run on. local is implied even when it is absent.
targetstable of named tablesnoemptyNamed runtimes, each naming the machine it runs on.
subagentstablenodefault [subagents] valuesGlobal task choices and limits for Mjolnir-owned child agents.
build_cachetablenononeLegacy compatibility input. A global opt-out is migrated to the existing machines and omitted on save.

The terminal Settings screen groups the prefix key, sessions_side, spinner, and theme under Interface. This is a presentation grouping: the prefix remains at keys.prefix, while the other three fields remain at the top level in config.toml.

A missing or empty file is treated as an empty version 18 configuration. Older versions acquire defaults in memory and upgrade on the next ordinary save. Unknown fields in the current top-level, viewer, review, profile, bundle, and repository schemas are errors. If a file declares a version newer than this build understands, Mjolnir salvages the sections it can read but treats the file as read-only. mj doctor reports that state; update Mjolnir before changing it.

Profile, bundle, repository, machine, and runtime IDs all use the same rule: 1–64 ASCII letters, digits, ., -, or _. The IDs . and .. are not allowed. IDs are the TOML table names, for example work in [profiles.work].

Sessions are created explicitly through the New session wizard or the CLI/API. Opening an empty workspace does not create a session. Older [startup] settings are ignored and removed the next time configuration is saved.

The optional [advanced] table controls detail that is useful while diagnosing activity without changing how sessions run:

[advanced]
detailed_activity_clocks = false
session_order = "project"
# symbols = "ascii"
FieldTOML typeDefaultBehavior
detailed_activity_clocksbooleanfalseWhen enabled, normal session rows and the conversation header show separate turn, step, and background clocks.
session_order"project" or "priority""project"project groups sessions under a heading per project in creation order. priority lists sessions that need you first (waiting, failed, unread, working, idle) with no project headings.
symbols"unicode" or "ascii"unsetWhich glyphs the dashboard draws status marks, borders, chart bars, and separators with. Unset follows the terminal: ASCII when TERM is linux or the locale (LC_ALL, LC_CTYPE, LANG) names no UTF-8 encoding, Unicode otherwise.

The terminal Settings screen edits these settings under Advanced. Detailed clocks do not change how sessions run: the normal Running status continues across the originating turn and its background work.

The optional [keys] table rebinds the prefix key and every command it drives. Mjolnir follows tmux’s model, described in Terminal surface: press the prefix, release it, then press a second key. An edit here takes effect the next time the daemon reloads the file, within about a second. The terminal Settings screen exposes the prefix under Interface; edit the command bindings by hand.

[keys]
prefix = "ctrl+b"
new_session = "prefix+c"
refresh = ["prefix+shift+r", "f5"]
switch_workspace = "prefix+1..9"
pane_preset = ""

Each field takes one key string or a list of them; a list binds every string in it to the same command. An empty string ("") unbinds a default. The 1..9 range form, optionally with a leading modifier such as ctrl+1..9, is accepted only for switch_workspace; it expands to nine bindings, one per digit.

A key string is modifier tokens joined by +, then a key name. Modifiers are ctrl/control, alt/option/meta, shift, and cmd/command/super. Key names include space, enter/return, esc/escape, tab, backspace/bs, delete, insert, home, end, pageup, pagedown, the arrow keys, f1–f12, punctuation names such as slash, semicolon, and backtick, and any single character. An uppercase letter is read as the lowercase letter plus shift. A binding that should fire only after the prefix key carries a prefix+ marker, for example prefix+shift+n; a binding written without that marker fires directly, with no prefix.

The default bindings:

FieldDefaultAction
prefixctrl+bThe prefix key itself
helpprefix+?Open help
settingsprefix+sOpen Settings
detachprefix+qDetach this terminal
new_sessionprefix+cOpen the session creation wizard
resumeprefix+gOpen the session dialog on every running session, with the resume, import, and archive lists on its other tabs
workspace_managerprefix+shift+nOpen workspace management
focus_workspacesprefix+wFocus the workspace tab row
next_workspaceprefix+nSelect the next workspace
previous_workspaceprefix+pSelect the previous workspace
switch_workspaceprefix+1..9Select a workspace by number
next_paneprefix+tabFocus the next support pane
previous_paneprefix+shift+tabFocus the previous support pane
pane_sizeprefix+shift+zCycle the focused support pane’s size
pane_presetprefix+bToggle the dashboard pane preset
refreshprefix+shift+rRefresh target capacity and profile quota
paletteprefix+:Open the command palette
cancel_operationprefix+shift+cCancel an in-flight launch, resume, or stop
mark_all_readprefix+aMark unread session activity as read
next_attentionprefix+oOpen the next session that needs you
previous_attentionprefix+shift+oOpen the previous session that needs you
web_viewerprefix+uShow the web viewer address and access code
rename_sessionprefix+shift+tRename the selected session
toggle_transcript_renderingprefix+tToggle rendered/raw transcript
toggle_dictationprefix+mStart or stop dictation
changed_filesprefix+dList the selected session’s changed files
open_subagentsprefix+shift+aShow the selected session’s sub-agents
session_actionsprefix+.Open the selected session’s actions menu (also . on the Sessions pane)
interrupt_turnprefix+iInterrupt the selected session’s running turn (Esc in the composer does the same)
split_verticalprefix+vOpen the selected session in a pane beside this one
split_horizontalprefix+-Open the selected session in a pane below this one
close_paneprefix+xClose the conversation pane you are in
focus_pane_leftprefix+hMove the keyboard to the pane on the left
focus_pane_downprefix+jMove the keyboard to the pane below
focus_pane_upprefix+kMove the keyboard to the pane above
focus_pane_rightprefix+lMove the keyboard to the pane on the right
zoomprefix+zFill the conversation area with the pane you are in, or put the others back
last_paneprefix+;Move the keyboard back to the pane it was in before
resize_modeprefix+rEnter resize mode: arrow keys or h/j/k/l move the pane border until Esc
swap_pane_leftprefix+shift+hSwap the pane you are in with the pane on the left
swap_pane_downprefix+shift+jSwap the pane you are in with the pane below
swap_pane_upprefix+shift+kSwap the pane you are in with the pane above
swap_pane_rightprefix+shift+lSwap the pane you are in with the pane on the right
rename_workspaceprefix+shift+wRename the current workspace in the workspace dialog
close_workspaceprefix+shift+dClose the current workspace from the workspace dialog
suspend_sessionprefix+shift+xSuspend the selected session (asks first)
restart_sessionunboundRestart the selected session
move_sessionunboundMove the selected session to another target
destroy_sessionunboundDelete the selected session
container_settingsunboundEdit container settings for the selected session
manage_profilesunboundOpen profile management
manage_targetsunboundOpen target management
manage_machinesunboundOpen machine management
restart_daemonunboundRestart the Mjolnir daemon
notice_logunboundShow the last notices the footer reported
change_go_setupunboundChange the mj go fast-start setup
cycle_spinnerunboundCycle the activity spinner style
resize_pane_leftunboundMove the conversation pane’s border left
resize_pane_downunboundMove the conversation pane’s border down
resize_pane_upunboundMove the conversation pane’s border up
resize_pane_rightunboundMove the conversation pane’s border right

The actions listed as unbound have no default key because they are destructive, infrequent, or fine adjustments that a mis-hit key should not run; use the command palette (prefix+:) instead, or bind them here.

The conversation-pane keys follow herdr and tmux; see Conversation panes. prefix+- can also be written prefix+minus.

Config::validate rejects an invalid [keys] table fatally, the same as any other configuration error, with one exception: a binding you write silently displaces a default binding on the same key, because rebinding a command onto a key another command defaults to is the normal way to move a key.

  • The prefix must be a modified chord (carrying ctrl, alt, or super) or a function key; a bare letter or a shift-only combination is rejected.
  • A direct binding (no prefix+ marker) on an unmodified printable character is rejected, because the composer reads that key as text; write "prefix+<key>" instead.
  • A direct binding on ctrl+c or ctrl+v is rejected: the dashboard and composer already treat them as cancel and paste.
  • A direct binding whose key equals the prefix is rejected with the prefix key cannot also be a direct binding, because that key already arms the prefix and would never reach the command.
  • A prefix binding whose key equals the prefix itself is rejected, because that combination is reserved for sending the literal prefix key through (see Terminal surface).
  • Two user-written bindings on the same key is rejected with an error naming both fields, for example keys.pane_preset = "prefix+space": already bound by keys.help.

This key-string syntax matches herdr’s own [keys] table, so a line can be copied between the two configuration files unchanged.

The optional [notify] table controls how the terminal dashboard reports a session you are not looking at when it asks a question, fails, or finishes with an answer you have not read. The session whose conversation is on screen never notifies.

[notify]
mode = "terminal"
bell = true
delay_seconds = 2
title = true
FieldTOML typeDefaultBehavior
mode"off", "terminal", or "system""terminal"terminal rings the terminal bell, which reaches you through SSH and multiplexers. system also posts a desktop notification through osascript on macOS, notify-send on Linux, or a PowerShell toast on Windows. off reports nothing.
bellbooleantrueWhether each notification rings the terminal bell. Turn it off with mode = "system" for silent desktop notifications.
delay_secondsinteger2How long a session must keep needing you before it is reported, so a question the agent answers itself stays quiet.
titlebooleantrueKeep the terminal window title showing the counts, for example mj · 2 waiting · 1 unread, independently of mode.

The terminal Settings screen edits these settings under Notifications.

The historical section name remains phone, although it controls both the web viewer and the native desktop shell.

[phone]
enabled = true
bind = "127.0.0.1:3765"
tailscale_detect = true
# tls_cert = "/absolute/path/fullchain.pem"
# tls_key = "/absolute/path/private-key.pem"
FieldTOML typeRequiredDefaultValidation and behavior
enabledbooleannotrueStarts the viewer with the daemon.
bindstringno"127.0.0.1:3765"; a named instance uses a port in 38000–38999Must parse as a numeric socket address, including a port.
tailscale_detectbooleannotrueAllows automatic trusted ts.net certificate discovery and renewal.
tls_certpath stringnounsetCertificate-chain path. Must be paired with tls_key.
tls_keypath stringnounsetPrivate-key path. Must be paired with tls_cert.

A named instance (MJ_INSTANCE=<name> or mj --instance <name>) that does not set bind listens on 127.0.0.1 at a port from 38000 to 38999 computed from its name. The port stays the same across daemon restarts, so a bookmarked login link keeps working, and it does not collide with the default instance’s port 3765. If the port is taken anyway, for example by another named instance whose name maps to the same port, the viewer reports the conflict and gives the exact bind line to add under [phone]. The mj commands that use the HTTP API, such as mj sessions, mj prompt and mj message, need the viewer’s listener and fail until the conflict is resolved.

A non-loopback bind is rejected unless both explicit TLS paths are present. When Tailscale detection succeeds, Mjolnir may advertise a secure non-loopback listener without changing the configured loopback fallback. Explicit TLS takes precedence. See Web viewer and desktop app for access and login.

[review]
enabled = true
profile = "reviewer"
# model = "provider-model-id"
# effort = "high"
FieldTOML typeRequiredDefaultValidation and behavior
enabledbooleannofalseExamines each eligible completed turn after queued work drains; an unchanged delta resolves without a review prompt.
tierstringnounsetDeprecated compatibility field. Existing quick or extended values are accepted but ignored.
profilestringnoAuto (unset)Auto selects an eligible profile by provider and quota. A named enabled review-capable profile is honored, including the primary profile.
modelstringnounset (harness default)Reviewer model override for a named profile. Auto uses fixed model-family defaults.
effortstringnounset (harness default)Reviewer effort override for a named profile. Auto does not accept manual overrides. Required effort is checked against the selected model.

These settings also select the plan second-opinion reviewer. Auto prefers another provider, falling back to another profile or the primary profile when needed. Turn review uses one reviewer. See Independent turn review.

mj new --review-model <model> and --review-effort <effort> review every turn of one new session with that model or effort, even when enabled is false. In Auto, Mjolnir picks the first enabled profile that offers the model. mj new --no-review turns off automatic review for one session, even when enabled is true; /review still works. Sessions created without these flags follow [review].

The deprecated mj new --review-tier quick|extended option still accepts either value and enables automatic review for that session, but the value is ignored. mj import <harness> accepts the same compatibility option. Existing tier values in session settings are also ignored.

In the terminal, these review fields are edited inside Settings so one Save or Cancel applies to the entire configuration draft. Settings can discover the selected review profile’s supported model and effort choices and filters the profile list to compatible reviewer profiles.

[jev]
enabled = false
FieldTOML typeRequiredDefaultValidation and behavior
enabledbooleannotruefalse stops every request to the hosted Jev service: turn classification, automatic continuation, and semantic help search.

Jev reads recent conversation text to answer three questions: whether a silent turn is waiting for you, whether a finished reply left work undone, and which help rows match what you typed. Security boundaries lists exactly what each request sends and where.

With enabled = false:

  • Workers do not classify turns. A turn ends when the harness ends it. A turn that goes quiet while the agent waits for an answer stays Working until the harness reports the end of the turn, and after a reply the session’s state follows the harness’s own background-task reports.
  • Automatic continuation does not run, whatever [continuation] says.
  • The help filter matches text only and shows no related shortcuts.

Workers read the switch when they start, so a running session follows the new value after it is next resumed or restarted. In the terminal the switch is Settings → Privacy → Jev (hosted service).

enabled is a boolean that defaults to true. Together with [jev] enabled, it controls the GitHub watcher as an event source. Jev classifies GitHub activity. Neither setting disables mailbox delivery, peer messages, child reports, drain hooks or idle wakes. The old [mailbox] enabled key still loads silently. If either key is false, the watcher stays off. Saving writes only [github.watch] enabled, preserving an older false setting.

mj new --no-github-events prevents the GitHub watcher from registering the session as a delivery target. Sub-agents inherit the flag. This is the flag’s only effect.

The watcher records updated issues and pull requests before processing them. At most once an hour, it rereads recorded activity that is at least an hour old. Saved progress preserves unfinished ranges across pauses and failed requests. An item can still be missed if page movement affects both reads. The watcher also overlaps processing times by 30 seconds to allow for response timing and clock differences. Repeated event IDs deliver only once. Jev classifies new items only for sessions that were already running when the item was created. When Jev identifies a session as the creator, that session receives new comments, reviews and pull request lifecycle events from the item’s timeline. Pull request line comments are read separately and saved with the timeline events. Each source keeps its own cursor, and both are saved in one item transaction. A failed item does not block delivery for other items. Failed classifications retry with a delay that grows from one minute to one hour; new work is classified before retries. Notices always include the repository name. Comments and reviews written by the token owner during that session’s turn do not wake it.

[continuation]
enabled = false
FieldTOML typeRequiredDefaultValidation and behavior
enabledbooleannotrueAllows Jev to check ended turns and continue work already requested by the user. Requires [jev] enabled = true.

There are at most three automatic continuations between user messages. New input or an interrupt cancels a pending check; continuation never supplies missing approval or answers a question. See Automatic continuation for background work, goals, quota recovery, and the text sent to the classifier.

SessionWiki is a separate tool that indexes AI coding sessions from many tools into one full-text SQLite index. The daemon always writes Mjolnir’s sessions into that index under the tool name mjolnir, and Resume searches it. This section only chooses whether old sessions are archived, because archiving is the only part that deletes anything.

[sessionwiki]
archive_after_days = 30
FieldTOML typeRequiredDefaultValidation and behavior
archive_after_daysintegernounset (keep every session)Suspended sessions older than this many days are removed from Mjolnir once SessionWiki has indexed them. 0 is rejected.

An enabled key written by an earlier build is still read and then ignored; indexing is no longer optional.

Because indexing is always on and Resume searches the index and nothing else, the first upgrade to this build builds the index before Resume can be searched. The search box says Indexing… until that first build finishes, which on a large corpus of other tools’ sessions can take several minutes. The list and the tabs work throughout, and the box opens by itself when the build ends.

The index is the user’s own SessionWiki index, in SessionWiki’s default location. There is no index path setting; set SESSIONWIKI_DATA if you move it. A daemon running against an overridden MJ_DATA_DIR indexes into $MJ_DATA_DIR/sessionwiki instead, so a test or lab daemon never writes your own index. Each Mjolnir instance indexes only its own sessions, and every instance shares the one tool name, so a single search covers them all.

See Search and restore archived sessions for what archiving deletes and keeps, and for the rule that the sessionwiki command-line tool must match the version Mjolnir links.

A Claude or Codex session can use Mjolnir’s spawn tool. Create and Move offer Native or Mjolnir. Native uses the harness’s own delegation. Mjolnir needs a straightforward choice, configured in Settings → Sub-agents or config.toml, or a session override. The subtle choice is optional, but cannot be configured without straightforward:

[subagents]
max_concurrent = 6
[subagents.straightforward]
model = "your-codex-model"
effort = "high"
[subagents.subtle]
model = "your-claude-model"
effort = "high"
[subagents.eligible_profiles]
codex = true
claude = true

Each choice requires a non-empty model. Effort is optional and must be offered by that model when specified; omitted effort uses the harness default. Adaptive effort is no longer supported. Each configured choice must be available to the parent’s eligible profiles before Mjolnir can be selected. Legacy per-profile subagent settings are ignored and dropped on save, with no migration to global choices.

With both types configured, spawn takes task_type: straightforward for work with a clear method, such as locating code, running commands or tests, or implementing a settled design; subtle for judgment, ambiguous diagnosis, design choices, or risk of subtle bugs. A subtle task gets a smarter, more expensive agent. Judge difficulty, not brief length. With only straightforward configured, spawn has no task_type and every child uses the straightforward choice. The parent is told to delegate straightforward work and to do work that needs judgment itself. Mjolnir chooses the eligible profile offering that task type’s model and effort with the most quota. The agent cannot override the selection. Children cannot delegate. mj new --subagents native|mjolnir|none defaults to Native; mj move --subagents native|mjolnir|none retains the recorded policy when omitted, including any session override. --subagent-straightforward M and --subagent-subtle M independently override their matching task type and imply Mjolnir. An unspecified type uses its global choice, so one override can be combined with one global choice. Each model and optional effort must be offered by an eligible profile. Each effort flag requires its matching model flag; adaptive and combining an override with native or none are rejected. Omitted effort uses the harness default. The retired --subagent-model and --subagent-effort flags fail with guidance to use the four per-type flags. none (CLI only) gives a Claude or Codex session no delegation tools.

FieldTOML typeRequiredDefaultValidation and behavior
max_concurrentintegerno6Most live sub-agents one session may have at once; between 1 and 64. A sub-agent counts while it holds processes on the target, including while it is idle. A parked sub-agent, one whose turn ended and whose worker Mjolnir stopped, does not count (see Parked sub-agents).
eligible_profilestable of booleansnoemptyProfiles, by id, that any session’s sub-agents may use. A session’s sub-agents may always use the session’s own profile, listed or not. A disabled profile is ignored, and mj doctor warns about it. An id that names no profile stops the configuration from loading.

A configuration file written before the per-session choice existed may still have an enabled key here. It is read and ignored, and a save drops it.

Mjolnir runs each child with the configured model and effort on the eligible profile that supports that selection and has the most quota left, meaning the lower of its 5-hour and weekly remaining percentages. A pay-per-use profile counts as 100% left, and a profile with no quota report comes last. On a tie, the parent’s own profile wins. mj doctor shows, for each profile, where its quota comes from and whether other sessions’ sub-agents may use it.

A sub-agent ends its task by handing its report back to the session that started it. Suspending that session stops its sub-agents and removes them, without a recovery copy of their own; only the parent session is saved. Mjolnir warns when a sub-agent has not handed back yet, because the work it has not reported is lost. Each stopped sub-agent’s conversation stays searchable through SessionWiki (mj sessions --session <id>). When the parent resumes, its agent is told which sub-agents were stopped and whether each had handed back, so it can start them again if it still needs their work. See Sessions.

Each profile names one harness installation or account on the controller:

[profiles.codex-work]
kind = "codex"
home = "/home/me/.codex-work"
# enabled = false
# context_window_bytes = 131072
# guardian_review_model = "newest-flash"
[profiles.codex-work.environment]
# PATH = "/opt/node/bin:/usr/local/bin:/usr/bin:/bin"
# PROVIDER_SETTING = "value"
# PROVIDER_API_KEY = { from_secret = "PROVIDER_API_KEY" }
FieldTOML typeRequiredDefaultValidation and behavior
enabledbooleannotrueDisabled profiles stay configured but cannot be selected for new work, login, import, review, quota reporting, or utility-model inference. Existing running sessions continue.
kindstring enumyesnonecodex, claude, kimi, grok, muse, opencode, or mimo.
homepath stringyesnoneNon-empty controller-side harness home. An absolute path is strongly recommended.
environmenttable of strings or referencesnoemptyEnvironment passed to harness/profile commands. Each value is a string, { from_env = "NAME" }, or { from_secret = "NAME" }; see Secrets. Keys cannot be blank or contain =. A Codex profile whose config.toml names a custom model provider with env_key reads that variable from the environment Mjolnir started with unless it is set here; when it is set in neither, the profile cannot start, the configuration still loads, and mj doctor says how to supply it.
context_window_bytesintegernounset (262144-byte fallback)Conservative byte budget for cross-harness transcript compaction; when set, must be at least 32768.
guardian_review_modelstringnounset (newest-flash)Which model reviews escalated actions in Codex’s guardian mode: newest-flash, session, or a slug from the provider’s model catalog. Only valid on a Codex profile whose config.toml names a custom model provider. See Profiles.

The profile’s harness-home variable cannot appear in environment; set home instead. Those variables are CODEX_HOME, CLAUDE_CONFIG_DIR, KIMI_CODE_HOME, GROK_HOME, MiMo Code’s MIMOCODE_HOME, and XDG_CONFIG_HOME for Muse and OpenCode. Muse and OpenCode also reserve XDG_DATA_HOME; their homes must end in /muse and /opencode, respectively. A MiMo Code profile at the conventional ~/.mimocode home reads the stock ~/.config/mimocode configuration and ~/.local/share/mimocode/auth.json login; every other MiMo home is a MIMOCODE_HOME root.

Profiles do not select target-side executables. All bare workers, including localhost, SSH, and EC2, resolve an exact pinned runtime from their managed cache. Containers use their image-provided runtime or the documented installer fallback. mj login invokes the harness’s canonical controller-side command from PATH.

Profiles do not accept model or reasoning_effort. Use /model and /effort inside a session, or configure the harness’s own defaults in its home. For credential files, staging allowlists, skills, quotas, and harness limitations, see Profiles and harnesses.

config.toml is copied into isolated instances, pasted into bug reports, and read by agents diagnosing a setup. A credential written into it as a plain string travels with it. Keep credentials out of it by writing a reference instead of a value in any environment table:

[profiles.deepseek.environment]
DEEPSEEK_API_KEY = { from_secret = "DEEPSEEK_API_KEY" }
GITHUB_TOKEN = { from_env = "GITHUB_TOKEN" }
FormValue passed to the harness
"text"The text as written.
{ from_secret = "NAME" }The NAME entry of secrets.toml beside this config.toml.
{ from_env = "NAME" }The variable NAME in the environment of the process that reads the configuration: the daemon for sessions, the CLI or TUI for their own commands. Start the daemon with the variable set, or use from_secret.

secrets.toml is a flat table of string values, in the same directory as config.toml, so each --instance has its own and copying a configuration never copies a secret:

DEEPSEEK_API_KEY = "sk-..."

Make it readable only by you (chmod 600 secrets.toml). References are resolved when the configuration is read. An entry that names a missing variable or secret does not stop the configuration from loading: the profile or target that holds it cannot start until the value exists, its sessions are refused with the entry and file named, and mj doctor reports it. Saving a setting writes the reference back as written, never the value it stood for. mj doctor warns about a secrets.toml other users can read, and about any environment value written as plain text under a name that suggests a credential, such as *_API_KEY or *_TOKEN.

A configuration that uses a reference needs a Mjolnir that understands it; an older build reports the entry as an invalid type. Plain strings keep working everywhere.

An optional GitHub App can provide installation tokens for private bundle repositories without putting a long-lived user token in the daemon environment. The daemon reads the App’s private PEM key from the controller host. The key and installation tokens are not stored in the database or session checkpoints. Without this section, Mjolnir keeps using GH_TOKEN, GITHUB_TOKEN, or gh auth token as before.

[github.app]
app_id = 1234
private_key_path = "/home/me/.config/mjolnir/github-app.pem"
[github.app.installations]
acme = 987654

app_id is the GitHub App ID, and private_key_path names its RSA PEM private key on the controller host. The optional installations table maps GitHub owner logins (organizations or users) to installation IDs. Owners not listed there are discovered through GitHub’s API and cached by the daemon. Find the installation ID on the installed App’s settings page.

Each managed session may use repositories from only one App installation. A bundle that resolves to multiple installations is rejected when the session is created. App tokens are refreshed before they approach expiry and are delivered to running remote/container sessions during credential sync. Local bare sessions do not receive periodic refresh; an App token used there expires within at most one hour.

See GitHub App credentials for creating an App and setting up its repository permissions.

A bundle is a non-empty repository set. The primary repository becomes the agent’s working directory; other repositories are additional workspace roots.

[bundles.product]
primary_repo = "app"
[[bundles.product.repositories]]
id = "app"
github = "acme/app"
destination = "app"
[[bundles.product.repositories]]
id = "shared"
local = "/home/me/src/shared"
destination = "shared"

Bundle fields:

FieldTOML typeRequiredValidation and behavior
primary_repostringyesMust exactly match one repository id in this bundle.
repositoriesarray of tablesyesMust contain at least one repository.

Repository fields:

FieldTOML typeRequiredValidation and behavior
idstringyesValid, unique ID within the bundle.
githubstringexactly one sourceGitHub source in a supported form; cannot be combined with local.
localpath stringexactly one sourceAbsolute controller-side Git path used to resolve its network remotes; cannot be combined with github.
destinationpath stringyesNon-empty relative path below the target workspace; no . or .. components.

Supported GitHub forms are owner/repository, https://github.com/owner/repository, git@github.com:owner/repository, and ssh://git@github.com/owner/repository, with an optional .git suffix. Sources cannot contain whitespace or begin with -. At session creation, Mjolnir uses the source’s default network fetch remote and preserves its configured push destination(s). A local path is inspected for that configuration; its local commits, staged changes, unstaged changes, and untracked files are excluded from an isolated clone. Repository destinations may not be equal, ancestors, or descendants of one another.

Bundles are used by container and EC2 sessions. New bare sessions select an existing Git project directory instead. git_ref is no longer accepted; remove it and use the source remote’s default branch. See Workspaces and bundles for network remotes, isolated clones, and project memory.

A machine is a host sessions run on: this computer, an SSH host, or an EC2 launch template. It owns the connection details, the remote workspace directory bare runtimes use, and the mbx build cache every runtime on it shares.

The machine local is this computer. It always exists, so a file only names it when it carries a setting of its own.

FieldTOML typeRequiredDefaultAccepted values
kindstring enumyesnonelocal, ssh, or aws-ec2. Only the id local may hold kind = "local".
[machines.local]
kind = "local"
[machines.local.build_cache]
max_size = "50GiB"

The only setting is the shared build cache below. Omit the table entirely to use this machine’s own defaults.

[machines.builder]
kind = "ssh"
host = "builder.example.com"
user = "ubuntu"
identity_file = "/home/me/.ssh/mjolnir"
extra_args = ["-o", "ServerAliveInterval=30"]
workspace_prefix = ".local/share/hel/workspaces"
FieldTOML typeRequiredDefaultValidation and behavior
hoststringyesnoneOpenSSH host or config alias; non-blank and contains no whitespace.
userstringnounset (SSH/config default)Cannot be empty, contain whitespace, or contain @.
identity_filepath stringnounset (SSH/config default)Private-key path passed to SSH; it is not required to be absolute.
extra_argsarray of stringsnoemptyAdditional OpenSSH arguments, passed in order.
workspace_prefixpath stringno".local/share/hel/workspaces"Per-session lifecycle/cleanup path prefix for bare runtimes on this machine. It does not select or relocate the remote Git project. May be home-relative or safely absolute.

host and user are combined as user@host; put only the host or alias in host. workspace_prefix cannot be empty, /, ., bare ~/~/, or contain ... A leading ~/ on a longer path is interpreted relative to the remote login home.

Two machines may not describe the same SSH connection; one machine is one host.

[machines.fleet]
kind = "aws-ec2"
aws_profile = "default"
region = "eu-west-1"
launch_template = "lt-0123456789abcdef0"
# launch_template_version = "3"
ssh_user = "ubuntu"
address_source = "public-dns"
# identity_file = "/home/me/.ssh/mjolnir-ec2"
ssh_args = ["-o", "ServerAliveInterval=30"]
FieldTOML typeRequiredDefaultValidation and behavior
aws_profilestringnounset (runtime uses "default")AWS CLI profile; cannot be an empty string when set.
regionstringyesnoneNon-blank AWS region.
launch_templatestringyesnoneNon-blank launch-template ID or name.
launch_template_versionstringnounset (runtime uses "$Default")Cannot be an empty string when set.
ssh_userstringyesnoneNon-blank login user for the launched instance.
address_sourcestring enumno"public-dns"public-dns, public-ip, private-dns, or private-ip.
identity_filepath stringnounset (SSH/config default)Private-key path passed to SSH; it is not required to be absolute.
ssh_argsarray of stringsnoemptyAdditional SSH arguments, passed in order. Note the field name differs from an SSH machine’s extra_args.

The launch template owns networking, security groups, storage, AMI, and any default instance type. The new-session wizard may override the instance type for one session. An EC2 machine runs a bare harness only: it accepts no container runtime and no build cache, because each session gets its own instance. See AWS EC2.

Linux raw local/SSH sessions and Podman/Docker sessions share native Go, Gradle, Turbo, Nx and Bazel build caches by default. Mjolnir detects the tools from manifests in the checkout root; it does not adopt or install a build system. Native caches do not require mbx. Set enabled = false to disable Mjolnir’s cache setup for new sessions on the machine. Existing containers keep their recorded mounts until recreated.

Rust and C/C++ also use the machine’s mbx 1.22.0 or newer. Container mbx needs a Linux host and a local filesystem with reflinks; install or upgrade it from Settings › Setup › Machines. Raw Rust builds keep the host’s existing Cargo configuration. In C/C++ projects, private cmake, make, gmake and ninja launchers use mbx exec, including the CMake configuration step. For Autoconf projects, run mbx exec ./configure so configuration records the cached compiler launchers before running make. The host executable is atomically refreshed at <cache>/.mjolnir/bin/mbx inside the read-write shared cache mount. Mjolnir checks that copy in the container before adding its marked Cargo and mbx launchers; periodic reconciliation refreshes it after host upgrades without interrupting running processes. In Settings, open Machines → [machine] → Build caches, or search for mbx, cache, or build cache. The machine row shows its configured state and total budget; opening it resolves the host’s actual defaults and reports any compatibility limitations. See Container targets for cache prerequisites.

FieldTOML typeRequiredDefaultValidation and behavior
enabledbooleannoenabled for supported tools and local filesystemsfalse runs sessions on this machine without the cache.
tools_directorypath stringno~/.cache/mjolnir/build on the machineAbsolute path on a local filesystem for Go, Gradle, Turbo and Bazel. Independent of mbx’s directory and budget.
directorypath stringnothe host’s native mbx cache directoryMust be absolute. It is a path on that machine, not on the controller. New sessions use the directory reported by native mbx; a missing or too-old mbx does not get a fallback cache.
max_sizestringnothe host’s native mbx configurationAn mbx size such as 100GiB. Configure limits on the machine with mbx.

Go shares GOCACHE and adds -trimpath to GOFLAGS, which makes package cache keys independent of checkout paths. Explicit environment overrides, including -trimpath=false, win. Gradle uses a private user home for each checkout and shares only task outputs. Existing user properties and initialization scripts are preserved; repository cache settings and --no-build-cache still apply. Turbo uses a per-project TURBO_CACHE_DIR. Bazel/Bazelisk launchers add a private rc file with build --disk_cache=...; explicit command-line flags take precedence, and --ignore_all_rc_files disables this configuration too.

Nx 23.2 or newer owns coordinated artifact and database sharing under the host’s ~/.nx. Mjolnir mounts that real directory at the container’s HOME/.nx; checkout-specific state remains private. The standard image uses HOME=/home/hel; custom Docker images with another home must declare HOME in their target environment. Existing Nx cache overrides remain untouched and may opt out of Nx’s automatic sharing. Older Nx versions need an upgrade for sharing between independent clones. Mjolnir never disables Nx cache validation.

These caches reuse completed outputs. Each tool manages its own retention and locking; mbx’s size and scheduler settings do not limit native caches. Task caches require correctly declared inputs and outputs in the repository. Cache stores persist when a session is destroyed. Private Gradle dependency caches are not shared across containers.

The optional scheduler table controls mbx’s shared compiler scheduler:

[machines.morannon.build_cache.scheduler]
cpus = 8
memory = "24GiB"
FieldTOML typeRequiredDefaultValidation and behavior
scheduler.cpusintegernounset (mbx uses the logical CPUs visible to it)Must be a positive integer. Sets the machine-wide compiler permit pool shared by all builds.
scheduler.memorystringnounset (mbx uses 85% of detected memory, constrained by a Linux cgroup limit)An mbx size such as 24GiB, 24GB, or 1.5GiB, or none to disable memory weighting. This is a shared admission budget for compiles, not a hard memory cap.

mbx shares this pool across independent builds on the machine. Compiler work uses permits according to its estimated memory demand, so a compile can use more than one permit. Configure scheduler values in the host’s mbx configuration; Mjolnir does not install a private mbx or create a fallback cache configuration.

A section with every field unset is the same as no section at all.

Version 14 removes the global build-cache switch. Older configurations with [build_cache] enabled = false load with caching explicitly disabled on every existing local and SSH machine, including implicit local. This preserves the old global override even when a machine explicitly requested caching. The next save writes those machine settings and removes the global section. Machines added afterward use the normal enabled default.

A runtime says how a session runs on a machine: a bare checkout, Podman, Docker, or Apple container. Every runtime requires a kind and names the machine it runs on; machine defaults to local and is omitted from the file when it is local.

FieldTOML typeRequiredDefaultAccepted values
kindstring enumyesnonebare, podman, docker, or apple-container.
machinestringno"local"The id of a [machines.<id>] entry, or local.

permissions is valid only on bare; setting it on another runtime is an error. Build cache settings are valid only on a machine; setting them on a runtime is an error.

[targets.localhost]
kind = "bare"
[targets.builder]
kind = "bare"
machine = "builder"
permissions = "guardian"
FieldTOML typeRequiredDefaultValidation and behavior
permissionsstring enumonly on an SSH machine"guardian"guardian preserves harness approvals; yolo disables sandbox checks (and approval checks, except Muse keeps its auto-review on). It has no meaning on local, where the harness keeps its configured approvals, or on an EC2 machine.

The new-session wizard asks for an existing absolute Git project directory on the runtime’s machine. On local supported harnesses retain configured approvals because there is no container or instance boundary; Muse’s guardian is muse-acp’s auto-review, see Harness limitations. On an SSH machine the wizard separately asks for an existing absolute remote Git directory; when Create isolated checkout is checked on the final review, the new checkout is created below the repository’s own .mj/clones/ tree. A bare runtime on an EC2 machine launches one instance per session. See SSH and SSH Podman and AWS EC2.

podman, docker, and apple-container accept the same container fields.

FieldTOML typeRequiredDefaultValidation and behavior
imagestringnothe running Mjolnir’s agent-dev imageNon-blank image reference. A release uses the immutable image published for its own version, such as ghcr.io/brokkai/mjolnir/agent-dev:2.36.0; a development build uses ghcr.io/brokkai/mjolnir/agent-dev:latest. Omitting the field follows the running build at every upgrade; an explicit value pins that reference.
pull_policystring enumno"auto"auto, always, newer, missing, or never.
platformstringnounset (runtime selection)Image platform such as linux/amd64 or linux/arm64; it also determines the required worker architecture when recognizable.
cpusstringnounset (no template override)Runtime CPU value, for example "8". Per-session selection can override it.
memorystringnounset (no template override)Runtime memory value, for example "32g". Per-session selection can override it.
environmenttable of strings or referencesnoemptyEnvironment placed inside the target container. Values take the same forms as a profile’s; see Secrets. Keys cannot be blank or contain =.
workspace_storagetableno{ kind = "podman-volume" }Podman accepts all variants. Docker and Apple Container reject non-default variants.

The schema checks that image is non-blank but leaves CPU, memory, and platform syntax to the selected runtime. A key a target table does not use is ignored, and the daemon log names it once. Profile environment and target environment are different: profile values configure the harness and profile commands, while target values become container environment variables.

Pull-policy behavior:

  • auto starts from an existing image and pulls only when absent. The daemon refreshes eligible moving tags for Podman, Docker, and Apple Container in the background. Versioned tags, digest references, and local images remain pinned or cached. Apple Container resolves auto during provisioning.
  • always and newer request a launch-time refresh; Docker treats newer like always because it has no distinct newer-only mode.
  • missing pulls only when no local copy exists.
  • never requires a local copy.

The Podman workspace-storage table can be written inline:

workspace_storage = { kind = "podman-volume" }
# workspace_storage = { kind = "container-layer" }
# workspace_storage = { kind = "host-helper", root = "/srv/mj-workspaces", helper = ["sudo", "-n", "/usr/local/libexec/mj-workspace-helper"] }

The inline table’s kind is a required string enum:

kind valueAdditional fields and TOML typesValidation and behavior
podman-volumenoneDefault; a named Podman volume backs /workspace.
container-layernoneStores /workspace in the disposable container layer.
host-helperroot (path string), helper (array of strings)Both fields are required. root must be absolute. helper must contain at least one non-empty argument. The helper owns host storage lifecycle.

See Container targets and Custom images for runtime behavior.

[targets.podman]
kind = "podman"
pull_policy = "auto"
platform = "linux/amd64"
cpus = "8"
memory = "32g"
workspace_storage = { kind = "podman-volume" }
[targets.podman.environment]
EXAMPLE = "value"

All common container fields are accepted, and workspace_storage supports all three Podman variants. A local Podman target runs rootless Podman on Linux, WSL2, or macOS with a rootless podman machine; rootful machines and manually configured remote connections are not supported. On an SSH machine the container and any workspace volume live on that host, so supplemental directory sources are paths on it rather than on the controller. It always runs the harness unconstrained inside the container boundary. See Podman and SSH and SSH Podman.

[targets.remote-podman]
kind = "podman"
machine = "builder"
[targets.docker]
kind = "docker"
pull_policy = "auto"
platform = "linux/amd64"
cpus = "8"
memory = "32g"
[targets.docker.environment]
EXAMPLE = "value"

All common fields except a non-default workspace_storage are supported, on this machine or on an SSH machine. See Docker.

[targets.apple]
kind = "apple-container"
pull_policy = "auto"
platform = "linux/arm64"
cpus = "8"
memory = "32g"
[targets.apple.environment]
EXAMPLE = "value"

All common fields except a non-default workspace_storage are supported. Apple container runs only on this machine. See Apple container.

Before version 12 one [targets.<id>] table fused the machine and the runtime, with kinds named local-bare, local-podman, local-docker, apple-container, ssh-bare, ssh-podman, ssh-docker, and aws-ec2. Mjolnir still reads such a file when its version is 11 or lower: it derives the machines, moves each container’s build_cache onto the machine that owns it, and writes the new shape on the next save. A file that says version = 12 or later must use the new kinds; an old one is refused with the spelling to write instead.

This example contains the sections most installations need. Add other machine and runtime kinds from the examples above rather than mixing fields between variants.

version = 18
[phone]
enabled = true
bind = "127.0.0.1:3765"
tailscale_detect = true
[review]
enabled = false
profile = "claude-review"
[profiles.codex-work]
kind = "codex"
home = "/home/me/.codex"
[profiles.claude-review]
kind = "claude"
home = "/home/me/.claude"
[bundles.product]
primary_repo = "product"
[[bundles.product.repositories]]
id = "product"
github = "acme/product"
destination = "product"
[machines.local.build_cache]
max_size = "100GiB"
[targets.localhost]
kind = "bare"
[targets.podman]
kind = "podman"
pull_policy = "auto"

These variables affect the running controller or its companion processes. Set them in the environment that starts the daemon, then run mj daemon restart.

VariablePurpose
MJ_CONFIG_DIRDirectory containing config.toml.
MJ_DATA_DIRDirectory containing the SQLite store, recovery archives, logs, viewer state, diagnostics, and project memory.
MJ_WORKER_BINARYExact worker binary; highest-priority worker override and must name a file.
MJ_WORKER_DIRDirectory containing architecture-named portable Linux workers.
MJ_WORKER_URLFallback worker URL template; {target} expands to the target triple. Requires MJ_WORKER_SHA256.
MJ_WORKER_SHA256Required 64-character hexadecimal digest for MJ_WORKER_URL.
MJ_DESKTOP_BINARYPath to mj-desktop used by mj app.
MJ_CONTROLLER_BINARYPath to mj when mj-desktop cannot find its sibling controller.
MJ_VOICE_WORKERPath to the local dictation helper.
MJ_INSTANCEInstance name; same effect as --instance.
MJ_SSH_MAX_CONCURRENTCap on concurrent SSH connections per host; see the SSH target guide.
MJ_SSH_SESSIONS_PER_CONNECTIONSessions per shared OpenSSH connection; defaults to 8. See the SSH guide.
MJ_SSH_CONTROL_MASTERSet to 0 to disable SSH connection sharing (ControlMaster) for diagnosis.
MJ_DEV_RESTART_STALE_DAEMONWhen set to any value, a client restarts a running daemon whose executable was replaced, or whose development workers changed, since it started, unless the daemon’s build is newer than the client’s. For development checkouts.
MJ_TURN_STALL_TIMEOUT_MSMilliseconds of harness silence, with no tool call open, after which the worker ends the turn with the reason harness_inactive. Off unless set to a positive value.
MJ_TURN_TOOL_STALL_TIMEOUT_MSMilliseconds one tool call may run before the worker ends the turn the same way. Off unless set to a positive value.
MJ_GITHUB_CLI_BINPath or command name for the GitHub CLI used to read tokens.
RUST_LOGTracing/log filter for Mjolnir processes.
GH_TOKEN, GITHUB_TOKENGitHub token source, checked in that order before gh auth token, for private clones and live non-local session sync.
GIT_SSH_COMMANDOverrides Mjolnir’s non-interactive SSH command for checkpoint/archive Git operations.

Worker lookup checks MJ_WORKER_BINARY, MJ_WORKER_DIR, packaged or sibling workers, the native mj-worker companion for a bare runtime on this machine, and finally the verified URL fallback. The normal release installer already supplies both supported portable Linux worker architectures.

Harness-home variables influence automatic first-run discovery and explicit mj setup reruns. Existing profiles retain their configured homes:

HarnessDiscovery variableConventional home
CodexCODEX_HOME~/.codex
Claude CodeCLAUDE_CONFIG_DIR~/.claude
Kimi CodeKIMI_CODE_HOME~/.kimi-code
Grok BuildGROK_HOME~/.grok
Muse CodeXDG_CONFIG_HOME (parent of home)~/.config/muse
OpenCodeXDG_CONFIG_HOME (parent of home)~/.config/opencode
MiMo CodeMIMOCODE_HOME~/.mimocode (the stock install keeps config/ and data/ in XDG)

The release installer separately accepts MJOLNIR_INSTALL_DIR (preferred over INSTALL_DIR), MJOLNIR_GITHUB_OWNER, MJOLNIR_VERSION, GITHUB_TOKEN, and PROFILE. See Install Mjolnir.

The platform data directory, or MJ_DATA_DIR, contains operational state:

  • mj.sqlite3 for sessions, workspaces, drafts, read frontiers, remembered resource choices, and prompt history;
  • sessions/ for recovery archives;
  • projects/<project-key>/memory/ for canonical project memory;
  • logs/ and daemon.log for logs;
  • daemon.json for daemon discovery;
  • api-token, phone-cookie-key, and phone-cookie-revocations.json for API and viewer credentials and logout records; and
  • diagnostics/ for diagnostic reports.

Session replicas sync with canonical project memory at checkpoints using a line-by-line three-way merge, including deletions. If sessions change the same lines, an available utility model merges them; otherwise the session being merged wins. Mjolnir does not create a conflicts folder.

Claude Code uses native project memory. Other harnesses read and write their session replica directly with their own file tools; startup context gives them its path and describes the MEMORY.md index convention.

Do not hand-edit the database or daemon files. Use the TUI, viewer, and commands in the CLI reference. See Durability and recovery before moving or deleting session archives.