Skip to content

Configuration

Agent Factory reads config from several layers, merged field by field:

  1. Global~/.agent-factory/config.toml: your personal defaults, applied everywhere.
  2. In-repo<repo-root>/.agent-factory/config.toml: checked into a repository, applied whenever af runs in that repo.
  3. Personal per-project~/.agent-factory/.agent-factory-projects/<project-id>/config.toml: your machine-local overrides for one registered project. Never checked in. See Personal per-project config.

Precedence is app defaults → global config → in-repo config → personal per-project: a higher layer overrides a lower one only for a field it actually sets, and program_overrides merges per key (a higher entry wins for that agent; lower entries for other agents still apply). The personal per-project layer sits above the checked-in in-repo file on purpose — the shared file is the team default, and a machine-local override exists precisely to beat it on your machine — but only preference keys admit it; repo-contract keys (backend, docker, ssh, hooks) never do, so a personal override can never rewrite repository reality.

Config is TOML — chosen so it is easy to hand-edit. If you are upgrading from a version that used config.json, see Migrating from JSON below; the change is automatic.

You can also read and write config from the CLI. Bare af config get <key> / af config list read the current repository's effective config; outside Git they fall back to global values. Add --repo <repository-path> to inspect another repository, and add --explain to see every candidate, whether it was present and allowed, and why it won or lost. Dotted reads such as af config get program_overrides.codex --repo . --explain show the source of one merged-table leaf. --project remains accepted as a deprecated read alias. The repository path is only a read-time selector: these commands do not register a project or write project identity. Displayed source locations preserve the selected/configured path spelling; symlinks are resolved only when paths must be compared for identity.

