Skip to content

CLI guide

Everything the TUI does is also scriptable. The af sessions and af tasks command groups output JSON to stdout and errors to stderr, so they compose with jq and shell scripts. Pass --json to wrap output in a {data, error} envelope for structured error handling. The TUI and the CLI share the same state — you can mix them freely.

Run af <command> --help for the authoritative flag list of any command.

Project scoping

Like the TUI and the web UI, every af sessions and af tasks command is scoped to one project. The rules are the same for all of them:

  1. --repo <path> names the project explicitly, and always wins.
  2. Otherwise the current directory's git repository is the project. A linked worktree resolves to its main repository, so a command run from inside a session's worktree still acts on the real project.
  3. Outside a git repository there is no project context:
    • sessions create and tasks add require --repo — a new binding is never guessed.
    • Listing spans every project.
    • A <title> or task <id> resolves across projects, but a title held by several projects is ambiguous and errors rather than picking one.

Acting on another project always takes an explicit --repo. The read-only list commands also accept --all as an explicit opt-in to span every project.

af sessions list              # this project's sessions
af sessions list --all        # every project's sessions
af sessions list --repo /repos/beta
af tasks list                 # this project's tasks
af tasks list --all           # every project's tasks
af tasks list --repo /repos/beta

These defaults changed (#1893)

af tasks list used to list every project's tasks; from inside a repository it now lists only that project's. Pass --all for the old behavior.

af tasks get, update, remove, and trigger used to accept --repo and silently ignore it, so an id reached any project's task from anywhere. They now refuse an id owned by a different project and name the --repo that would reach it.

Remote daemons

For the session reads that follow --daemon-url/AF_DAEMON_URL (sessions list, get, watch, preview, and attach), rule 2 does not apply: your current directory names a repository on this machine, which says nothing about the daemon's projects, so only an explicit --repo scopes a remote lookup.

af tasks is not remote-targeted at all. Every task command talks to the local daemon over its control socket, whatever --daemon-url/AF_DAEMON_URL is set to — so af tasks list scopes by your local current directory and lists local tasks, and af tasks remove removes a local one. There is no way to manage a remote daemon's tasks from here today.

af — the TUI

cd your-project    # a git repo
af                 # launch the TUI
Flag Description
-p, --program Agent to run in new sessions, one of claude, codex, aider, gemini, amp, opencode, devin. To pass custom paths or flags, use program_overrides in the config instead — see configuration.md.

af sessions

All subcommands honor --repo <path> to target a project other than the current directory's (see Project scoping), and --json to wrap output in the shared envelope.

Session titles are unique within a project, not across projects — the same name may exist in several repos at once. Every command that takes a <title> resolves it inside the repo named by --repo, falling back to the current directory's repo. With no repo context (no --repo, cwd outside a repo) a title held by exactly one session anywhere still resolves; a title held by sessions in several projects is ambiguous and reports an error naming them rather than picking one:

session "foo" exists in multiple projects: /repos/alpha, /repos/beta — pass --repo to pick one

Against a remote daemon (--daemon-url/AF_DAEMON_URL), the split follows the transport:

  • Served by the targeted daemonlist, get, watch, preview, attach — ignore the current directory rather than sending it as a scope, since it names a repo on your machine, not the daemon's. A bare title resolves across the remote's projects.
  • Everything elsekill, archive, restore, retry-limit, send-prompt, tab create/delete — reaches the local daemon regardless of --daemon-url, so it stays scoped to the current directory.

Caveat for the reads: --repo becomes an id by hashing the path as given on this machine, so it only disambiguates when the daemon has that project checked out at the same absolute path. Prefer a bare title against a remote and let the ambiguity error tell you when to narrow it.

One exception to per-project titles: remote hook sessions share a global name namespace, because the slug reaches launch_cmd/delete_cmd verbatim and external provisioners key real sandboxes on it — see remote-hooks.md.

af sessions list [--all]                                  # list this repo, or every repo explicitly
af sessions get <title>                                   # fetch one session
af sessions create <title> [--prompt "..."] [--program <agent>] [--here]
# --name <title> remains an alias for existing scripts
af sessions send-prompt <title> "..."                     # append a prompt to a session
af sessions send-prompt <title> "..." --create            # send-or-create
af sessions tab-create <title> --kind shell               # spawn the same bare $SHELL Terminal tab the TUI/web opens
af sessions tab-create <title> --command "<cmd>"          # spawn a process tab in the session's worktree
af sessions tab-create <title> --kind web --port 5173     # web/iframe tab pointing at a localhost dev server
af sessions tab-create <title> --kind vscode              # VS Code editor tab on the session's worktree
af sessions tab-delete <title> --name <tab>               # delete a single tab (the daemon won't respawn it)
af sessions tabs create <title> --command "<cmd>"         # alias for tab-create (hyphen verb still works)
af sessions tabs create <title> --kind web --url <url>    # alias for tab-create (hyphen verb still works)
af sessions tabs delete <title> --name <tab>              # alias for tab-delete
af sessions preview <title>                               # snapshot the session's pane
af sessions attach <title>                                # attach interactively (foreground)
af sessions whoami                                        # report the session this shell is inside
af sessions archive <title>                               # default done action: restorable later
af sessions archive --self                                # archive the current session (resolved via whoami)
af sessions handoff <title> --to <agent>                  # continue under a different agent, same worktree/branch
af sessions retry-limit <title>                           # retry a session parked at a usage-limit wall
af sessions kill <title>                                  # permanently destroy + prune owned branch
af sessions restore <title>                               # restore an archived/lost/dead session

Flags:

  • list: --all spans every project's sessions explicitly; it is mutually exclusive with --repo.
  • create: takes <title> positionally; --name <title> remains an alias for existing scripts. --prompt sends an initial prompt, and --program selects the agent enum (defaulting to the configured default_program). --here (alias --in-place) attaches the session to the repo's existing working tree at its current branch instead of cutting a new worktree+branch: the agent runs in the repo root, no branch is created, and killing the session never removes the working tree or branch. Requires a git repository (the current directory, or --repo); incompatible with remote sessions. The title root (any casing) is reserved for the daemon-managed root agent — see the root_agents key in configuration.md.
  • send-prompt: --create auto-creates the session if it doesn't exist; --program picks the agent when creating.
  • tab-create: creates a new tab in the session's worktree. --kind shell starts the user's bare $SHELL through the same daemon operation the TUI and web use. Its canonical kind/base name is shell; Terminal is the presentation-only UI label, and --name/--command/--url/--port are rejected for this kind rather than ignored. By default, --command is run in the worktree as a process tab; --name sets the tab's name — the handle the other tab verbs address it by, not the label the TUI renders (defaults to the command's basename; sanitized to [A-Za-z0-9_-], then auto-suffixed -2, -3, … on collision, so the name you pass is not always the name you get). With --kind web, creates an iframe tab targeting --url (or --port as a localhost: convenience) instead. With --kind vscode, creates a VS Code editor tab on the session's own worktree — it takes no target, so --url/--port/--command are rejected, and it needs code-server (or openvscode-server) installed, which af detects rather than bundles. Both are browser panes with no PTY — see web.md. The resolved tab name is printed as {"name": "..."} so scripts/agents can address it. The tab persists and reconnects across a daemon/af restart like every other tab. Local sessions only: an off-box session (docker/ssh/hook) has no daemon-side worktree to spawn a tab in, so the daemon rejects the request — its tab list is fixed at the single agent tab (see remote-hooks.md).
  • tab-delete: the counterpart of tab-create--name (required) selects the tab to delete. Pass the tab's name (as reported by af sessions get), not the label the TUI tab bar shows: agent and shell tabs render a fixed Agent/Terminal that is not their name (agent/shell). A miss lists the tabs that exist with both spellings, so a wrong name is a next step rather than a dead end. The tab is removed from the daemon's session state and its tmux window is killed; the removal is persistent (the daemon won't respawn it, and it doesn't return on restart). The deleted tab's name is printed as {"name": "..."}. The agent tab can't be deleted — use af sessions kill to tear down the whole session. An archived session's tabs can't be deleted either (its preserved web-tab URLs must still be there for restore) — restore it first. Targeting a missing tab or session is an error. Local sessions only (an off-box session has no daemon-side worktree and no user-managed tabs to delete).
  • tabs {create,delete}: additive noun-subcommand aliases — af sessions tabs create == af sessions tab-create and af sessions tabs delete == af sessions tab-delete (same flags and output). The hyphen verbs are kept for existing scripts; nothing is renamed. There is no tabs list — list a session's tabs via af sessions get.
  • archive: the default way to finish with a session. It tears down the session's tmux and moves its git worktree out to the global archive directory (<AGENT_FACTORY_HOME>/archived/<repoID>/<title>/), preserving the branch and any uncommitted changes. The session is not deleted — it becomes a quiescent archived row that survives restarts and is never auto-restored. Shell/process tabs do not survive the archive (their processes are torn down), but web tabs do — their target URLs are preserved and render again on restore. While archived they are inert: the tab shows a placeholder rather than loading, and it can't be deleted. Prints {"ok": true, "title": "...", "archived_path": "..."}. An off-box session (docker/ssh/hook) archives differently — it pushes its branch to origin and tears the sandbox down (durability lives in GitHub, not the sandbox), and restore re-provisions from the pushed branch. Not available for in-place (--here) sessions (they don't own a relocatable worktree). Bring it back with restore. Pass --self instead of a <title> to archive the current session — it resolves the caller's own session the way whoami does, so an agent can archive itself once its work is done; --self and a <title> are mutually exclusive, and it errors with an actionable message when not run from inside a session.
  • handoff: continues a session under a different agent, in place — the alternative to waiting out a provider usage limit. --to <agent> (required) names the incoming agent; any supported agent is a valid target. The session keeps its identity, worktree, branch, tabs, and task binding — only the agent process is replaced, so uncommitted work is untouched. Agent conversations are not portable between providers, so the incoming agent starts a fresh conversation and is delivered a mission brief: the session's goal, the fact that it is continuing someone else's work, and where to read it (git log/git diff on the branch). Use --brief to supply a mission that replaces the session's stored prompt when that prompt is stale or too broad. The swap is recorded along with the branch tip at that moment, which is the attribution boundary between the two agents' work. Prints {"ok": true, "title": "...", "from": "...", "to": "...", "head_sha": "..."}. Local-worktree sessions only (an off-box session runs its agent inside a provisioned sandbox, a different lifecycle). Refuses a handoff to the agent already running. See usage-limits.md.
  • retry-limit: resumes a session currently parked at a provider usage-limit wall. It calls the same daemon recovery action as the TUI's c key and the web's Retry button: an exited agent is re-spawned when necessary, the pending prompt is re-delivered (or continue when none is stored), and the limit state clears only after delivery succeeds. Prints {"ok": true, "title": "..."} and refuses a session that is not currently limit-blocked. See usage-limits.md.
  • kill: permanently destroys a session: tmux is torn down, the worktree is removed, the stored row is deleted, and the branch is pruned when Agent Factory owns it. It always destroys the session, including any uncommitted or unmerged work on its branch — there is no undo. Use af sessions archive <title> to keep it restorable instead. --force is accepted but has no effect (kept for backward compatibility).
  • restore: restores an archived, Lost, or Dead session. Archived sessions move their worktree back next to the repo, re-register it, re-spawn the agent (shell/process tabs are not restored — their processes were torn down at archive time; web tabs come back with their target URLs, since a web tab is just a URL and has no process), and mark the session running. Lost/Dead sessions recover in place, rebuilding a missing worktree when possible and resuming the recorded agent conversation when required. Prints {"ok": true, "title": "...", "worktree_path": "..."}. Fails if the session is not restorable, or if its origin repo is gone (an archived worktree is left intact for manual recovery). Honors --repo like kill.

af tasks

Tasks deliver a prompt to an agent automatically — on a cron schedule or whenever a long-running watch script emits a stdout line. Full semantics (trigger × delivery matrix, watch-script contract, status model) live in tasks.md. All subcommands honor --repo <path> and --json, and follow the shared project scoping rules.

af tasks list [--all]
af tasks add --name <n> --prompt <p> --cron "0 9 * * *" [--target-session <title>] [--program <agent>]
af tasks add --name <n> --watch-cmd <cmd> [--prompt "... {{line}} ..."] [--target-session <title>]
af tasks get <id>
af tasks update <id> [--cron ...|--watch-cmd ...] [--prompt ...] [--target-session ...] [--project-path <repo>] [--program <agent>] [--enabled true|false]
af tasks restart <id>          # reload an edited watch script (watch tasks only)
af tasks trigger <id>          # run a cron task immediately (cron tasks only)
af tasks remove <id>

Exactly one of --cron / --watch-cmd per task. On update, setting one trigger clears the other. --target-session "" explicitly reverts to create-a-session-per-run; omitting the flag leaves it untouched. --project-path moves the task to that repository; --repo still names the task's current project for authorization. --program accepts the same agent enum as tasks add; omitting it keeps the task's current program.

Project binding

A task is bound to exactly one project when it is created, and every run's worktree is created inside it. The binding comes from --repo, or from the current directory's project. The two flags on update do different jobs: --repo scopes which task may be edited and never re-binds one, while --project-path <repo> moves the task to another existing git repository — that path becomes both its new working directory and its project binding. So af tasks update <id> --repo /repos/alpha --project-path /repos/beta authorizes the task in alpha and moves it to beta.

The project a task belongs to is recorded as an id resolved when the task is bound, not re-derived from its path on each read. This is why deleting a directory a task points at — a subdirectory or a linked worktree — never hides the task from its own project.

If another client re-binds a task to a different project while a command is acting on it, the command is refused rather than applied to the moved task:

$ af tasks remove a1b2c3d4
{"error":"task \"a1b2c3d4\" was re-bound to a different project while this command was running
 (expected /repos/alpha, now /repos/beta) — nothing was changed; re-run the command to act on it
 in its current project"}

Nothing is changed when this happens, and re-running acts on the task where it now lives.

tasks add reports the resolved binding, so you can check it is the project you meant:

$ af tasks add --name nightly --prompt "sweep" --cron "0 3 * * *"
{
  "id": "a1b2c3d4",
  "project_path": "/repos/alpha"
}

Because the binding is inherited from the current directory, running tasks add inside a throwaway clone of a repository binds the automation to the clone rather than to the real project — the task's sessions then appear under a project nobody looks at. Creating a task from a clone that lives inside AGENT_FACTORY_HOME is refused for that reason; pass --repo to name the intended project. Session worktrees under AGENT_FACTORY_HOME are unaffected: a linked worktree resolves to its main repository, so an agent adding a task from inside its own session binds to the real project.

af daemon

The background daemon hosts task cron schedules, watch-task scripts, session monitoring, and the web UI. It starts on demand whenever af runs and an enabled task exists; installing it as a user-level autostart unit (systemd user service on Linux, launchd agent on macOS) keeps scheduled tasks firing after reboots. See tasks.md.

af daemon install      # register autostart at login
af daemon restart      # restart a running daemon and re-adopt live sessions
af daemon adopt        # hand a detached daemon back to the installed autostart unit
af daemon uninstall    # remove the autostart unit (the daemon still starts on demand)
af daemon status       # read-only health, supervision, and config freshness (+ --json)

af daemon status uses the same no-spawn Ping as af doctor, plus bounded read-only service-manager inspection. It reports the responder PID separately from the recorded PID, whether the installed unit owns that responder, and whether the listener/auth config on disk matches what the daemon booted with. Manager failures and older daemons render as unknown; they are never guessed into “unsupervised” or “current.” It never starts, stops, or reloads a daemon or unit.

af daemon restart is safe to run after replacing the af binary. It asks a running daemon to shut down cleanly, restarts the autostart unit when one is installed, otherwise starts an ad-hoc daemon from the current binary. If no daemon is running, it exits successfully without starting one.

af daemon adopt reclaims supervision when a daemon is running detached from its unit — an ad-hoc child a live af spawned, which the service manager can no longer restart. It stops that daemon, starts the installed unit in its place, and verifies the unit now owns the process answering the control socket. Live sessions keep running; the new supervised daemon re-adopts persisted state on startup, exactly as af daemon restart does. It needs an installed unit that serves this home; if that unit already owns the running daemon, adopt reports so and changes nothing.

af config

Read the current repository's effective config (or global config outside Git), inspect another repository, and write the global config (~/.agent-factory/config.toml) from the CLI. Config is a hand-editable file read by the daemon and TUI at startup (not daemon-owned state). --json wraps output in the shared envelope (success and error).

af config list                                      # current repository's effective values
af config get <key>                                 # one current-repository value
af config get <key> --explain                       # candidates and why one won
af config list --repo ../another-project            # inspect another repository
af config get program_overrides.codex --repo . --explain
af config set <key> <value>                         # global write, preserving comments/ordering

Bare get/list report the current repository's effective values (defaults plus global, legacy per-repo, checked-in, and personal layers); outside Git they report global values. --repo <repository-path> inspects another repository, while the deprecated --project read alias remains accepted for compatibility. The path is a selector only and does not register or persist a project. --explain reports the on-disk effective value, merge policy, precedence, every candidate's path/presence/result/reason, and per-leaf origins for merged maps/tables. Displayed source locations keep the selected/configured path spelling; symlinks are resolved for identity comparison, never for display. The output also says explicitly that the running daemon value was not checked. JSON mode carries the same data structurally.

set edits only the target value's bytes — every comment, blank line, and key ordering is preserved (the file is not regenerated) — and validates the value with the loader's own rules before writing, so it can never produce a config that fails to load. Without --project it writes the global config; with --project <id-or-path> it writes a personal per-project override instead, and af config unset <key> --project <id-or-path> clears one (only the preference keys default_program, program_overrides.<agent>, branch_prefix, on_archive_command admit a per-project override). Only scalar tunables are settable — run af config set --help, or see the full key reference in configuration.md, for the current list rather than one hand-copied here. cors_allowed_origins is settable as a comma-separated list that replaces the whole allow-list (each entry validated as a scheme://host[:port] origin; "" clears it). The structural tables (root_agents, [theme], the [keys] rebind table) and the session_env_passthrough list have no single-scalar shape, so they are not settable here — ask the config assistant to change them (it edits the file and validates), or edit config.toml directly and run af config validate. A global write is applied to the running daemon in place (#2480), so most keys take effect at once; branch_prefix waits for the next daemon start, and the keys af itself reads (auto_update, update_channel, detach_keys) wait for the next af launch. set prints which of those applies to the key you just wrote. A raw hand-edit is not applied for you and takes effect on the next af/daemon start.

Maintenance commands

af version             # print the version and the release URL
af debug               # print the resolved config and its path
af keys                # print the effective TUI key bindings (defaults + [keys] rebinds)
af upgrade             # self-upgrade to the latest GitHub release (Linux/macOS)
af doctor --setup      # verify first-run prerequisites and writable storage
af doctor              # diagnose setup, leaked resources, and daemon health
af doctor --fix        # also apply the safe remediations
af bug-report          # bundle logs + versions + tasks + redacted state into one file to attach
af bug-report --json   # emit the structured manifest to stdout instead of writing a file
af reset               # nuclear option — see below

af bug-report collects one shareable diagnostics file: the daemon log tail (bounded to the last ~2MiB / 5000 lines), versions (af, Go, OS/arch, the daemon snapshot), the configured tasks, the session state from instances.json, the af daemon status health snapshot, and the global config. It writes a single text file (default ~/af-bug-report-<ts>.txt, mode 0600; override with -o/--output) so you can read the whole thing in one scroll before attaching it. Redaction is best-effort: free-text and secret-bearing fields (session titles, session prompts, task prompts, tab commands, remote metadata) are dropped, $HOME and your username are collapsed to ~ / [user], and known credential shapes are scrubbed everywhere — but perfect redaction is impossible, so review the file before sharing it publicly. It is read-only and local (like af doctor): it never dials the daemon or the network, and is not part of the HTTP af api surface.

By default it also opens a pre-filled GitHub issue draft in your browser, always against the agent-factory project — never whatever repo you happen to be in, since the report is about af itself. The draft body carries a bounded, redacted excerpt of the key diagnostics (versions, daemon status, counts, and the newest log lines) so the report is useful as filed; the complete bundle is far too large for a URL, so it stays on disk for you to attach. Nothing is submitted for you — review the draft, drag the bundle onto it, and click Submit yourself. Use -o/--output or --file to skip GitHub and only write the file.

af doctor --setup is the first-run profile: it checks AF home/config/state/log writability, git and git identity, tmux, configured agent commands, daemon health, and remote-hook setup for the current repo when configured.

af doctor is read-only by default: it reports orphaned processes left behind by dead sessions, processes pegging a CPU core inside live sessions, af_ tmux sessions with no backing record, abandoned temp agent-factory homes, and daemon problems (stale socket, stale pid file, a daemon still running a replaced binary). With --fix it kills orphans whose ancestry markers prove they came from a dead Agent Factory session and removes stale temp homes, logging each action; anything it cannot verify is reported, never touched, and stays advisory rather than failing the run. Exits 1 only when an actionable condition remains — advisory warnings do not.

When the repository you run it in configures a remote-hook backend, af doctor also validates that setup, so a misconfigured remote surfaces as a diagnosable problem instead of a cryptic failure at session-launch time:

  • remote-config — the required remote_hooks commands (one of provision_cmd/launch_cmd, plus delete_cmd) are present, naming the missing field and the in-repo config file when one is not. It also rejects a config still carrying the removed pre-PR7 keys (list_cmd/attach_cmd/terminal_cmd), naming the stale key and pointing at the migration recipe.
  • remote-hook-script — every configured hook command resolves to something runnable: a path that exists and carries the execute bit (with the exact chmod +x fix otherwise), or a bare name found on $PATH.

There is no connectivity round-trip probe: the provision-and-expose contract has no read-only verb (launch_cmd provisions real infrastructure and starts an af agent-server; delete_cmd tears it down), so the live wire round-trip is exercised by actually creating a session, not by a doctor dry-run.

These checks run only for a repo that configures remote_hooks. Run outside a git repo, or in a repo with no remote backend — the common local-only case — they collapse to a single n/a — no remote backend configured line and add no findings, so local users see no new noise. The remote checks are validated against the current working directory's repository; run af doctor from inside the repo whose remote setup you want to check.

af reset stops every af daemon running for this AF home (the managed one plus any orphan left behind by an upgrade or a source-built agent-factory --daemon), removes the daemon sockets so a stale daemon or socket cannot serve the next af, kills all Agent Factory tmux sessions, removes every linked git worktree (and its branch) from each repo that has stored sessions — including worktrees you created by hand — wipes the durable project registry (registered bindings and their reachable checkout identity markers for this AF home), and deletes all stored session records. Only daemons owned by you AND using this AGENT_FACTORY_HOME are touched; a daemon or autostart unit for a different AF home is left alone. Use it to recover from a corrupted state, not for day-to-day cleanup — af sessions archive <title> (or a in the TUI) finishes with one session while keeping it restorable.