Configuration¶
Agent Factory reads config from several layers, merged field by field:
- Global —
~/.agent-factory/config.toml: your personal defaults, applied everywhere. - In-repo —
<repo-root>/.agent-factory/config.toml: checked into a repository, applied wheneverafruns in that repo. - 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--yolospelling 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"inopencode.json; this is OpenCode configuration, not anafprogram_overridesline. 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, pasteprogram_overrides.devin = "devin --permission-mode accept-edits"(orsmart, which additionally auto-runs actions a fast model judges safe;dangerousauto-approves everything — the higher-risk choice). af leaves the default atautorather than pickingaccept-editsfor you, so an unattended devin session is opt-in to auto-editing, matching devin's own default. The env varDEVIN_PERMISSION_MODEworks too but aprogram_overridesline 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 validLC_*name,TZ,TMPDIR/TMP/TEMP,PWD, XDG config/data/cache/state/runtime paths, terminal color preferences, and tmux's ownTMUX,TMUX_PANE, andTMUX_TMPDIRmarkers. - Agent Factory state:
AGENT_FACTORY_HOME,AGENT_FACTORY_AUTO_UPDATE,AF_HOME,AF_SESSION,AF_DAEMON_URL, andAF_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. Storedghlogin, Git credential helpers, and native keyrings continue to work throughHOME, 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 throughenvorexec) 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 getsOPENAI_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 byGOOGLE_GENAI_USE_VERTEXAIorGOOGLE_GENAI_USE_GCA, on the same exported-or-inline terms as Claude's; Amp getsAMP_API_KEYandAMP_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:
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.
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
rootalready exists and is alive — however it was created — the daemon leaves it completely alone. Only arootwhose tmux has died (statusDead) 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
--resumeevery 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 contextwhen the context is provably gone,context unknownwhen 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
rootis reserved. Normal session creation (TUI,af sessions create, the API, task spawns) rejects the titleroot(case-insensitively); auto-derived titles skip it. - An explicit kill gets a grace window. If you kill the
rootsession (TUID,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 legacyroot_agentsentry only if no other root-agent layer enables it, or set its personal per-project[root_agent]toenabled = false. Restart the daemon to apply the disable, then kill anyrootsession 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_agentsare 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:
Semantics:
- Precedence (low → high): built-in
enabled=false< global[root_agent]< legacyroot_agents[path]< personal per-project[root_agent]. Layers merge by field: a higher layer overridesenabledonly if it set it (an explicitfalsecounts) andprogramonly if non-empty. So a personalenabled = falsecan disable a root that the global default — or a legacyroot_agentsentry — 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. Legacyroot_agentsentries keep working unchanged and forever. - Hand-edited for now. Like
[theme],[root_agent]is not writable withaf config setyet (af config get root_agentreads 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.tomlor 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:
- Off by default. With
limit_auto_resume = false(the default), a limit is surface-only — the badge and the manualcretry — 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_intervalcadence (a Go duration such as30mor1h). Settinglimit_retry_intervalto empty or0leaves 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 ofconfig.tomltakes 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 statusparked: 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 manualcretry) 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.
- 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/opencodetoday) is ignored with a warning — af ships built-in matchers forclaude,codex, anddevinonly. devinis 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 manualcretry, or thelimit_retry_intervalfallback iflimit_auto_resumeis on. Its healthyN% 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_patternsis 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.
- Key strings are the forms the terminal reports: a single character (
Q,/,?), a named key (up,enter,f5,space), or actrl+/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. (Runaf keysto print the full effective table.) pane_prev/pane_nextare contextual: their defaultleft/rightbindings 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 digit1–9is 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+cis a fixed hard exit, not a reserved key. Validation does not reject it — you can writequit = "ctrl+c"(or point any action at it) with no error — butctrl+calways 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.
keysis rejected in in-repo configs — a cloned repository can never rebind your terminal. - TOML-only. The keymap exists only in
config.toml; akeysblock in a legacyconfig.jsonis 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:
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:
-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
afwrites (e.g. saving worktree hooks from the TUI) are created asconfig.toml. - An existing
config.jsonis updated in place, still as JSON, so your collaborators'afkeeps reading it. - A repo carrying both
config.tomlandconfig.jsonis a hard error naming both files —afwill 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:
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:
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
afstarts and finds a~/.agent-factory/config.jsonbut noconfig.toml, it reads your settings, writes an equivalentconfig.toml, and moves the original aside toconfig.json.bak. From then onconfig.tomlis the file to edit;config.json.bakis kept as a backup you can delete once you're happy. An existing backup is never overwritten — ifconfig.json.bakis already there (e.g. from an earlier convert-and-downgrade round trip), the new one lands asconfig.json.bak.1,.bak.2, and so on, so your oldest backup is always preserved. - If both
config.tomlandconfig.jsonare ever present,config.tomlwins andconfig.jsonis ignored (with a warning). Delete or rename the strayconfig.jsonto silence it. - A
config.jsonthat 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
afafter converting, it won't see yourconfig.tomland will regenerate a defaultconfig.json. Your settings are safe inconfig.toml(andconfig.json.bak); when you upgrade again,config.tomltakes over. To restore the old file explicitly,mv config.json.bak config.jsonbefore 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.