af config set <key> <value> writes a single settable scalar key in place, preserving all comments and ordering (it never regenerates the file) and validating the value first. Settable keys are the scalar tunables — default_program, program_overrides.<agent>, auto_update, listen_addr, require_token, require_loopback_token, preview_listen_addr, daemon_poll_interval, log_max_size_mb, log_max_backups, branch_prefix, on_archive_command, worktree_root, detach_keys, update_channel, vscode_server_binary, limit_auto_resume, limit_retry_interval, limit_patterns.<agent>, global_agent_skills, docker_mount_agent_credentials, ssh_host_key_verification, sandbox_ssh, and the comma-separated cors_allowed_origins list (which replaces the whole allow-list; each entry is validated as a scheme://host[:port] origin, "" clears it); the structural tables (root_agents, [root_agent], [theme], [keys]) have no single-scalar shape — the config assistant edits them for you (and validates), or edit config.toml directly and run af config validate. Without --project it edits the global config; with --project <id-or-path> it writes a personal per-project override instead (see Personal per-project config). See af config in the CLI reference. A global write is applied to a running daemon in place — no restart and no session loss — and the command prints when the key you just set takes effect: most keys are live at once, branch_prefix waits for the next daemon start, and the keys af itself reads (auto_update, update_channel, detach_keys) wait for your next af launch. A --project write and a raw hand-edit are read the next time the relevant operation resolves that project's config.

Global config

~/.agent-factory/config.toml:

default_program = "claude"
auto_update = true
daemon_poll_interval = 1000
branch_prefix = "username/"
on_archive_command = 'find . -type d -name node_modules -prune -exec rm -rf {} +'
worktree_root = "sibling"
detach_keys = "ctrl-w"
log_max_size_mb = 50
log_max_backups = 2
update_channel = "stable"
limit_auto_resume = false
global_agent_skills = false
limit_retry_interval = "30m"
session_env_passthrough = []

[program_overrides]
claude = "/home/me/.local/bin/claude --dangerously-skip-permissions"

[theme]
foreground = "#DCDCCC"
foreground_strong = "#FFFFEF"
foreground_muted = "#989890"
foreground_dim = "#656555"
background = "#3F3F3F"
background_subtle = "#494949"
background_panel = "#4F4F4F"
accent = "#8CD0D3"
success = "#7F9F7F"
warning = "#F0DFAF"
error = "#CC9393"
info = "#93E0E3"
purple = "#DC8CC3"
selection_background = "#4F4F4F"
selection_foreground = "#FFFFEF"
pane_border_default = "#989890"
pane_border_selected = "#8CD0D3"
pane_border_interactive = "#7F9F7F"
pane_border_preview = "#DC8CC3"
Field Description
default_program Default agent enum. Must be one of claude, codex, aider, gemini, amp, opencode, devin.
program_overrides Optional map from agent enum to the full command string used when launching that agent. Use this to pin a path or pass flags (e.g. --dangerously-skip-permissions). Keys must be one of claude, codex, aider, gemini, amp, opencode, devin. Agent-specific injection (claude's --plugin-dir flag, aider's --read flag, opencode's OPENCODE_CONFIG env var pointing at an af-owned config, devin's --respect-workspace-trust false to skip its workspace-trust modal, and — when global_agent_skills = true — the af skill file dropped into codex/gemini/amp/devin's own skills folder) and readiness detection follow the program the override actually runs, not the key: pointing an agent name at a different command (even a non-agent one like bash) launches it with no injected agent flags, and a command running no known agent counts as ready once its pane shows output. The agent is identified by command-token basename (/opt/tools/claude --model opus and ionice -c 3 claude are claude; /opt/claude-wrapper/run is not), so if you wrap an agent in a script, name the script after the agent to keep its flags and readiness behavior.
session_env_passthrough Extra exact environment variable names agent sessions may inherit. Default: none beyond af's built-in runtime, Git/GitHub, network, and selected-agent authentication allowlist; Docker requires explicit names because repo config selects its image. Global-only and hand-edited; values stay in the process environment and must not be placed in this list. See Session environment isolation.
auto_update Startup self-update. Defaults to true: an interactive af checks the configured update_channel on launch — at most once every 6 hours, so a relaunch inside that window costs nothing and makes no network call — and when a newer release exists it installs it, restarts the daemon from it (sessions survive), and relaunches you into the new version straight away. It never downgrades, never interrupts an af that is already running, and skips silently when the check fails or you are offline. It is also skipped whenever stdout is not a terminal, so a script or CI job that calls af keeps the binary it installed. A running daemon checks too, on the same 6-hour window and only when an interactive af has not just used it, so a box that never opens the TUI still records in its log when a newer release exists — the daemon only reports it; installing is still the launch path's job, or af upgrade. Set to false, or set AGENT_FACTORY_AUTO_UPDATE=0, to pin the installed version and stop both checks — no release lookup, no network call; af upgrade still works either way. A daemon can also be opted in to installing what it finds, through a transactional upgrade that preserves the previous binary and rolls back automatically if the new one does not come up healthy: set AGENT_FACTORY_DAEMON_UPGRADE=1 in the daemon's environment. It is off by default and auto_update = false still overrides it.
daemon_poll_interval Daemon polling interval in ms.
listen_addr Address the daemon serves the bundled web UI + HTTP/WS API on, over plain HTTP (no TLS). Defaults to 127.0.0.1:8443 (loopback), so a fresh install has a browser client at http://127.0.0.1:8443 that connects with no token and no login screen. Set it to "" to disable the web server entirely (pure-unix daemon); set it to a routable host:port like 0.0.0.0:8443 to expose it to the network — pair that with require_token = true unless you trust the network, because a tokenless network bind serves the control API to anyone who can reach it. af allows it and warns once at daemon start rather than refusing (see Remote daemon access). af serves no TLS either way, so front a routable listener with a TLS-terminating proxy or a private network. A web-port bind conflict is logged and skipped — it never blocks the daemon. Global-only. See The web client and Remote daemon access.
require_token Whether the web/TCP listener requires the bearer token for non-loopback (network) peers (default false — the token is off and auth is opt-in, so the bundled web UI opens with no login). What keeps that safe is the loopback-only listen_addr default. A network bind under this default serves an unauthenticated control plane to anyone who can route to it — including DeliverPrompt, which runs instructions through your agents. af permits that (the call is yours) and warns once at daemon start, in af config set, in af doctor, and in af daemon status; set require_token = true unless you trust every host that can reach the port. Note require_loopback_token does not substitute: it is inert while require_token is false. Set true to require the token from network peers (loopback stays exempt on a loopback bind — see require_loopback_token). The listener is plain HTTP, so the token travels over the connection as-is — front it with a proxy/private network. Global-only.
require_loopback_token Whether even loopback peers (127.0.0.1/::1) must present the bearer token on the web/TCP listener (default false). The default loopback web UI is reachable with no token, which grants any local process or user the same access as you — weaker than the unix control socket, whose 0600 perms restrict it to your account. On a shared/multi-user machine set both require_token = true and this true so a same-machine browser must present the token (af token show), or set listen_addr = "" to disable the web server. It only tightens the loopback path, so it is inert on its own: require_token = false (the default) disables the token for all peers, loopback included. Global-only.
preview_listen_addr Address for a separate plain-HTTP listener that serves web-tab previews (and, on a loopback fixed port, a per-session VS Code editor origin) on their own origin, kept apart from listen_addr (#1856). Defaults to "" (disabled) — no second port opens unless you set one. With it set, each web tab is served from its own http://af<label>.localhost:<port>/ origin whose root is the dev server's root, so an app's absolute-path assets (/assets/app.js) load with no base-path configuration, and the distinct origin keeps one preview from reading another or reaching the web UI's token. *.localhost is resolved by the browser to its own loopback, so this is same-machine only: a remote viewer keeps the same-origin sandboxed preview on listen_addr. A non-loopback value gains a remote browser nothing, but it is not harmless: *.localhost binds the browser, not the port, so any client that reaches the address can send Host: <tab>.localhost itself, and a tab's hostname is the only credential this listener checks — a network bind makes every tab hostname a network-reachable capability, and one leaked through a log, a screenshot, or browser history stops being usable only from this machine. Editor tabs are withheld entirely while it is network-bound, and it is warned about once at daemon start. Keep it on loopback. It accepts the same host:port grammar as listen_addr, and a bind conflict is logged and skipped, never fatal. It serves previews/editors only — never the daemon control API. See Web UI → per-tab preview origins. Global-only.
cors_allowed_origins Exact-match allow-list of browser origins permitted to call the API cross-origin, e.g. ["https://af.example.com"]. Empty (the default) emits no Access-Control-Allow-Origin, so no cross-origin browser can reach the API; non-browser clients (TUI/CLI, curl) are unaffected. The bundled web UI is same-origin and needs no entry here — this is for a web client you host yourself. Global-only. See Remote daemon access.
vscode_server_binary Binary that backs a VS Code tab (af sessions tab-create <title> --kind vscode, and the web UI's + menu). Empty (the default) detects one on the daemon's PATH: code-server first, then openvscode-server. af never bundles or installs either — when neither is found the tab still creates and the pane renders an install hint. Set a full path (a leading ~ is expanded) when the editor lives outside PATH or under another name; a configured path that is not executable is an error, never a silent fall back to detection. Global-only, like root_agents/listen_addr: it names a binary the daemon executes, so a repo's checked-in config must never be able to choose what af runs on your machine. See VS Code tabs.
branch_prefix Prefix for worktree branches (defaults to username/).
on_archive_command Optional operator-authored shell command run with the worktree as its current directory after every session pane has exited and immediately before the local worktree moves into the archive. Empty (default) disables it. It receives AF_SESSION_ID, AF_SESSION_TITLE, AF_REPO_ROOT, AF_WORKTREE_PATH, and AF_ARCHIVE_PATH, plus af's filtered session environment. A failure or 30-minute timeout is surfaced as a warning, but the archive still proceeds and remains restorable. Restore does not run a counterpart command; if this hook removes reconstructible dependencies, rerun their package manager after restoring. Set globally or in personal per-project config only; checked-in config is rejected because archiving a cloned repository must never execute repository-controlled code on the daemon host. af never supplies a default pruning command because it cannot distinguish disposable dependencies from deliberately retained fixture data. The example above is shaped to be safe to run on every archive in its scope: it succeeds when there is nothing to prune (a command that fails on a missing directory reports a hook failure for a healthy archive, and teaches you to ignore the warning that reports real ones), it reaches the per-package trees a workspace repo creates rather than only the root one, and -type d steps over a node_modules symlink so it never deletes through one into a package-manager store shared with your other worktrees.
worktree_root Where new worktrees are created: sibling (default, next to the repo as <repo>-<session>) or subdirectory (under ~/.agent-factory/worktrees/<branch>).
detach_keys Key combination that detaches from an attached session (defaults to ctrl-w).
log_max_size_mb Size cap in MB for agent-factory.log and the per-task watch-script logs before they are rotated (defaults to 50). Must be positive.
log_max_backups How many rotated logs (agent-factory.log.1, .2, ...) to keep per log file; older ones are deleted (defaults to 2). 0 keeps none.
update_channel Release channel that auto-update and af upgrade follow: stable (default) tracks manual 1.x.y releases only; preview opts into the automatic 1.x.y-preview-z prereleases cut every 3 hours. Any other value falls back to stable with a warning. See release-process.md.
root_agents Opt-in table of repositories that get an always-ensured root agent (default: none). See Root agents.
root_agent Singleton successor to root_agents: whether a registered project keeps a root agent and the command it runs (default: not enabled). A global default plus an optional personal per-project override; layers with the legacy root_agents map. See The [root_agent] singleton.
limit_auto_resume Opt in to the daemon auto-resuming a session parked at a usage-limit wall once its limit window elapses (default: false). See Usage-limit auto-resume.
limit_retry_interval Fallback retry cadence (Go duration, e.g. 30m) used only when limit_auto_resume is on and the limit banner carried no parseable reset time (default: 30m). Empty or 0 disables the fallback.
global_agent_skills Opt in to af writing its agent-factory skill file into your global codex/gemini/amp/devin config directories so those agents discover af's CLI guidance (default: false). See Agent guidance and your global agent config.
docker_mount_agent_credentials Opt in to a backend = "docker" session bind-mounting the operator's on-disk credential file for that session's own agent (only), read-only, so a containerised agent can authenticate (default: false). Global-only: a repo selects the docker image, but only the operator grants it credential access. See backends.md → Agent credentials in a container.
ssh_host_key_verification How the backend = "ssh" runtime verifies a remote host key: strict (default — verify, refuse an unknown or changed key), accept-new (trust-on-first-use: record an unknown key, still refuse a changed one), or insecure (no verification). Global-only: a repo selects ssh.host, but only the operator relaxes verification (a repo-settable waiver + repo-settable host would be a one-commit MITM). accept-new writes learned keys to an af-owned store under the AF home, never ~/.ssh/known_hosts. See backends.md → SSH backend.
limit_patterns Optional map from agent enum to a regex that overrides the built-in usage-limit detection banner for that agent (the built-in reset-time parser is kept). Default: none. See Custom usage-limit detection.
theme Optional TUI color table. Defaults to a Zenburn-derived palette and validates each value as #RRGGBB; invalid values fall back to the corresponding default with a warning. See Theme colors.
keys Optional keymap overrides for the TUI. See Key bindings.

Agent approval behavior

Agent Factory does not answer an agent's routine approval prompts. Configure that behavior in the agent itself, usually through program_overrides. These settings weaken or remove provider safety checks; use them only in an execution environment whose access matches the risk.

  • Claude: paste program_overrides.claude = "claude --dangerously-skip-permissions". Claude documents this as a full permission bypass intended for isolated containers or VMs. See Claude permission modes.
  • Codex: paste program_overrides.codex = "codex --ask-for-approval never". This disables approval prompts while keeping the sandbox selected by Codex's configuration. To disable both approvals and sandboxing, Codex also exposes --dangerously-bypass-approvals-and-sandbox; that is the higher-risk choice.
  • Aider: paste program_overrides.aider = "aider --yes-always". See Aider's option reference.
  • Gemini: paste program_overrides.gemini = "gemini --approval-mode=yolo". The older --yolo spelling is deprecated. See the Gemini CLI reference.
  • Amp: no flag or override is needed. Amp does not ask before running tools by default. Existing permission settings can opt back into prompts. See the Amp manual.
  • OpenCode: no flag or override is needed with its default permissions. If your OpenCode config currently asks, set "permission": "allow" in opencode.json; this is OpenCode configuration, not an af program_overrides line. See OpenCode permissions.
  • Devin: af launches devin in its own default permission mode, auto, which auto-approves read-only tools but still asks before edits and commands. For an unattended af session that would pause the agent on every edit, so to let devin edit and run without prompting, paste program_overrides.devin = "devin --permission-mode accept-edits" (or smart, which additionally auto-runs actions a fast model judges safe; dangerous auto-approves everything — the higher-risk choice). af leaves the default at auto rather than picking accept-edits for you, so an unattended devin session is opt-in to auto-editing, matching devin's own default. The env var DEVIN_PERMISSION_MODE works too but a program_overrides line is the af-native, per-repo way.

Agent Factory still dismisses a supported agent's one-time workspace trust dialog during first-run setup. That only gets a new session to a usable prompt; it does not approve later tool calls or actions.

Devin is the exception: af suppresses the dialog at launch instead of dismissing it, appending --respect-workspace-trust false — af created and owns the worktree, so it is already trusted. This applies to every launch path af controls, ordinary sessions and agent-assisted af config alike. Your own value wins if you set one, so include the flag in a custom program_overrides.devin only when you want to choose it; af appends it when your override omits it. The one way to still see the modal is to set --respect-workspace-trust true yourself: af leaves your explicit choice alone, and it has no dismissal for the dialog once it renders.

Codex's additional safety checks model-routing picker is separate from approval and sandbox flags. The daemon recognizes the known picker, navigates to Keep waiting by its label, confirms that row is selected before accepting it, and compares Codex's model status line before and after. The intervention and verification result are written to agent-factory.log; a changed picker that af cannot match safely is logged and left untouched.

Session environment isolation

Agent processes use a default-deny environment. af no longer copies every variable held by the shell or daemon into a session. This limits unrelated credentials from secret managers, databases, CI systems, and infrastructure providers from becoming ambient authority in a coding agent.

The built-in allowlist keeps the pieces sessions need:

  • Process and terminal basics: PATH, HOME, USER, LOGNAME, SHELL, TERM, COLORTERM, LANG, LANGUAGE, every valid LC_* name, TZ, TMPDIR/TMP/TEMP, PWD, XDG config/data/cache/state/runtime paths, terminal color preferences, and tmux's own TMUX, TMUX_PANE, and TMUX_TMPDIR markers.
  • Agent Factory state: AGENT_FACTORY_HOME, AGENT_FACTORY_AUTO_UPDATE, AF_HOME, AF_SESSION, AF_DAEMON_URL, and AF_DAEMON_TOKEN.
  • Git and GitHub authentication: GH_TOKEN, GITHUB_TOKEN, their Enterprise variants, GH_HOST, GH_CONFIG_DIR, SSH_AUTH_SOCK, Git SSH/askpass and config-file selectors, Git author/committer identity, and GPG agent/home selectors. Stored gh login, Git credential helpers, and native keyrings continue to work through HOME, XDG paths, and the desktop session bus.
  • Network access: upper- and lower-case HTTP/HTTPS/all/no-proxy variables plus the standard OpenSSL, Node, Requests, and curl custom-CA paths.
  • Authentication for the selected agent only: Claude gets Anthropic/OAuth, and gets each Bedrock, Vertex, or Foundry credential group only when that mode's CLAUDE_CODE_USE_* selector is exported or is a literal assignment in the resolved command (for example, program_overrides.claude = "CLAUDE_CODE_USE_BEDROCK=1 claude"). A command-local selector is trusted only when the entire command is one literal Claude invocation (optionally through env or exec) or af's own generated agent-server handoff. Compound commands, redirects, arbitrary wrappers, and dynamic words require exporting the selector before starting af, or explicitly listing the provider credential names. Codex gets OPENAI_API_KEY, CODEX_API_KEY, CODEX_ACCESS_TOKEN, CODEX_HOME, CODEX_SQLITE_HOME, and its CA path; Gemini gets Gemini/Google API keys, and gets Google Cloud's credentials (GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION) only when one of its own cloud modes is selected by GOOGLE_GENAI_USE_VERTEXAI or GOOGLE_GENAI_USE_GCA, on the same exported-or-inline terms as Claude's; Amp gets AMP_API_KEY and AMP_HOME; Aider and OpenCode get their common model-provider API keys and their own config locations, and no cloud-infrastructure credentials. File- or keyring-backed logins remain preferred because they do not put a credential in any environment at all.

OpenCode against Bedrock or Vertex needs its cloud credentials listed explicitly in session_env_passthrough (for example AWS_PROFILE, AWS_REGION, and whichever of AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/ AWS_SESSION_TOKEN or AWS_SHARED_CREDENTIALS_FILE your setup uses). Unlike Claude and Gemini, OpenCode has no environment variable that selects a cloud provider — it chooses one inside its own config file or model id — so there is no mode signal af could gate those credentials behind. Since the alternative is handing them to every OpenCode session unconditionally, af requires the operator to name them. That matters because the agent a session runs is repo-settable through default_program/program_overrides: without this, a cloned repository could swap the agent to OpenCode and inherit your cloud credentials.

Docker is a stricter trust boundary. A repository selects its container image, so af does not automatically send that image any built-in agent, GitHub, proxy, or CA variable. Add each required name to session_env_passthrough only after trusting the configured image, preferably at an immutable digest. Local, SSH, and hook launches retain the built-in selected-agent behavior; SSH reads matching values from the remote account rather than copying the daemon's. If the Docker client itself needs a proxy or private CA to reach its daemon or registry, list those exact names too.

Claude's cloud modes are selected by CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, and CLAUDE_CODE_USE_FOUNDRY, and turning one on grants the session that provider's whole credential group — your AWS keys, Google application credentials, or Azure client secrets. af reads that selector from your environment or from the resolved agent command, so program_overrides.claude = "CLAUDE_CODE_USE_BEDROCK=1 claude" in your global config works as written.

A repository may not do the same. program_overrides is one of the keys an in-repo .agent-factory/config.toml may set, and a checked-in value carrying one of those assignments is rejected at load with an error naming the selector: otherwise cloning a repository and starting a session would hand that repository's agent your cloud credentials. A repo may still choose which program runs — a path, flags, a wrapper — because that grants nothing. Set the selector in your own global config or export it in your shell if you want it.

Gemini's cloud modes work the same way, selected by GOOGLE_GENAI_USE_VERTEXAI or GOOGLE_GENAI_USE_GCA. Choosing which agent runs is also a repo's to make — default_program and program_overrides are both repo-settable, and swapping the program is a legitimate thing to do — so no agent's allowlist carries cloud credentials unconditionally. Whichever agent a session ends up running, reaching your AWS, Google Cloud, or Azure credentials takes a selector you set or a name you listed in session_env_passthrough.

An agent wrapper that hides the real executable name, a custom Codex model provider whose env_key is user-defined, or a less-common Aider/OpenCode provider may need another variable. Add its name, never its value, to the global config:

session_env_passthrough = ["CUSTOM_PROVIDER_TOKEN"]

Entries are exact POSIX names; assignments and wildcards are rejected. The key is global-only so a cloned repository cannot request secrets from the daemon's environment. Git worktree subprocesses and post_worktree_commands use the same boundary; list a package-manager/build credential explicitly if a setup command needs it. For Docker, listing a name is also the explicit trust grant that lets the repo-selected image receive it. New and respawned panes use the current list. A pane that was already running before an upgrade keeps the environment it started with until that process is restarted.

Theme colors (theme)

The TUI palette defaults to Zenburn: low-contrast foreground/background colors with muted green, red, yellow, blue, cyan, and purple accents. Override any slot in the global [theme] table; omitted slots keep their defaults.

[theme]
accent = "#8CD0D3"
success = "#7F9F7F"
warning = "#F0DFAF"
error = "#CC9393"
pane_border_preview = "#DC8CC3"

All values must be #RRGGBB. Invalid values are ignored with a warning and the default for that slot is used, so a bad color cannot prevent the TUI from starting. [theme] is global-only and TOML-only, like [keys]: in-repo configs reject it so a cloned repository cannot recolor your terminal.

Root agents (always-ensured)

root_agents opts a repository into a root agent: a reserved session titled root that the daemon guarantees is always running. It is created in-place at the repo root (the af sessions create --here shape — no worktree or branch is created; killing it never touches your working tree or branch), and if its tmux session dies or vanishes, the daemon re-creates it automatically.

[root_agents]
"/home/me/myrepo" = {}
"~/work/other" = { program = "claude --model opus" }

Keys are repository paths (a leading ~ expands to your home directory). Per-repo profile fields:

Field Description
program Command the root session runs. Unlike default_program this may be a full command string; a bare agent enum name (e.g. claude) still resolves through program_overrides. Default: the repo's resolved claude command with --dangerously-skip-permissions ensured — the root agent is meant to operate autonomously.

Behavior and guarantees:

  • Strictly opt-in and global-only. Nothing gets a root agent unless you add it here, in your ~/.agent-factory/config.toml. The key is rejected in in-repo configs, so cloning a repository can never opt your machine into an always-on agent.
  • Adopt, never clobber. If a session titled root already exists and is alive — however it was created — the daemon leaves it completely alone. Only a root whose tmux has died (status Dead) or that is missing entirely is (re-)created.
  • A re-created root keeps its conversation. Healing a root replaces its session record rather than re-spawning into it (that is what makes the root always-ensured), so the daemon carries the recorded conversation across and relaunches the agent on it — the same --resume every other recovered session gets. The application log says which happened: resumed its prior conversation, or started with a fresh context. A fresh context is the deliberate fallback when the conversation cannot be recovered — the configured root program runs a different agent, it pins its own resume flag, or the provider no longer has that conversation — because an always-on root that exists outranks one that keeps its history.
  • A root that came back without its history says so. When a re-create does not demonstrably resume the prior conversation, the replacement carries a one-shot note that every rail renders on its row: fresh context when the context is provably gone, context unknown when the resolved command selects its own conversation and af cannot tell. The note survives a daemon restart and clears the first time you open that session's pane. A root that resumed cleanly carries no note.
  • A re-created root keeps its tabs. The rest of the tab strip rides across the same replacement: every terminal, process, web, and editor tab comes back with its name, its target, and its position. The processes behind them are new — the tmux server died, so nothing survived to reconnect to — so a terminal tab comes back at a fresh prompt and a process tab re-runs its recorded command, exactly as they do when any other session is restored. The application log reports how many came back.
  • The name root is reserved. Normal session creation (TUI, af sessions create, the API, task spawns) rejects the title root (case-insensitively); auto-derived titles skip it.
  • An explicit kill gets a grace window. If you kill the root session (TUI D, af sessions kill root), the running daemon grants an in-memory grace: after about 2 minutes the root becomes eligible for re-creation on a following ensure pass. Restarting the daemon ends the grace immediately and re-asserts configured state. To keep the root down, first disable it in configuration: remove its legacy root_agents entry only if no other root-agent layer enables it, or set its personal per-project [root_agent] to enabled = false. Restart the daemon to apply the disable, then kill any root session that is still running.
  • Failures back off but never give up. If ensuring a root repeatedly fails (e.g. the configured path is not a git repository, or the tmux server is temporarily unusable), the daemon retries with exponential backoff that settles at one attempt every 5 minutes, logging each outcome to the application log (with an escalation to ERROR after 6 consecutive failures). The first attempt after the cause clears heals the root — no daemon restart needed.
  • Changes to root_agents are picked up on the next daemon restart.

Because the default profile skips permission prompts, only opt in repositories where you are comfortable with a fully autonomous agent running at the repo root.

The [root_agent] singleton

[root_agent] is the canonical successor to the path-keyed root_agents map: a single profile — whether a project keeps a root session, and the command it runs — that layers per registered project instead of per hard-coded path.

# ~/.agent-factory/config.toml — a global default applied to registered projects
[root_agent]
enabled = true
program = "claude --model opus"   # optional; empty = the default root profile

The same table is also valid in a project's personal per-project config (--project, see Personal per-project config), where it overrides the global default for that one project on this machine:

# a registered project's personal config — disable the root here only
[root_agent]
enabled = false

Semantics:

  • Precedence (low → high): built-in enabled=false < global [root_agent] < legacy root_agents[path] < personal per-project [root_agent]. Layers merge by field: a higher layer overrides enabled only if it set it (an explicit false counts) and program only if non-empty. So a personal enabled = false can disable a root that the global default — or a legacy root_agents entry — turned on.
  • The global default reaches registered projects only. It never scans disk for repositories; a project must be registered (af projects add) to receive it. Legacy root_agents entries keep working unchanged and forever.
  • Hand-edited for now. Like [theme], [root_agent] is not writable with af config set yet (af config get root_agent reads it); editing through the CLI/TUI lands in a later phase. Edit the file (or use the config assistant) directly.
  • Strictly opt-in, and global or machine-local only. Like root_agents, [root_agent] is rejected in checked-in in-repo config: it is valid only in your ~/.agent-factory/config.toml or a project's personal per-project file, both of which are yours. Cloning a repository can never opt your machine into an always-on agent through either key.
  • Restart-to-apply, exactly like root_agents: changes take effect on the next daemon start. All the always-ensure guarantees above (adopt-never-clobber, reserved name, temporary kill grace window, back-off-but-never-give-up) apply identically to a root the singleton enables.

Usage-limit auto-resume

This section covers the two auto-resume config keys. For the whole usage-limit feature end to end — detection, the [limit] badge, manual retry, auto-resume, and task park-don't-fail — see docs/usage-limits.md.

When a claude, codex, or devin session hits a plan usage-limit wall, af marks it with a [limit] badge in the sidebar and — when the banner states a reset time — shows when the limit resets (devin is detect-only and carries no reset time). By default the row stays there until you resume it yourself (the c key on the session).

limit_auto_resume = true opts the daemon into resuming such a session on its own once the limit window has elapsed:

limit_auto_resume = true
limit_retry_interval = "30m"
  • Off by default. With limit_auto_resume = false (the default), a limit is surface-only — the badge and the manual c retry — and the daemon does no scheduling.
  • When it resumes. If the banner carried a parseable reset time, the daemon resumes shortly after that time (a small grace buffer is added because limit windows are rolling and approximate). A reset time already in the past resumes promptly.
  • No parseable reset time. Some banners don't state a reset time. In that case the daemon falls back to retrying on the fixed limit_retry_interval cadence (a Go duration such as 30m or 1h). Setting limit_retry_interval to empty or 0 leaves such a session surface-only.
  • Re-limit backoff. If a resumed session immediately hits the wall again, the daemon backs off exponentially (settling at one attempt every 5 minutes) rather than hammering a genuinely exhausted plan. Killing the session is always the off-ramp.
  • Global-only, daemon behavior. Both keys are rejected in in-repo configs. A save through af config set (or the web config editor) applies them to the running daemon at once — no restart; a raw hand-edit of config.toml takes effect on the next daemon start.

Resuming re-delivers the session's stored task prompt (task-driven sessions resume their work); an interactive session with no stored prompt is sent a bare continue, which loses the agent's prior in-context state.

  • Task runs park, don't fail. When a cron/watch task fires while your plan is already exhausted, the task-driven session that hits the wall at startup is parked — kept, marked [limit], and recorded with the run status parked: usage limit — instead of being torn down and recorded as a failed run. Once the window resets, the same resume machinery (auto-resume or your manual c retry) re-delivers the stored task prompt and the run proceeds to completion. See docs/usage-limits.md.

Custom usage-limit detection (limit_patterns)

The built-in usage-limit detection recognizes the shipped claude, codex, and devin banners. If an agent reworded its banner, override the detection regex per agent with limit_patterns; the built-in reset-time parser (where the agent has one) is kept, so a custom detect pattern still schedules auto-resume against the parsed reset time.

[limit_patterns]
claude = "Claude usage limit reached\\."
codex  = "You've hit your usage limit"
  • Keys must be a supported agent enum (claude, codex, aider, gemini, amp, opencode, devin).
  • An override for an agent with no built-in matcher (aider/gemini/amp/opencode today) is ignored with a warning — af ships built-in matchers for claude, codex, and devin only.
  • devin is detect-only: it gets the [limit] badge but no reset time (its exhaustion banner is inferred from the binary and docs rather than captured live, and its reset format is uncharacterized), so it never auto-resumes on a parsed time — it waits for your manual c retry, or the limit_retry_interval fallback if limit_auto_resume is on. Its healthy N% remaining / resets in … quota-status line is not treated as a limit.
  • An uncompilable regex warns and falls back to the built-in default, so a typo can never disable detection.
  • limit_patterns is a detection tweak, not a behavior switch: it is honored everywhere the built-in detector runs (the daemon status poll, and the task-run startup park path).

Key bindings ([keys])

The TUI's key bindings are rebindable from a [keys] table. Each entry maps an action to a key string or a list of key strings, replacing that action's default binding entirely; actions you don't list keep their defaults.

[keys]
quit = "Q"
new = "c"
up = ["u", "ctrl+p"]
tasks = "ctrl+t"
  • Key strings are the forms the terminal reports: a single character (Q, /, ?), a named key (up, enter, f5, space), or a ctrl+/alt+/shift+ combination (ctrl+t, shift+up).
  • Rebindable actions: up, down, scroll_up, scroll_down, attach, new, kill, quit, help, new_remote, new_tab, close_tab, tasks, search, open_pr, copy_pr, hooks, config_agent, config_editor, open_pane, split_pane, hide_pane, pane_prev, pane_next, collapse, expand, next_section, prev_section, archive, restore, limit_retry, handoff, error_details, switch_project. (Run af keys to print the full effective table.)
  • pane_prev / pane_next are contextual: their default left / right bindings switch panes only while a workspace pane has focus. With tree focus, the same arrows keep the tree's collapse/expand behavior.
  • Reserved keys are rejected: binding any action to enter, tab, shift+tab, esc, ctrl+], or a digit 19 is a startup error naming the key and why it's reserved (they drive interaction, the focus ring, overlay cancel, the interactive-mode exit, and the 1–9 tab jump respectively).
  • ctrl+c is a fixed hard exit, not a reserved key. Validation does not reject it — you can write quit = "ctrl+c" (or point any action at it) with no error — but ctrl+c always quits and is handled before the keymap ever sees the keypress, so binding an action to it has no effect: the hard exit wins. It is therefore not effectively rebindable, which is different from the reserved keys above that are outright rejected at load.
  • Any problem — an unknown action, an unparseable or reserved key, or two user overrides bound to the same key — is a hard error at startup that names the file and the offending action, so a typo can't silently leave you with a dead key. A user override on a key suppresses any default binding for that key rather than erroring, so an upgrade that ships a new default binding never breaks an existing config — the user's binding wins and the new action is simply unbound by default. The bottom menu and the ? help overlay both reflect your rebinds.
  • Global-only. keys is rejected in in-repo configs — a cloned repository can never rebind your terminal.
  • TOML-only. The keymap exists only in config.toml; a keys block in a legacy config.json is ignored with a warning.

Run af keys to see the effective bindings (defaults plus your rebinds).

The default TUI keys changed to ergonomic lower-case bindings in #1027: archive is a, restore is r (#1605), the task manager is m, copy PR URL is y, hooks is e, and preview scrolling is ctrl+u / ctrl+d. The previous defaults are not built-in aliases; restore any old binding you still want by pinning it here:

[keys]
archive = "A"
tasks = "S"
split_pane = "alt+s"
copy_pr = "P"
hooks = "H"
scroll_up = "shift+up"
scroll_down = "shift+down"

Agent guidance and your global agent config

af teaches each agent how to drive af itself — af sessions whoami, af sessions archive --self, and the rest. How that guidance reaches the agent depends on what the agent supports.

For claude, aider and opencode, af owns the file and points the agent at it for that launch only (--plugin-dir, --read, and OPENCODE_CONFIG respectively). Everything lives under af's own config directory, so it disappears when you uninstall af and is invisible to an agent af did not launch.

codex, gemini and amp auto-discover skills from a directory in your home and offer no per-launch pointer to an extra one. The only way to reach them is to write a file into your config — $CODEX_HOME/skills/agent-factory/, ~/.gemini/skills/agent-factory/, ~/.config/amp/skills/agent-factory/ — which outlives the session, survives uninstalling af, and applies when you run those agents by hand somewhere af has nothing to do with. Creating a session is not consent to that, so af does not do it by default:

global_agent_skills = true

With it on, af writes (and keeps up to date) a single agent-factory/SKILL.md under each of those agents' skills directories. With it off — the default — those three agents simply do not get af's guidance; everything else about the session is unchanged.

af only ever manages the file it wrote. Each one carries an af marker, and:

  • a file at that path without the marker is yours and is never overwritten or removed;
  • turning the key off (or leaving it off after an af version that wrote one) removes af's own marked file, so af's edit does not outlive the decision;
  • the agent-factory/ directory is removed only if it is empty, so anything you put beside af's file keeps the directory alive.

Choosing the agent

Override the agent for new sessions with -p:

af -p aider

-p and the per-task program field both accept a bare agent enum only (claude, codex, aider, gemini, amp, opencode, devin). To pass a custom path or flags for an agent, set program_overrides.<agent> in your config — every session that launches that agent will use the override.

In-repo config

A repository can carry its own configuration in <repo-root>/.agent-factory/config.toml, so every clone gets the same setup:

default_program = "codex"
post_worktree_commands = ["npm install"]

[program_overrides]
codex = "/usr/local/bin/codex --profile work"

[remote_hooks]
launch_cmd = "./infra/launch.sh"
delete_cmd = "./infra/delete.sh"

TOML top-level ordering: put plain keys and arrays (like post_worktree_commands) above any [table] header. Once a table is opened, every following bare key belongs to it — that is TOML, not an af rule.

Field Scope
default_program, program_overrides Valid globally and in-repo (in-repo wins).
post_worktree_commands, remote_hooks In-repo only. The legacy ~/.agent-factory/repos/<repoID>/config.json location keeps working for one more release (a deprecation warning in the log points at the new file) and is shadowed whenever the in-repo file sets the same key — including by an explicit empty value like post_worktree_commands = [].
backend, docker, ssh In-repo only. Select the runtime a repo's sessions run on.
auto_update, require_token, require_loopback_token, listen_addr, preview_listen_addr, cors_allowed_origins, daemon_poll_interval, branch_prefix, on_archive_command, worktree_root, detach_keys, log_max_size_mb, log_max_backups, update_channel, keys, theme, root_agents, root_agent, limit_auto_resume, limit_retry_interval, limit_patterns, vscode_server_binary, global_agent_skills, docker_mount_agent_credentials, ssh_host_key_verification, sandbox_ssh, session_env_passthrough Operator-only. Setting them in-repo is rejected with an error naming the key. Most are global only; branch_prefix, on_archive_command, and root_agent also admit the machine-local personal-project layer. The daemon network-surface keys (require_token, listen_addr, preview_listen_addr, cors_allowed_origins) are global-only so a cloned repo can never open a port, widen CORS, or disable auth. on_archive_command and vscode_server_binary are rejected in-repo because they name code the daemon host executes. session_env_passthrough, docker_mount_agent_credentials, and ssh_host_key_verification are global-only so a cloned repo cannot grant its own docker image access to the daemon environment or the operator's credentials, nor waive ssh host-key verification (a repo-settable waiver + repo-settable ssh.host is a one-commit MITM) — a repo selects the image/host, only the operator relaxes the safeguard. sandbox_ssh is the strongest case of the same rule: af EXECUTES it on the daemon host, so a repo-settable version would be arbitrary code execution from a cloned repository rather than merely a widened permission — a repo selects backend = "sandbox", only the operator says what command reaches the sandbox. See remote-http-auth.md.

post_worktree_commands are shell commands run after each new worktree is created (e.g. npm install, make build) — they can also be edited from the TUI via the e (worktree hooks) key. remote_hooks configures a remote-machine backend; see remote-hooks.md for the script protocol.

Backend runtime (backend, docker, ssh)

backend selects the runtime a repo's sessions run on, and --backend overrides it per af sessions create:

Value Runtime
local (default, or unset) Today's in-process runtime: the agent runs as a tmux session in a git worktree on the machine running the daemon.
hook The remote-hook backend — a bring-your-own provisioner driven by the [remote_hooks] scripts (equivalent to the TUI's "new remote session").
docker Run the workspace + agent in a container started from [docker].image.
ssh Run the workspace + agent on [ssh].host over ssh.
backend = "docker"

[docker]
image = "af-runtime:latest"
run_args = ["--memory", "2g"]

[ssh]
host = "build-box"
user = "ci"
port = 2222
identity_file = "~/.ssh/id_ed25519"

An unknown backend value (or --backend) is reported when the session's runtime is resolved at create time, naming the valid options.

In-repo file name: config.toml or config.json

Because the in-repo file is checked into your repository, both names are accepted indefinitely: <repo-root>/.agent-factory/config.toml or <repo-root>/.agent-factory/config.json. This is deliberate — a repo shared with collaborators still on an older af (which only understands config.json) must keep working, so af never renames a checked-in file out from under them.

  • New in-repo files that af writes (e.g. saving worktree hooks from the TUI) are created as config.toml.
  • An existing config.json is updated in place, still as JSON, so your collaborators' af keeps reading it.
  • A repo carrying both config.toml and config.json is a hard error naming both files — af will not guess which is live. Keep exactly one.

If your whole team is on a current af, prefer config.toml. While versions are mixed, keep config.json.

Relative hook paths

Relative remote_hooks paths (like ./infra/launch.sh above) resolve against the repository root — the repo whose .agent-factory/config.toml was loaded; for sessions in linked worktrees that is the main repository root — so checked-in hook scripts work no matter what the working directory of af or its daemon is. Bare names without a path separator (e.g. bash) keep normal $PATH lookup. See remote-hooks.md for the full rules.

Trust

An in-repo config executes what it configures: post_worktree_commands run after each worktree is created, and remote_hooks and program_overrides values are invoked as shell commands. Cloning a repository and running af in it implies trusting that repo's in-repo config. The first time a config carrying such fields loads (and whenever its content changes), af records one log line naming the fields and the file's content hash.

Personal per-project config

The global config applies everywhere and the in-repo config is checked in for everyone who clones a repository. Sometimes you want neither: a preference that is yours, on this machine, for one project — a different default agent for your work monorepo than your side projects, a project-specific program_overrides path, a branch prefix that matches a team convention. That is the personal per-project layer.

It lives outside the repository, under your agent-factory home:

~/.agent-factory/.agent-factory-projects/<project-id>/config.toml

so it is never committed and never imposed on collaborators. Because it is your own machine-local file — like the global config, and unlike a checked-in in-repo file — it may set the same keys the global config can, including a cloud-credential selector in a program_overrides value.

It attaches to a registered project, not a path. A project has a durable, opaque id (prj_…) that survives the checkout moving or being cloned twice, so a personal override does not silently stop applying when you move a repo. Register a repository once:

af projects add ~/work/myrepo   # prints the project's prj_ id
af projects list                # every registered project

Then set and clear overrides. --project accepts either the prj_ id or any path inside the registered repository:

af config set default_program codex --project ~/work/myrepo
af config set program_overrides.claude "/opt/claude --verbose" --project prj_01234…
af config unset default_program --project ~/work/myrepo   # fall back to the lower layers

Only preference/operator keys admit this layer: default_program, program_overrides.<agent>, branch_prefix, on_archive_command, and the [root_agent] table (whether this project keeps a root agent — the highest-precedence root-agent layer, so it can disable one the global default or a legacy root_agents entry enabled). on_archive_command is safe here because the file is machine-local and operator-written; the same key is rejected from checked-in in-repo config. Setting a global-only key (listen_addr, daemon tuning, …) or a repo-contract key (backend, docker, ssh) per project is rejected with the location it actually belongs to. Setting a value equal to the lower layer is still a present, winning override — use af config unset to genuinely fall through again; it removes only the key you name (comments and other keys are preserved) and deletes the file once its last override is cleared.

Precedence for a key that admits every layer is:

app default → global → in-repo (shared) → personal per-project

Inspect exactly which layer wins, and why, with af config get <key> --repo <path> --explain (or af config list --repo <path> --explain): the trace shows the personal-project candidate alongside the others, marked won, shadowed, absent, or — for a key that cannot be overridden per project — disallowed. Changes apply on the next af/daemon start, the same as a hand-edit.

Migrating from JSON

Earlier versions stored config as config.json. The move to TOML is automatic and one-time — you don't run anything:

  • The first time a current af starts and finds a ~/.agent-factory/config.json but no config.toml, it reads your settings, writes an equivalent config.toml, and moves the original aside to config.json.bak. From then on config.toml is the file to edit; config.json.bak is kept as a backup you can delete once you're happy. An existing backup is never overwritten — if config.json.bak is already there (e.g. from an earlier convert-and-downgrade round trip), the new one lands as config.json.bak.1, .bak.2, and so on, so your oldest backup is always preserved.
  • If both config.toml and config.json are ever present, config.toml wins and config.json is ignored (with a warning). Delete or rename the stray config.json to silence it.
  • A config.json that can't be parsed is left untouched with an error telling you what's wrong — it is not converted until it's valid, so you never lose settings to a half-broken file.
  • If you downgrade to an older af after converting, it won't see your config.toml and will regenerate a default config.json. Your settings are safe in config.toml (and config.json.bak); when you upgrade again, config.toml takes over. To restore the old file explicitly, mv config.json.bak config.json before downgrading.

The in-repo file is not auto-converted — see In-repo file name.

Where state lives

All data (sessions, tasks) is scoped to the current git repository — the TUI shows only what's relevant to the active project. Press Ctrl-p to switch projects without restarting.

Path Contents
~/.agent-factory/config.toml Global config.
~/.agent-factory/config.json.bak Backup of your pre-TOML config, left by the one-time migration. Safe to delete.
~/.agent-factory/.agent-factory-projects/<project-id>/config.toml Personal per-project overrides for a registered project (see Personal per-project config). Machine-local, never checked in.
~/.agent-factory/instances/<repoID>/instances.json Persisted sessions, per repo.
~/.agent-factory/tasks.json Tasks (see tasks.md).
~/.agent-factory/logs/task-<id>.log Per-task watch-script logs. Rotated with the same log_max_size_mb/log_max_backups policy as the application log (task-<id>.log.1, .2).
~/.config/agent-factory/agent-factory.log Application log (os.UserConfigDir on other platforms). Rotated once it exceeds log_max_size_mb (default 50 MB); the most recent log_max_backups rotations (default 2) are kept as agent-factory.log.1, .2.

Setting the AGENT_FACTORY_HOME environment variable relocates the ~/.agent-factory state directory — useful for sandboxed or test setups. When it is set, the application log also moves into that directory ($AGENT_FACTORY_HOME/agent-factory.log) so a relocated home is fully self-contained.