Configuration Reference
Settings resolve in layers, each overriding the one before it, field by field:
- Global:
~/.agent-of-empires/config.toml - Profile:
~/.agent-of-empires/profiles/<name>/config.toml - Repo:
.agent-of-empires/config.tomlin the project root
Unset fields inherit from the layer above. List fields replace rather than extend. Everything below is also editable from the TUI settings screen (s) and, unless noted, from the web dashboard.
Global-only settings use the global config. On upgrade, the default profile’s values for them move there, and other profiles’ values are removed. PATCH /api/profiles/<name>/settings rejects global-only fields with HTTP 400; use PATCH /api/settings instead.
Over the web API, GET /api/settings returns the settings as they apply to the profile the server serves (or ?profile=<name>): the profile’s overrides over the global config. PATCH /api/settings saves each field to the layer it belongs in, that profile for fields it can override and the global config for the rest, so a save always lands where the read looks. Add ?layer=machine to either to read or write the global config alone.
A project registry entry can also override worktree.enabled, session.smart_rename and sandbox.enabled_by_default for that project, from the web Projects view or the TUI add-project form. This override wins over all three layers. It lives in your own registry (projects.json), not the repo, so it does not weaken the repo = "deny" policy on any of them.
File locations
| Platform | Global config |
|---|---|
| Linux | $XDG_CONFIG_HOME/agent-of-empires/config.toml (default ~/.config/agent-of-empires/) |
| macOS | ~/.agent-of-empires/config.toml, or the XDG path when XDG_CONFIG_HOME is set or that directory already exists |
On macOS nothing is moved for you: an existing ~/.agent-of-empires/ keeps being used even after you set XDG_CONFIG_HOME.
~/.agent-of-empires/
config.toml # Global configuration
state.toml # Runtime/UI bookkeeping (auto-managed)
trusted_repos.toml # Hook trust decisions (auto-managed)
.schema_version # Migration tracking (auto-managed)
profiles/default/
sessions.json # Session data
groups.json # Group hierarchy
config.toml # Profile overrides
logs/
state.toml holds global-only bookkeeping (tour seen, last browse directory, sort order, dismissed tips and updates, and the agent hook approval described below). It is not a setting: it has no profile or repo layer. The TUI and the web dashboard both read some of these fields, the TUI writes several of them, and the CLI writes a flag when asked; the agent hook approval below is set by aoe hooks approve and is hand-editable. GET /api/settings still reports these under app_state.*, but PATCH rejects writes to them.
Environment variables
| Variable | Description |
|---|---|
AGENT_OF_EMPIRES_PROFILE |
Default profile |
AOE_LOG_LEVEL |
File log level: trace, debug, info, warn, error |
AGENT_OF_EMPIRES_DEBUG |
Legacy alias for AOE_LOG_LEVEL=debug |
AOE_DEFER_SANDBOX_MIGRATION |
Start without moving sandboxed sessions’ agent stores; retried later or by aoe migrate. See Per-session agent stores |
Theme
[theme]
name = "default" # see the picker or `aoe theme list`
color_mode = "truecolor" # truecolor | palette (TUI only)
name applies to both the TUI and the web dashboard. Builtins: default, empire, phosphor, tokyo-night-storm, catppuccin-latte, dracula, rose-pine, deep-ocean. color_mode = "palette" downsamples the TUI to xterm-256 for transports that mangle 24-bit color (some mosh setups); the web dashboard is always truecolor.
Custom themes are TOML files in ~/.agent-of-empires/themes/, listed in the picker under their filename:
aoe theme export empire # writes ~/.agent-of-empires/themes/custom-empire.toml
aoe theme list
aoe theme dir
Every field is optional. Missing colors fall back to the Empire baseline, except unread and favorite, which inherit the theme’s own accent. An omitted appearance or [syntax].shiki_theme is derived from the theme’s background luminance. appearance = "dark" | "light" and [syntax].shiki_theme (any id Shiki bundles) drive the dashboard’s surface ramp and code highlighting.
Session
[session]
default_tool = "claude"
yolo_mode_default = false
agent_status_hooks = true
smart_rename = true
name_agent_session = false
auto_stop_idle_secs = 0 # 0 disables; e.g. 7200 = stop after 2h idle
row_tag = "branch" # none | auto | profile | sandbox | branch
sidebar_position = "left" # left | right; TUI session list
After your 31st session creation, a one-time tip explains how to hide the shortcut bar. It waits until the home screen is free, respects Show tips, and never changes the setting. You can browse tips from Ctrl+K > Show tips even with the bar hidden.
| Option | Default | Description |
|---|---|---|
default_tool |
(auto-detect) | Default agent for new sessions, including a custom agent name. Falls back to the first available tool. |
yolo_mode_default |
false |
Start sessions with permission prompts skipped. In the terminal view this passes --dangerously-skip-permissions; in the structured view it maps to ACP bypassPermissions (see Modes and YOLO). |
auto_stop_idle_secs |
0 |
Seconds a plain tmux session may sit Idle before its tmux session and container are killed, leaving a restartable Stopped row. Idle age counts from the later of the last Idle transition and the last interaction, a session with an attached tmux client is spared, and the check runs about once a minute. Structured view workers use acp.auto_stop_idle_secs. |
prevent_sleep_when_active |
false |
Have the aoe serve daemon hold an OS sleep inhibitor (caffeinate -i, systemd-inhibit) while any session is active. Global only; a TUI without a daemon gets nothing. |
prevent_sleep_idle_grace_minutes |
15 |
Minutes (0 to 240) every session must stay idle before the inhibitor is released. A session that never reaches Idle (Waiting on a prompt, Creating forever) holds it indefinitely. |
session_id_poller_max_threads |
50 |
Ceiling on concurrent session-id pollers per process. Past the ceiling, an overflow session’s id is not refreshed until it gets a poller; starting one is retried on a 5 s to 60 s backoff. Global only, applied at process start. |
row_tag |
"branch" |
Metadata next to a TUI session title: none, auto (profile code in all-profiles view), profile, sandbox, or branch. |
show_activity_age |
true |
Show the age column at the right edge of each TUI session row: time since the agent stopped on Idle rows, time since last access on Unknown rows, and remaining snooze time under the Attention sort. |
sidebar_position |
"left" |
TUI session sidebar position: left or right. Global only. Narrow terminals keep the stacked layout. |
show_shortcut_bar |
true |
Show the TUI’s bottom shortcut row, including the LIVE banner. Global only, under Settings > Global > Interaction. Hiding it gives that row to the main view; shortcuts, ? help, dialog controls, and temporary notifications still work. |
preview_link_prefixes |
[] |
Link targets beyond http and https that a click in the preview may open, as case-insensitive scheme://authority/ prefixes, for example ["vscode://file/"] to open file links in VS Code. Entries not in that form, including a missing / after the authority, are ignored, and file:, javascript:, data: and vbscript: never open. Unlisted targets are neither opened nor passed to the terminal; a new entry applies to links printed after it. Global only and not editable from the web dashboard; under Settings > Global > Interaction (advanced). |
tie_workdir_to_name |
true |
Keep a managed worktree session’s directory named after its title. See Worktrees. |
pre_trust_agent_folders |
false |
Pre-trust each host session’s worktree in the agent’s own config (Claude Code, Codex, Gemini) so it does not open on a folder-trust prompt. Config-dir overrides are honored, and an agent_config_dir entry wins over them. Trust also activates the repo’s .claude/settings.json, hooks included, so enable it only for directories you would have trusted by hand. Sandboxed sessions always pre-trust their own staged config. |
agent_status_hooks |
true |
Install status-detection hooks into the agent’s config; see Agent hook approval for the approval that gates it and Adding a New Agent for the formats. Disabling it leaves status to pane reading but keeps identity hooks used for native resume. |
opencode_preassign_session_id |
false |
Pre-assign OpenCode’s native session id before a host launch (about two seconds per session) so resume captures it. Unsupported for sandboxed OpenCode. |
smart_rename |
true |
Auto-rename a still-default-named structured session from its first turn, using the session’s agent in one-shot mode. Title only; a session you named is never touched. Skipped for agents with no one-shot mode and command-overridden agents. Overridable per project. |
smart_rename_agent |
"" |
Agent used for one-shot utility calls (the rename title and the conversation summary). Empty means the session’s own agent. A sandboxed session only mounts its own agent’s credentials, so a different value makes it ineligible instead of falling back. |
smart_rename_model |
{} |
Per-agent model for the rename one-shot, e.g. { claude = "haiku" }. An absent key uses the agent’s built-in default, an empty value forces the CLI default, and any other value is passed to the agent’s model flag. |
name_agent_session |
false |
For a session whose title was typed in the TUI’s New Session dialog, give its title at the first launch (including a rename in AoE before then) to the agent as its own session name, so it shows in the agent’s own apps (Claude’s --name). For a structured session that is its first terminal launch. Skipped for a generated or suggested title, a title starting with -, a launch that resumes a conversation, sandboxed sessions, a launch whose command, arguments or PATH AoE cannot attest (including your own -n/--name), and when the agent’s --help does not list the flag. |
inherit_host_environment |
false |
Forward AoE’s whole environment to host sessions. See Host environment. |
agent_extra_args |
{} |
Per-agent arguments appended after the binary, e.g. { opencode = "--port 8080" }. Ignored for structured view sessions. |
agent_command_override |
{} |
Per-agent command replacing the binary. Managed resume and fork validate the actual native command and store; opaque wrappers require an explicit execution contract. See execution identity and wrappers. |
custom_agents |
{} |
User-defined agents (name to command). See Custom agents. |
agent_detect_as |
{} |
Built-in status detection and ACP adapter inheritance for a custom agent. This is not authority for terminal resume or fork. |
agent_execution_as |
{} |
Explicit native-agent contract for an opaque terminal wrapper. Requires agent_config_dir naming its store. Trusted global/profile configuration only; repository overrides are refused. |
agent_acp_cmd |
{} |
ACP launch command that makes a custom agent structured-view capable, e.g. { "oc-superpowers" = "ocp run sp acp" }. Split into argv and run with no shell. |
agent_config_dir |
{} |
Config directory an agent reads instead of its built-in default, keyed by agent name. Wins over the agent’s config-dir environment variable. Two names pointing at the same agent are two accounts of it; a restart that swaps between them carries the conversation across. A resumed host or structured Claude conversation keeps the store it recorded, which outranks this entry; a sandboxed one keeps the store its container was created with and follows the entry only once that container is recreated, see Session Resume. Global/profile only. |
agents.<name>.status_map |
{} |
Trusted hook-event to status mapping (running, waiting, idle, error), applied on the next hook install. Hooks receive AOE_PROFILE, so a script can read the resolved map with aoe -p "$AOE_PROFILE" profile show --status-map <agent> --json. Global/profile only. |
agents.<name>.status_rules |
[] |
Declarative pane status rules. See Status rules. Global/profile only. |
Per-agent structured view defaults live under [acp], not [session]:
| Option | Default | Description |
|---|---|---|
acp.offer_structured_in_new_session |
false |
Offer the structured view in the TUI: the Structured toggle in the new-session dialog, and switching a terminal session into it. Off, every TUI-created session is a terminal. The web dashboard always offers it. |
acp.default_new_session_view |
auto |
View the new-session dialog starts on when the agent supports the structured view: auto, structured, or terminal. auto keeps each surface’s default, structured on the web dashboard and terminal in the TUI. The dialog’s toggle still wins per session. |
acp.acp_defaults.<agent> |
{} |
model, effort (thinking level), mode, and an effort_by_model map applied when a worker starts. effort and mode go through the agent’s ACP config options when advertised and are skipped with a warning otherwise. A model chosen at creation wins over model, unless pin_model = true makes it a pin: creation then refuses any other model (kind = "model_pinned") and the dashboard’s picker collapses to it. The entry is keyed by the agent the session spawns as, so a custom agent reads its agent_detect_as base’s entry. |
acp.restrict_agents |
false |
Restrict structured view sessions to acp.allowed_agents. Read from the global config only, so a profile cannot widen it; changing it from the web needs the passphrase step-up. |
acp.allowed_agents |
[] |
Registry keys allowed while the restriction is on, e.g. ["claude", "codex"]. Each alias counts separately, and an empty list denies every agent. A worker on a now-disallowed agent is terminated at its next respawn. |
acp.rate_limit_auto_resume |
false |
Respawn a worker parked on a provider rate limit once the reported reset passes. See Rate-limit recovery. |
The rest of [acp] tunes the structured view globally; see Structured View Internals.
Recovering hook-based status detection
AoE writes per-session status into /tmp/aoe-hooks-<euid>/. When that directory cannot be initialized (another user squatted the path, a permissive umask or ACL widened it, a symlink was planted, or /tmp was reaped mid-run), AoE logs the resolved path with a recovery hint and falls back to reading the pane, which costs latency but no functionality. Check it with ls -ldn /tmp/aoe-hooks-$(id -u), which should be drwx------ and owned by you. Removing the directory (rm -rf /tmp/aoe-hooks-$(id -u)) and starting a session recreates it, a wrong ACL clears with setfacl -b, and a path owned by another user has to be removed by them or by root, since /tmp is sticky. aoe uninstall removes the hooks from every agent settings file and tears the directory down, leaving status detection on pane content alone.
Status hooks
Local shell commands run by the TUI on a status change, for desktop notifications and similar personal automation. Off by default, and global/profile only because they run arbitrary commands.
[status_hooks]
enabled = true
on_waiting = "notify-send -a aoe 'AoE: Waiting' \"$AOE_SESSION_TITLE is waiting for input\""
on_error = "notify-send -u critical -a aoe 'AoE: Error' \"$AOE_SESSION_TITLE errored\""
on_starting, on_running, on_waiting, on_idle, and on_error fire on that transition; on_change fires on every transition, after the status-specific command. A status must hold for a 100 ms debounce before a hook runs. Commands run in the session’s project directory, are best-effort, and never block status updates or sounds.
Each command receives AOE_SESSION_ID, AOE_SESSION_TITLE, AOE_PROJECT_PATH, AOE_PROFILE, AOE_TOOL, AOE_GROUP_PATH, AOE_OLD_STATUS, AOE_NEW_STATUS, and AOE_STATUS_CHANGED_AT.
Agent hook approval
agent_status_hooks (above) makes AoE write hook entries into the agent’s own config, which lives under your home directory unless agent_config_dir, the profile environment, or a config-dir variable exported in the launching shell moves it, so status comes from the agent reporting it rather than from reading its pane. That writes into files you own and runs a command whenever the agent fires a hook, so it is gated behind a one-time approval. The TUI offers it as a dialog when you create a session that writes to the host; aoe hooks approve is the same approval for a launch with no TUI, and aoe hooks status reports the current answer alongside the files and hook events the effective profile resolves. That list is a disclosure, not a manifest: a launch that routes through a native store, merges into a selected agent, or targets a selected or recorded Claude conversation store resolves its own target at launch time.
The approval is per installation and is not bound to a profile, so every profile and every agent resolves its own paths under it. One agent’s hooks also change launcher state: installing Kiro hooks may run kiro-cli agent set-default aoe-hooks, which Kiro keeps as its persistent default, so it affects later Kiro sessions including ones outside AoE. aoe hooks status prints that next to the files. There is no revoke command: set has_acknowledged_agent_hooks = false in <app_dir>/state.toml to take it back. A sandboxed session stages its hooks inside its own container config and is never gated. It is not the repo trust gate: aoe add --trust-hooks covers the hooks a repository declares in .agent-of-empires/config.toml and its project-local MCP servers, which are a separate decision. See Hook trust.
Turning agent_status_hooks off stops AoE installing status hooks, but it does not end the gate: identity hooks, which native resume depends on, stay installed. How many agents remain gated then depends on which ones declare an identity event, so check aoe hooks status for the list under a given profile. A session launched with its own command resolves the file that command names, which the TUI creation dialog describes exactly. aoe hooks status has neither a session nor a project directory: it resolves each tool from the profile config, so for a repository that sets session.agent_detect_as it can name a different agent than the same session launched inside that repository.
Custom agents
Custom agents name commands AoE cannot detect as a built-in binary: SSH wrappers, local scripts, or a CLI pointed at a second account. Configure them once, then pick the name in the TUI picker, aoe add --tool <name>, or the web wizard.
[session]
default_tool = "lenovo-claude"
custom_agents = { "lenovo-claude" = "ssh -t lenovo claude" }
agent_detect_as = { "lenovo-claude" = "claude" }
custom_agentsmaps a display name to the command AoE runs in a tmux pane.agent_detect_asreuses built-in status detection and ACP adapter inheritance. It never proves which native CLI owns a terminal conversation.agent_execution_asdeclares the native agent actually invoked by an opaque wrapper, together with itsagent_config_dirstore contract. This is required for managed resume and fork through such a wrapper. Bare, non-path wrappers can receive Default automatic resume flags and pane-scoped capture without this contract. Cleared launches selected by a failed-resume marker or disabledauto_resume_on_restartstart fresh, mint and pass fresh-session flags where supported, and capture the new pane-scoped ID; explicit resume and fork remain unavailable. See Execution identity and wrappers.agent_acp_cmdgives the agent its own ACP command (see below).agent_config_dirnames the config directory the wrapper points its CLI at (see below).
Custom agents remain selectable even for remote or unmanaged commands. Profile maps replace global maps, so redeclare entries you want to keep. agent_execution_as and agent_config_dir are trusted global/profile declarations; repository overrides are refused. The web wizard can select a custom agent but never edits these command strings.
One CLI, two accounts
A wrapper that runs the same CLI against a second login usually exports the agent’s config-dir variable, which AoE cannot see: the wrapper sets it after AoE has chosen which file to write. Name the directory in agent_config_dir so folder-trust records and native MCP discovery land on the config the agent actually reads, for as long as the conversation has not recorded a store of its own.
[session.custom_agents]
claude-personal = "claude-personal" # a wrapper that exports CLAUDE_CONFIG_DIR
[session.agent_detect_as]
claude-personal = "claude"
[session.agent_execution_as]
claude-personal = "claude"
[session.agent_config_dir]
claude-personal = "~/.claude-personal"
The value is a host path. Host sessions use the directory itself; each sandboxed session gets a private sandbox-v2/<instance-id> child mounted at the agent’s canonical container config path, so do not mount that tree through sandbox.extra_volumes and keep the config-dir variables AoE sets inside the container. Once a host Claude conversation has recorded a store, repointing the entry moves new sessions only: the folder-trust record still lands in the directory named here, and a host terminal launch logs a warning naming both stores. See Native Session Resume for the account swap that carries a conversation across.
Status rules for custom agents
agent_detect_as only fits a wrapper whose output matches the built-in it aliases. For a harness that merely resembles one, declare pane rules instead:
[session.custom_agents]
gjc = "gjc"
[[agents.gjc.status_rules]]
status = "waiting"
contains = "(y/n)"
[[agents.gjc.status_rules]]
status = "running"
regex = "esc to interrupt|thinking"
Each rule sets status (running, waiting, idle, error) and exactly one of contains (case-insensitive substring) or regex (Rust regex; use (?i) for case-insensitive). Rules are matched in order against the ANSI-stripped pane; first match wins and no match reports idle, so put the specific states before broad ones. Rules outrank agent_detect_as, a built-in detector of the same name, and a status hook the agent writes. Invalid rules are skipped with a warning and take effect on the next config resolve.
OMP terminal status
Built-in OMP detection reads the visible pane and locates activity, pinned errors, and blocking dialogs relative to the current composer. Question dialogs remain waiting when paging hides the selected option; session pickers accept bracketed key legends. Extension status rows, including plugin badges, do not need agent-specific matching rules. Editable composer text is excluded from dialog controls, even without status segments.
A recognized working terminal title supplements visible controls; it does not override a blocking dialog or pinned error. Visible retry and compaction controls can indicate activity even when the turn timer is stopped. Disabling OMP title updates or terminal program-status reports does not disable visible-control detection.
Untimed interrupt hints must start in the native two-space key column; alignment padding alone does not supply activity evidence. Custom editors or widgets can hide or replace these controls. Without a recognizable turn timer or working title, passive text that exactly reproduces a loader row remains indistinguishable from that control. Use the explicit status rules above for such interfaces; they remain authoritative, including their no-match behavior.
Running a custom agent in the structured view
Give the agent an ACP launch command; it must speak the Agent Client Protocol.
[session.custom_agents]
"oc-superpowers" = "ocp run sp"
[session.agent_acp_cmd]
"oc-superpowers" = "ocp run sp acp"
The value is split into argv and executed with no shell, so wrap explicitly for shell features (sh -lc '...'). The name must match a custom_agents entry and cannot shadow a built-in.
A wrapper around a supported agent needs no agent_acp_cmd at all: map it with agent_detect_as and it inherits the base agent’s ACP adapter, which also lights up Switch to structured view on an existing terminal session. The wrapper binary is never executed in that case, so anything it does outside the CLI (selecting an account, gateway, or profile by setting env) does not apply. Inheritance works only for bases with a built-in adapter (claude, codex, opencode, gemini, vibe, pi, omp, kimi, prime-agent).
To pass those overrides to a host structured session anyway, use the session’s extra_env or environment. A Docker-sandboxed session reads only sandbox.environment and pins its own config dir at a container path, so set the container-side value there, or give the wrapper a real agent_acp_cmd. An explicit agent_acp_cmd wins if you set both.
Agent command overrides
An override replaces the command AoE launches for an agent, which is how you run it with fixed options, through a script, or under a sandbox such as nono.
[session.agent_command_override]
opencode = "nono run --profile opencode-dev --allow-cwd -- opencode"
Set the same thing in the TUI under Agents, using <agent>=<cmd>, or per session with aoe add --cmd-override <CMD>. Overrides are evaluated per-session first, then profile, then global (a repo config cannot set one).
A configured override also applies to plain aoe add --cmd <agent>, and the on-PATH check validates the resolved override binary, so a session works when only the wrapper is installed. Native conversation resume survives an override only when the command starts with the built-in’s exact binary token, or is a single bare token, and contains no shell control syntax; see session resume.
The web wizard previews the resolved command in its Agent panel, including the ACP registry args a structured view session adds (opencode acp). Extra args are ignored for structured view sessions, so change the command override instead.
An override runs through your $SHELL, falling back to bash when $SHELL is unset or non-POSIX (fish, nu, pwsh). If your wrapper is a function or abbreviation in a non-POSIX shell, write it as a bash script or spell the command out here.
Host environment
environment = [
"CLAUDE_CONFIG_DIR=/Users/me/.claude-accounts/work",
"GH_TOKEN=$AOE_GH_TOKEN",
"TERM",
]
The top-level environment list injects variables into every host (non-sandboxed) session, in both views. Entries follow the same grammar as sandbox.environment: KEY=value literal, KEY=$VAR read from AoE’s environment at spawn, KEY=$$literal escape, and a bare KEY passthrough. Keys must match [A-Za-z_][A-Za-z0-9_]*; anything else is dropped with a warning.
In the terminal view each pair becomes a shell-assignment prefix on the pane command and is therefore visible in ps; the structured view applies the list to the agent process’s environment instead. For genuine secrets prefer sandbox.environment or a host hook. Host and sandboxed sessions read disjoint lists, so set both if a variable must be present either way. A profile’s environment replaces the global list.
What host sessions inherit automatically
Independent of that list, AoE forwards DISPLAY, WAYLAND_DISPLAY, XAUTHORITY, DBUS_SESSION_BUS_ADDRESS, SSH_AUTH_SOCK, and every XDG_* variable into host sessions, so a browser the agent launches (an OIDC login) can reach your desktop. Worth knowing what that grants: DISPLAY plus XAUTHORITY is X11 access to your whole session, which includes screen capture and input injection. A sandboxed session never receives them.
To forward everything else too:
[session]
inherit_host_environment = true
Every variable AoE holds then reaches host sessions, except AOE_* and AGENT_OF_EMPIRES_* (its own wiring) and TERM (owned by tmux). Off by default, since it widens what every agent can read, including tokens exported in your shell. In the terminal view the pairs ride tmux new-session -e, so a secret is briefly visible in ps while that command runs.
Managed OpenCode selects this policy before probing or launching the native agent. It also retains process essentials and OpenCode database, configuration and workspace selector inputs for validation when wholesale inheritance is off. Unsupported selectors remain unsupported. Explicit profile environment entries and before-session overrides take precedence; login-only exports are not imported. See managed OpenCode contexts.
When AoE has no environment to forward
Forwarding is a passthrough, not a store: AoE can only hand a session what its own process holds. A daemon started by systemd, launchd, cron, or an SSH command inherits that launcher’s environment, not your shell’s.
[Service]
PassEnvironment=DISPLAY XAUTHORITY XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS
EnvironmentFile=%h/.config/agent-of-empires/env
For a user unit, populate the manager from your graphical session and restart the unit (import-environment does not touch running units); make it permanent with ~/.config/environment.d/*.conf.
systemctl --user import-environment DISPLAY XAUTHORITY XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS
systemctl --user restart agent-of-empires
To see what a running daemon can forward, read its environment: tr '\0' '\n' < /proc/$(cat ~/.config/agent-of-empires/serve.pid)/environ on Linux, ps eww -o command= -p "$(cat ~/.agent-of-empires/serve.pid)" on macOS.
Host hooks
[host_hooks] commands run on the host, unlike [hooks], which for a sandboxed session runs inside the container. They compute a value with host-only tooling and credentials and hand only that value to the agent: the canonical case is minting a short-lived, repo-scoped token, or resolving which account a session runs as at spawn time.
[host_hooks]
before_start = ['echo "GH_TOKEN=$(my-mint-tool "$AOE_REPO_SLUG")"'] # sandboxed sessions
before_session = ['my-account-switcher env --profile "$AOE_PROFILE"'] # host sessions
before_destroy = ['sync-transcripts "$AOE_SESSION_ID"'] # any session, at purge
A launch runs exactly one of them, chosen by whether the session is sandboxed. before_start runs when a container is created or restarted (attaching to a running container reuses the last values); before_session runs on every host agent launch, including a restart or a view switch, and persists nothing. A plain tool session is not an agent launch and runs neither.
before_destroy is not a launch hook: it runs on the host for every session, sandboxed or not, when the session is purged, before container and worktree cleanup. With session.delete_to_trash on (the default), moving a session to trash does not run it; it runs when retention expires, on empty-trash, or on aoe remove --purge. Its output is ignored and never logged. A failure is logged without blocking removal, and a command still running after 2 minutes is killed.
Each KEY=VALUE line the command prints to stdout becomes an environment variable for the agent; other lines are ignored, stdout is never logged (so it is safe to print a secret), and a non-zero exit aborts the launch. A key that is a single token but not a valid identifier is dropped with a warning. Minted pairs are applied after the static environment list, so they win over a same-keyed entry.
For before_start the values reach docker through the process environment, never argv. In the terminal view a minted value rides tmux new-session -e, so it stays out of the pane command’s argv, but tmux keeps it in the session environment where any client of that tmux server can read it back with tmux show-environment. A value that must stay out of both belongs in a sandboxed session with before_start.
The command’s environment carries the lifecycle variables (AOE_SESSION_ID, AOE_SESSION_TITLE, AOE_PROJECT_PATH, AOE_PROFILE, AOE_TOOL, AOE_GROUP_PATH, AOE_SESSION_BRANCH on worktree sessions, and AOE_REPO_SLUG, the owner/repo of the project’s origin remote). In the structured view before_session receives the subset available at that spawn site: AOE_SESSION_ID, AOE_PROFILE, AOE_TOOL, AOE_PROJECT_PATH. before_start additionally receives the session’s sandbox environment, which is the per-session input channel: set TEST_VAR=foo in the new-session dialog’s env list and the hook reads $TEST_VAR. That env is resolved from the session, profile, or global sandbox.environment, never from a repo config.
host_hooks is global/profile only: a checked-out repository must not be able to run host commands.
tmux
[tmux]
status_bar = "auto"
mouse = "auto"
clipboard = "auto"
# socket_name = "aoe"
vt_live = true
| Option | Default | Description |
|---|---|---|
status_bar |
"auto" |
Paint aoe’s themed status bar (title, branch, sandbox, detach hint) on its own sessions. The bar is a whole theme, so "auto" steps aside whenever you have a tmux config at all. "disabled" reverts aoe’s session-scoped status* overrides, so your own config governs. See tmux status bar. |
mouse |
"auto" |
Set tmux mouse on aoe’s sessions, which is what turns a wheel or touch scroll into copy-mode scrollback. "auto" defers only when your tmux config sets mouse itself, and enables it otherwise. |
clipboard |
"auto" |
Forward the agent’s OSC 52 clipboard writes (set-clipboard on, allow-passthrough on) to your terminal or the dashboard. Without it, “select to copy” inside an agent silently fails. Same per-option "auto" as mouse; live-send forwarding stays on for "auto" and "enabled". |
socket_name |
unset | Run aoe’s sessions on a private tmux server (tmux -L <name>), so your own tmux ls stays separate. Bare name only, applied at the next aoe start. Global only. |
vt_live |
true |
Render agent previews and the dashboard’s agent terminal from a persistent VT channel instead of capture-pane polling. See the VT live transport. |
Per-option detection reads ~/.tmux.conf, $XDG_CONFIG_HOME/tmux/tmux.conf, and ~/.config/tmux/tmux.conf for a set / setw of the option. It is deliberately conservative: an option reached through source-file, if-shell, a false %if, or a key binding is not detected, so set the mode to "disabled" if you keep yours in one of those places. /etc/tmux.conf is not consulted.
systemd
[systemd]
enabled = true
slice = "background-aoe-{profile}.slice"
survive_daemon_restart = true
# memory_max = "4G"
tasks_max = 0
| Option | Default | Description |
|---|---|---|
enabled |
true |
Start every session through systemd-run --user --scope, so it runs in its own transient systemd scope. Sessions start unscoped when systemd-run is missing or the user manager is unreachable. false only prevents new scopes and skips the check below; stopping, deleting and trashing sessions still stop their existing scopes. Global only. |
slice |
"background-aoe-{profile}.slice" |
Template for the slice the session scopes live in; must end in .slice. {profile} expands to the session’s profile name, escaped for systemd: every byte other than ASCII alphanumerics, : and . becomes _<hex> (so - and _ are escaped too, as are spaces, / and non-ASCII characters). A value without {profile} is used as is. An invalid value falls back to the default with a warning. Global only. |
survive_daemon_restart |
true |
Keep sessions running when the aoe daemon service stops: agents live in their own scopes, outside the daemon’s cgroup. false binds the scopes the daemon creates to its service with PartOf=, so stopping or restarting the daemon service stops them, and systemd does not start them again; this only takes effect when the daemon itself runs as a .service, otherwise it is ignored with a warning. Scopes created from the CLI or TUI are never bound. Global only. |
memory_max |
unset | Memory cap for each session scope, a number followed by b, k, m, or g (e.g. 4G, 512M, 512b). Unset or empty means unlimited; any other value is ignored with a warning. Global only. |
tasks_max |
0 |
Task cap for each session scope (0 to 4194304). 0 means unlimited. Global only. |
Each session start runs the agent as systemd-run --user --scope, in a fresh scope aoe-session-<id>-<n>.scope, where <n> is the start time in unix milliseconds. Only the session id reaches systemd: the scope description is aoe session <first 8 chars of the id>, and aoe list maps the id to the title. With the default slice template the scopes sit in background.slice/background-aoe.slice/background-aoe-<profile>.slice; a custom slice has its own hierarchy. Inspect them with:
systemctl --user status 'aoe-session-*'
systemd-cgls --user
A session that changes profile keeps its old slice until its next start. To cap a whole profile or all of aoe, put limits on the slice itself with systemctl --user set-property background-aoe-<profile>.slice MemoryMax=8G or a drop-in. There <profile> is the escaped profile name, for example my_2dprofile for my-profile. The memory_max and tasks_max settings apply to each session scope on its own. Set slice to group sessions elsewhere, for example aoe.slice.
aoe checks once per process that systemd-run works with the configured limits, with --expand-environment=no so $ in session commands stays literal. The check is one attempt of at most 5 seconds. Sessions start unscoped, with one line in the log, when systemd-run or env is not on PATH, when systemd-run cannot reach the user manager or times out, when it rejects the configured memory_max/tasks_max (a warning naming them), or when enabled is false (systemd.enabled=false). If systemd-run fails after that check passed, the session start fails visibly: the tmux pane dies, and an ACP session reports an attach error after the dial deadline, with the systemd-run error in the runner log.
Sessions that are already running are not moved into a scope. They stay unscoped until you restart or recreate them; restarting aoe or the daemon does not change that. Stopping, deleting or trashing a session releases its scopes with systemctl --user stop --no-block, also when enabled is false, so scopes started before you turned it off are still stopped. One known limitation: aoe rm from the CLI does not shut down the ACP runner of a structured session, so that runner and its scope keep running. Trashing or archiving a tmux session releases them when its pane is killed (the default). ACP sessions release whenever their worker is shut down, for example on archive, snooze, idle auto-stop, rename or a provider switch, and when their first launch fails after the runner registered itself (a runner that exits on its own before that leaves no record, so its scope is not swept). Restarting a tmux session does not release the old scope: it ends when its processes exit. A tmux start or restart whose launch fails once its pane may exist, and a resume whose pane dies right away, release all of the session’s scopes, including the old one. aoe creates the scopes; systemd never starts sessions at boot or restarts them after a crash. The [systemd] settings are read once per aoe process, so changes take effect after aoe (including the daemon) restarts.
For a sandboxed session only the container CLI or runner process is in the scope, so memory_max and tasks_max do not constrain the container itself; use the sandbox settings for that.
The tmux server is not scoped. A tmux server started by the daemon lives in the daemon’s cgroup, and restarting the daemon service can kill it and every pane with it, although the agent processes are still in their session scopes. Either set KillMode=process on the daemon unit (this leaves the daemon’s other processes running after a stop), or select a dedicated tmux socket with tmux.socket_name whose server is started outside the daemon.
The daemon reports STATUS=<n> running, <n> waiting, <n> idle to systemd, visible in systemctl --user status <daemon unit>.
aoe list --json adds a systemd_unit field to live sessions that have a running process, when enabled is true and the user manager’s socket exists ($XDG_RUNTIME_DIR/systemd/private, $XDG_RUNTIME_DIR/bus or the socket in DBUS_SESSION_BUS_ADDRESS). Its value is the unit glob aoe-session-<id>-*.scope. It is the expected pattern, not proof that a unit exists, since the check does not call systemd.
Diff
[diff]
default_branch = "main" # auto-detected when unset
context_lines = 3
split_view = false # side-by-side instead of unified
Updates
[updates]
update_check_mode = "notify" # auto | notify | off
auto: install a new release silently in the background through the same tarball path asaoe update, picked up on the next launch. Only when the install location is writable; Homebrew installs fall through tobrew upgrade.notify(default): show the TUI banner and the CLI nag.Ctrl+xsnoozes the banner until a newer release ships.off: no check, banner, or dashboard poll. Use it on offline networks.
Checks hit GitHub at most once a day; the dashboard re-polls the cached status hourly while open.
Tools
[tools.*] defines dev tools tied to each session’s working directory. Each entry takes a command, an optional hotkey (Alt+<char>), and optional background = true for fire-and-forget commands.
[tools.lazygit]
command = "lazygit"
hotkey = "Alt+g"
See Tool Sessions for the full reference.
Worktree and sandbox
[worktree]
enabled = false # auto-enable for new sessions
path_template = "../{repo-name}-worktrees/{branch}" # {repo-name}, {branch}, {session-id}
auto_cleanup = true
[sandbox]
enabled_by_default = false
default_image = "ghcr.io/agent-of-empires/aoe-sandbox:latest"
environment = ["GH_TOKEN=$AOE_GH_TOKEN"]
Git Worktrees and Container Sandbox document the remaining keys in each section.
Profiles
Profiles are separate workspaces with their own sessions, groups, and overrides of profile-overridable settings.
aoe # "default" profile
aoe -p work # the "work" profile
aoe profile create client-xyz
aoe profile list
aoe profile default work
Repo config
Per-repo settings live in .agent-of-empires/config.toml; aoe init writes a template. A repo may set [hooks], parts of [session], [sandbox], and [worktree], and nothing else. See Repo Config & Hooks for which keys are accepted and why the rest are not.