agent manual
Table of contents
Overview
agent.el collects the Emacs integration layer for AI coding command-line tools. It provides a shared session model, a unified transient menu, notification handling, terminal helpers, backend modules for Claude Code and Codex, and an optional chief-of-staff loop.
The package is split into a small backend-agnostic core and focused modules:
| Module | Role |
|---|---|
agent.el | Core: backend registry, session identity and state machine, session keys and teardown, unified orchestration commands (handoff, restart, exit, resume, branch navigation, skills, backtrace debugging), transient menu, alerts, theme sync |
agent-account.el | Multi-account support shared by all backends: selection, persistence, per-account config homes, symlinked shared state |
agent-usage.el | Per-account usage store and poller shared by all backends |
agent-doom-modeline.el | Optional doom-modeline integration: the ai-session modeline of session buffers, defined only once doom-modeline is loaded |
agent-capture.el | Persisted Org prompt capture, keyed to the session it targets |
agent-project.el | Account-scoped project sources: registry and path-pattern candidates, model ranking, and the project prompt for things that carry no directory of their own |
agent-slack.el | Slack-message-to-project routing: reads the message at point as text with no directory of its own, leaving the project to the shared project reader |
agent-forge.el | Forge-notification-to-project routing: starts a session in the working tree of the repository the notification belongs to |
agent-mu4e.el | Email-to-project routing: reads the mu4e message at point as a maildir path, leaving the project to the shared project reader |
agent-snippet.el | Yasnippet TAB expansion and snippet insertion in eat-based session buffers |
agent-todo.el | Org TODO workflows that work with any backend: batch processing of a list of entries and routing the entry at point to a session |
agent-util.el | File helpers shared by the backends, such as reading a transcript’s first line whatever its length; depends on no other module |
agent-claude.el | Claude Code backend: integrates claude-code.el with status and usage polling, hook setup, and agent-claude-mode |
agent-claude-cli.el | Quarantined undocumented Claude CLI conventions: Keychain credential naming, the usage API endpoint, the ~/.claude/projects/ directory encoding, transcript JSONL parsing, and the .claude.json merge engine |
agent-codex.el | Codex backend: integrates codex.el with theme sync, codex exec execution, config.toml helpers, modeline helpers, and agent-codex-mode |
agent-chief.el | Inspectable chief-of-staff loop: Emacs owns timers, state, context collection, and notifications, while Claude Code or Codex provides one-shot judgment on each tick |
agent-setup.el | First-run checks: finds the agent CLIs and emacsclient, starts the Emacs server, checks the hooks passed to Claude sessions and offers to remove copies left in global settings, offers to link the bundled skills, and reports what is left |
The main workflow is to load agent.el, require the backend modules you use, and enable their minor modes (see Backend minor modes); loading a backend module installs nothing by itself. Once the Claude and Codex modules are active, M-x agent-menu and M-x agent-select-session operate across both tools. Sessions are listed with their account in a column of its own, highlighted when waiting for input, and disambiguated by project and instance name when several sessions share a directory.
The package depends on transient for menus, consult for completion, claude-code.el for the Claude backend, and codex.el for the Codex backend. All four are declared in the package metadata so that the package manager builds agent with them available; the backend modules hard-require their respective backend at load time. Optional integrations use packages such as yasnippet, doom-modeline, gptel, agent-log, and monet when those are available.
Architecture
Session identity
Every live session buffer carries a buffer-local agent-session struct, captured once when the session starts. Its slots are backend (the backend symbol), account (the account name, or nil for the default), directory (the abbreviated project directory), instance (the instance name, or nil), and id (the CLI session id, or nil until known). The function agent-session returns the struct for a buffer, and agent-session-buffer-name derives the canonical buffer name from the identity. All orchestration code reads identity from this struct; nothing parses buffer names.
The id slot holds the backend’s native session id and is maintained for the whole session lifetime: a non-fork resume seeds it in agent-start-session, the Claude backend records the session_id reported by every status poll (so a branch switch, which changes the id, updates it), and the Codex backend records the id codex-session-identity reports on every CLI hook event. A fresh session therefore acquires its id within one status poll interval (Claude) or at its first hook event or prompt submission, whichever comes first (Codex); a fresh app-server Codex session knows its thread id before any CLI hook event fires, so the submission hook is the earliest per-buffer signal. Whenever the recorded id changes, the abnormal hook agent-session-id-functions runs with the session buffer, so optional integrations — such as the Agent Log bridge — can observe identity without reading private variables. The public surface for such integrations is agent-session, the agent-session-id accessor, agent-session-buffers (all live session buffers across backends), and agent-session-display-state.
Session state machine
Each session buffer tracks its readiness in the buffer-local variable agent--session-state, which is one of unknown, busy, awaiting-input, or closing. Buffers start as unknown and stay there until a session event arrives, so a state that was never observed is never presented as one that was; the session switcher renders such sessions with agent-unknown and agent-jump-to-waiting skips them. All transitions go through agent-session-event, which accepts the events stop, idle-prompt, blocked, submit, activity, and exit-request. An activity event marks the session busy on evidence that it is working. It exists because turns the user did not type in Emacs — one driven by remote control, a scheduled task, or a resumed background job — would otherwise be unobservable, leaving the session displayed as waiting while it works. Claude Code has no busy/idle state of its own, so the backend assembles one from its hooks. The bundled notify-emacs-state.sh wrapper forwards the UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, and SubagentStart hooks as activity, whoever started the turn, and Stop and StopFailure — a turn ended by an API error, which fires no Stop hook — as stop. The tool hooks also return a session to busy once the user answers a permission dialog. Because these hooks fire on every tool call of every session and subagent, most of them report a session Emacs already marks busy. To keep that traffic off the Emacs server, agent-claude--record-state mirrors each Claude session’s state to a file in agent-claude-status-directory, keyed by the session’s per-process UUID like its statusline file, and the wrapper skips an activity event while that file reads busy. A missing or unreadable file sends the event as before, and a failed write deletes the file. Other backends can observe transitions through agent-session-state-change-functions. Each forwarded event carries its send time, and agent-claude--stale-hook-event-p discards one sent before the session last began waiting, because background delivery can land a turn’s final tool event after its stop event. The Stop hook delivers in the background too, so that a stalled Emacs server never holds up the end of a turn, and each forwarded call waits at most ten seconds for the server; a stall can then deliver one turn’s stop after the next turn has begun. agent-claude--stale-stop-p therefore discards a stop sent before the event that began the session’s current turn: the first hook event of the turn, or, for a turn without one, such as a prompt sent from Emacs, the moment Emacs marked the session busy. A hook that fires inside a subagent carries the subagent’s agent_id, which the wrapper forwards too. A background subagent keeps calling tools after the main turn ends and while the main thread waits at a dialog, so agent-claude--subagent-hook-event-p ignores a subagent’s event while the session awaits input, unless the session is blocked on a dialog that same subagent raised: the Notification payload names the subagent behind a permission or elicitation dialog, and agent-claude--blocking-agent records it, or main for the main thread, until the session’s state next changes. A session whose background subagent is working therefore stays waiting, shown with agent-waiting-with-background when the backend reports the task. A turn Claude starts without a prompt and runs without tools fires none of these hooks, so the status-line poll remains as a fallback: Claude’s status line reports a fresh prompt_id for every turn whatever its origin, and agent-claude--detect-turn-start emits activity when the identifier changes, within one poll interval, agent-claude-status-interval. A turn shorter than that interval both begins and ends between polls, and its stop event has already landed by the time the change is seen, so that case is detected and skipped rather than stranding the session as busy. Interrupting a turn fires no hook at all, so an escape sent with claude-code-send-escape to a busy session schedules agent-claude--check-interrupt: after agent-claude-interrupt-check-delay seconds, if the session is still busy with no state change since the escape and its terminal shows the idle input prompt, the session is marked blocked. An interrupt from outside Emacs, such as through remote control, is not detected until the next turn ends. A blocked event marks the session as waiting on the user without firing a ready alert, and is used for permission and elicitation dialogs: Claude reaches those from inside a turn with a tool call still in flight, so nothing else reports them as waiting even though they proceed only once the user answers. Backends translate raw CLI notifications into these events and never set session state directly. A stop or idle-prompt event first offers the transition to the before-exit chain; when the chain consumes it, ready alerts and display refreshes are suppressed. Ready alerts fire only for idle-prompt events, and a submit event delivered while the session is already busy is ignored, because backend submission hooks can fire more than once per submission.
Session teardown and leak policy
Session resource cleanup has a single owner: agent--session-teardown runs a session buffer’s agent--teardown-functions exactly once, whether the buffer is killed first or the CLI process exits first. Backends and features register their per-session resources (status poll timers, monet servers, before-exit chains) as teardown functions instead of installing their own kill-buffer hooks. A session key has buffer lifetime instead: assigning one atomically installs its buffer-kill cleanup, while session teardown can release it early after process exit. Claude buffers also adopt any already-running Monet server when agent-claude-mode is enabled or reloaded. The monet websocket server needs one extra guard: when the Claude client disconnects, monet--on-close-server drops the session from monet’s own table before teardown can look it up, so an :after advice closes the listening server at that moment rather than letting it escape to the GC safety net. Resources that escape teardown are not silently reaped: safety nets report each one through a Warnings buffer entry tagged agent, naming the leaked resource. A leak warning therefore indicates a lifecycle bug worth reporting, not routine noise. The Monet GC sweep identifies servers by ownership, not by process name: Agent registers every server process it sees monet create or adopt, and the sweep only ever reaps registered servers that have lost their monet session. Websocket servers belonging to other packages (atomic-chrome, org-roam-ui) share the generic websocket server on port N process name but are invisible to the sweep and can never be reported or killed as monet leaks.
Account model
Multi-account state lives in agent-account.el and is keyed by backend. Each backend declares an accounts alist mapping account names to per-account configuration homes (agent-claude-accounts, agent-codex-accounts), a persisted selection file that survives Emacs restarts (agent-claude-account-file, agent-codex-account-file), and the environment variable that points the CLI at the selected home: CLAUDE_CONFIG_DIR for Claude Code and CODEX_HOME for Codex. Account homes keep credentials separate, while shared state (settings, hooks, skills, sessions, history) is symlinked from the backend’s canonical home so it remains available after switching accounts; conflicting account-local files are backed up to timestamped *.agent-backup-* siblings first. Codex hook trust is keyed by the lexical hooks.json path rather than the symlink target, so account sync copies the canonical path’s persisted trust state to the account path. It copies existing hashes rather than computing new ones; a changed canonical hook therefore remains untrusted. Any sync failure aborts session startup instead of letting a session run with missing or stale policy. agent-account-select persists a choice and syncs the account home, and agent-account-init prepares or repairs a home. The account infix in agent-menu always asks which backend to change before it asks for an account; it never silently infers the backend from the buffer where the menu happened to be opened. When the merge engine reads a home’s .claude.json, a parse failure is retried over increasing delays before warning, since a running CLI rewrites the file frequently and a read can land mid-write; a failure that persists is warned about once, with the invalid bytes preserved in a snapshot file so a torn read can be told apart from real corruption. Writes go to a temporary file that is renamed into place, so concurrent readers never observe a truncated .claude.json. The account a session was started under is recorded in its agent-session struct, so restart and handoff preserve it even after the global selection changes.
An accounts entry is either (NAME . HOME) or (NAME :home HOME :pool POOL ...). The plist form carries arbitrary further properties, read with agent-account-property, for integrations that need per-account settings beyond the home. Accounts sharing a :pool are interchangeable: they hold the same configuration and differ only in whose quota they spend. The selection prompt lists pools ahead of accounts, and selecting a pool persists the pool name, so agent-account-current may answer with a pool; agent-account-resolve always answers with a concrete account, routing a pool selection through agent-account-route. Selecting a pool syncs every member’s home, so routing never has to touch the filesystem. A member chosen by name pins that account as before. A pool may carry the same name as one of its members — convenient when an existing selection file names the first of what became several equivalent accounts — in which case the name is listed once and selects the pool, so that member can no longer be pinned by name.
Routing skips members whose credentials are missing, then sets aside members whose latest fresh usage reading (Account usage) flags them as limited or shows either window at 100%. The rest are ranked by agent-account-usage-score: the larger of the weekly and session percentages, with the weekly share breaking ties, so lower is better; a member with no fresh reading ranks after every member with one. The member the pool was last routed to is kept unless a sibling beats it by agent-account-route-hysteresis points (default 10), so near-equal members do not alternate between consecutive sessions — except that a current member with unknown usage never holds off a sibling with a known score. When every member is limited, the one whose window resets soonest is returned, and the echo area says so, so the session start can still report the limit. A route that changes hands is announced with the chosen member’s usage; a resolution that keeps the previous member stays silent, since routing also runs for background processes and would otherwise flood the echo area. Every routing decision is followed by a refresh of the pool’s readings. A restart of a session whose account belongs to the selected pool reruns routing without prompting, so an exhausted member can be replaced by one with capacity. Both backends share session history between account homes, so the conversation is preserved.
The persisted selection is global, so on its own it would start a session for one organization’s project under whichever account was last selected for another. agent-account-directory-rules ties directories to accounts: each rule is (DIRECTORY . SELECTION), where SELECTION is an account or pool name for every backend or an alist of (BACKEND . NAME) entries, and it governs DIRECTORY and every directory below it, with symbolic links resolved on both sides. When no rule covers a session’s directory, the rules are matched against its common Git directory, so a linked worktree checked out elsewhere follows the rule of its repository; a repository whose Git directory lives apart from its working tree (git init --separate-git-dir) needs a rule for that Git directory for its worktrees to be covered. The first matching rule wins, and a backend missing from a rule’s alist is not governed by it. agent-account-resolve consults the rule for the session’s directory after the account of a session being started and before the persisted selection, so a rule naming a pool still routes among its members. To make that possible, agent-start-session fills a session’s missing directory from the backend’s ambient directory (the current buffer’s project) before it resolves the account, and the commands that start a new session do the same; the backends’ environment hooks and non-interactive Claude runs resolve against the directory they run in, and agent-restart compares the session’s account with the one its directory’s rule names rather than the global selection. A rule naming an account or pool the backend does not declare signals an error instead of falling back to the persisted selection. An account the caller passes explicitly, such as the one recorded on a session being restarted, handed off or restored, still wins, so sessions that are already running keep their account. For example:
(setopt agent-account-directory-rules
'(("~/repos/epoch/" . ((claude-code . "epoch") (codex . "epoch-pool")))
("~/My Drive/Forethought/" . "forethought")))
Credentials are part of the account model too. A backend may declare the account-local credentials file inside each home (auth.json for Codex) and the CLI arguments of its login flow. agent-account-login — bound to a l in agent-menu — runs that login flow with the selected account’s environment, so the credentials always land in the right account home; running the bare CLI login in a terminal would write to the backend’s default home instead. The command shows the login output, including the authentication URL, in a dedicated buffer and reports success or failure when the flow finishes. Session starts guard on the same declaration: when an account’s credentials file is missing — after the in-session logout command, or when tokens were removed out of band — agent-start-session refuses to spawn a session that could only fail to authenticate and points at agent-account-login instead. Backends that do not declare a credentials file (Claude Code stores credentials in the macOS Keychain) are assumed logged in and never blocked.
Account usage
Per-account usage lives in agent-usage.el and is keyed by backend and account. A backend that can report usage declares a :usage-fetch slot: a function called with an account name and a callback, which it calls with a normalized plist, or with a failure from agent-usage-failure that gives the reason and, when the server sent one, its Retry-After delay. The plist carries :session-pct and :weekly-pct (percent of the short and long windows used), :session-reset and :weekly-reset (reset times as float seconds), :limited (non-nil when the backend refuses further requests), and :fetched-at. Claude reads the undocumented api/oauth/usage endpoint with the Keychain OAuth token; Codex reads the ChatGPT wham/usage endpoint with the access token and account id stored in the account home’s auth.json, classifying a window of at least a day as weekly and a shorter one as the session window, so a plan with a single weekly window reports only that. agent-usage-get answers the latest reading and agent-usage-fetch refreshes one account.
One poller serves every backend. It starts with the first session of any backend and stops when the last one is torn down, polling every account that has a live session plus every member of every pool, so routing has data for idle members too. agent-usage-interval defaults to 300 seconds. A failed fetch keeps the account’s last reading and records the reason on it as :error. A request that got no HTTP answer at all, such as one cut off while the machine slept, is a network failure (agent-usage-network-failure): it says nothing about the account, so it is retried at the next poll and announced nowhere but the usage buffer. Any other failure also records :retry-at, the time before which the poller and pool refreshes leave the account alone. The wait is the server’s Retry-After when it sent one, however long; otherwise the account’s own wait starts at agent-usage-interval and doubles with each consecutive failure up to agent-usage-max-interval, which defaults to 900. Backoff is per account, so one failing account neither slows the others nor is reset by their successes. g in the usage buffer skips the backoff but still honours a server’s Retry-After. The echo area reports such a failure when its reason changes and again when the account recovers. A Claude token the endpoint rejects is reported as needing Claude Code to start as that account, which renews it. A Codex access token that has expired is reported as needing Codex to run as that account, since the Codex CLI refreshes an account’s token only when it runs with that home. Readings are written to agent-usage-cache-file after every fetch and read back on first use, so a fresh Emacs can route before its first poll completes; a reading older than agent-usage-stale-after seconds no longer counts for routing. The Claude status helpers agent-claude-status-session-usage and friends read this store and keep answering ISO timestamps for the reset times. Claude usage requests report synchronous network failures, including DNS lookup errors, as failed fetches without entering the debugger. Only stale URL process write failures are retried immediately, once.
agent-usage-show — bound to a u in agent-menu — lists every configured account, live-session account, and pool member in a *agent-usage* buffer with its pool, session and weekly percentages, weekly reset time, limit flag, the age of the reading, and the last fetch error with its retry time (a network error, dimmed, with the time it happened), then fetches fresh readings and re-renders as each arrives; g refreshes again. Configured accounts appear even without a live session; backends without configured or active accounts show their default account. Background polling remains limited to live-session accounts and pool members. The limit flag and the percentages are what routing acts on, so the buffer shows at a glance why a pool went to the member it did. A reading older than agent-usage-stale-after, which routing ignores, is dimmed and its age highlighted, and a window whose reset time has passed shows no percentage or reset, since the reading no longer describes the current window.
Backend minor modes
The global minor modes agent-claude-mode and agent-codex-mode own every hook, advice, timer, and notification handler their backend installs. Loading agent-claude.el or agent-codex.el installs nothing; the modes must be enabled explicitly. Disabling a mode removes its hooks and advice symmetrically and restores the backend’s previous notification function, leaving running sessions untouched. A typical setup:
(use-package agent
:config
(require 'agent-claude)
(require 'agent-codex)
(require 'agent-chief)
(agent-claude-mode 1)
(agent-codex-mode 1))
User options
Shared session behavior
The user option agent-protect-buffers is a boolean that defaults to t. When enabled, killing a live AI session buffer prompts for confirmation through agent-protect-buffer. Buffers whose processes have already exited are allowed to die without the prompt.
The user option agent-warn-kill-with-branches is a boolean that defaults to t. When enabled, killing a session that other sessions were branched from asks a second confirmation naming how many branches it has, after the ordinary kill-protection prompt. The check scans only the sessions that could descend from the one being killed, and any failure to scan allows the kill, so a session never becomes unkillable because its transcripts moved. It replaces agent-claude-warn-kill-with-branches, which is kept as an obsolete alias; that option was declared but never read by any code, so setting it had no effect until the check described here existed.
The user option agent-branch-worktree-directory names the base directory for the git worktrees agent-create-branch creates with a prefix argument (Branch navigation). It defaults to a location under the cache directory, away from cloud sync, and replaces agent-claude-fork-worktree-directory, which is kept as an obsolete alias. The default path still ends in the literal segment claude-worktrees, unchanged from the Claude-only original so that worktrees created before the rename are still found where they were left.
The user option agent-alert-on-ready is a boolean that defaults to nil. When enabled, backend notification handlers can call agent-notify to emit visual and/or audible alerts when a session is waiting for input. Codex’s own terminal-bell notification path is routed through the same alert setting. Visual alerts preserve the current echo-area message even when the selected alert.el style runs a synchronous notifier process. agent-alert-style selects visual, sound, or both; it defaults to both. agent-alert-sound defaults to nil; set it to a sound file to enable sound alerts. agent-alert-sound-player optionally names an external sound player when Emacs sound support is unavailable.
The user option agent-backtrace-file controls where agent-save-backtrace and agent-debug-backtrace write Emacs backtraces. It defaults to a file in temporary-file-directory.
The unified orchestration commands share one set of options across backends. agent-handoff-files maps backend symbols to the handoff files read by agent-handoff. agent-debug-backtrace-model and agent-debug-backtrace-backend configure the gptel request used by agent-debug-backtrace to identify likely culprit packages from an Emacs backtrace.
Slack-message routing has no options of its own any more. The model and gptel backend that pick a project from the message are the shared agent-project-ranking-model and agent-project-ranking-backend (Project sources), and agent-act-on-slack-message-model and agent-act-on-slack-message-backend are obsolete aliases of them. The project candidates come from agent-project-sources, so a registry that used to be named by agent-epoch-project-registry-file now belongs in that alist: that option is obsolete and nothing reads it, which means a configuration setting it alone routes against the default source list instead of the registry. Loading agent-project warns when that option still holds a value, because marking a variable obsolete warns only code that mentions it and says nothing to a configuration that merely sets it, which is the whole of what such a configuration does. agent-epoch-projects-root is an obsolete alias of agent-project-registry-root and still resolves the registry’s relative paths. The function agent-epoch-project-candidates, which read that registry into project plists, is replaced by agent-project-candidates, which reads every source an account has and answers in a different shape. The old name is marked obsolete but no longer defined, so a configuration still calling it is warned at byte-compile time which name replaced it, and fails if it runs. All three renamings are declared in agent-project.el ahead of the options they rename, because a setting made before the package loads — which is what use-package’s :custom does — reaches the new name only while that name is still unbound.
The user option agent-prompt-capture-directory controls where agent-capture-prompt stores per-session Org files. Each file is keyed by backend, account, project directory, and session instance, so prompts are linked to the session they target and remain available after Emacs restarts. agent-prompt-capture-auto-save-delay controls the idle delay before capture buffers are saved automatically.
The user option agent-sync-theme is a boolean that defaults to nil. When enabled, theme changes in Emacs are persisted through each registered backend’s theme-sync handler. This updates configuration files for new or restarted CLI sessions without sending theme commands into active terminal conversations.
The user option agent-sigwinch-delay is a number of seconds and defaults to 0.5. It controls how long agent-fix-rendering waits before sending SIGWINCH to a terminal process. Increase it if an AI CLI reads the terminal size before the eat window has settled.
The user option agent-session-annotation-max-width caps the width of session annotations in the session switcher (Shared commands). It defaults to nil, which applies no cap beyond the fit to the frame. An integer narrows annotations to at most that many columns; it never widens them past what the frame holds, since the frame fit applies either way. Text longer than the resulting width is truncated with an ellipsis. A cap of 0 leaves no room at all, and every session then renders as the bare label it would have without an annotation, rather than a padded label followed by nothing.
Snippet helpers use yasnippet and consult-yasnippet only when those optional packages are loaded.
Project sources
The agent-project module enumerates the projects a session can be started in when the thing the session acts on carries no directory of its own. The user option agent-project-sources is an alist of (REGEXP . SOURCES). The regexp is searched for anywhere in the account name of the backend that will run the session — anchor it with \` and \' to match a whole name, since a bare "epoch" also matches an account called epoch-personal — and the first matching entry wins, so an empty regexp is a catch-all that also covers a backend with no account selected. It defaults to one catch-all entry listing project-known followed by ~/repos/*, ~/code/*, ~/src/*, ~/projects/* and ~/Developer/*; a directory that does not exist contributes nothing.
Each source is the symbol project-known, a registry file, whose name ends in .json, a (FILE . ROOT) pair naming a registry file together with its root, or a path pattern. project-known stands for the local projects Emacs remembers, as listed by project-known-project-roots; remote roots and roots that no longer exist are passed over.
A path pattern may contain wildcards. A wildcard is *, ?, or a [...] character class, and a pattern containing one is expanded a single level per wildcard component: ~/repos/* reaches the children of ~/repos/, and ~/repos/.worktrees/*/* its grandchildren two levels down. Wildcard expansion keeps only git repositories and worktrees; a worktree records its git directory in a file rather than a directory, and both count. A pattern without a wildcard names exactly one directory, which is kept whether or not it is a repository, and dropped when it does not exist or is not a directory. A pattern that cannot be expanded at all, such as one holding an unbalanced [, is reported in the echo area and skipped, so one malformed source never suppresses the candidates of the others.
A registry is a JSON file holding a projects array, in the format the obsolete agent-epoch-project-registry-file used to read (Shared session behavior). Every entry becomes one candidate, labelled by its id and title, or by its id alone when it records no title. Unlike a path pattern it carries a description: the entry’s summary, or its aliases when it records no summary, followed by its browser_keywords and its Slack channels; an entry recording none of the four has no description at all rather than an empty one. The keywords are worth their length: they are the terms the project is recognized by elsewhere, repository slugs and domain names among them, which is what a routed Slack message or email is likeliest to name, whereas aliases are often the title restated. An entry that records a JSON null where a string belongs counts as recording nothing, so a null summary falls back to the aliases just as an omitted one does. Later stages use that text to judge which project a piece of text is about. An entry’s directory is its first project_doc_paths folder when it records one, because that folder holds the project’s instruction files, its first repo_paths directory when it records only that, and the registry’s root itself when it records neither. Relative paths are expanded against the root of the registry that records them, and each registry finds its root in this order: the ROOT of a (FILE . ROOT) source; for a registry at .../projects/shared/project-registry.json, that projects directory two levels above the file; and finally the user option agent-project-registry-root, which defaults to nil. Each registry therefore resolves against its own directory, so an account listing a shared registry and a personal one gets each project where its own registry places it. A registry with no root by any of these routes can still be read while every path it records is absolute; a relative path in it raises an error. A source naming a registry that does not exist raises an error rather than being skipped, since it points at a file the user meant to have.
The function agent-project-candidates returns the candidates for an account as plists with :label, :directory, :repository and :description. The repository is the candidate’s local clone: the directory itself for a path source that is a git repository or worktree, the first repo_paths directory for a registry entry, and nil for a project holding no clone of its own. It is what agent-project-repository-directory answers from, which returns the clone of a named repository for an account, or nil when no source holds one that exists. Forge routing asks it for a repository whose worktree Forge does not know or no longer knows, which is what a repository that moved looks like (Shared commands). Candidates are deduplicated by true path, so listing the same directory twice under different names, or through a symbolic link, yields one entry. A label shared by two candidates is replaced with the abbreviated directory, which is what then appears in completion, so no two projects offer the same string.
The function agent-project-read asks for one of those candidates. It takes the account, the text of the thing being routed, the completion prompt, a callback and an optional suggested directory, and calls the callback with the directory of the project chosen. A suggestion is offered first, which makes it what a bare RET answers, and it joins the candidates when no source holds it, so a caller that worked its directory out for itself leads the choice instead of bypassing it. Nothing is ranked when a suggestion is given, since a caller that already knows the project has no use for a model’s opinion of it. It answers through the callback even when nothing is ranked, so every caller reads the answer the same way: the directory arrives in the callback, never as a return value. An account whose sources contribute no candidate at all raises an error rather than offering an empty prompt.
The candidates are ordered by a model before completion only when there is something to order them by: the routed text must hold more than whitespace, and at least one candidate must carry a description holding more than whitespace. A blank string counts as no text on either side, so candidates that came from path patterns alone, or from registry entries whose summary, aliases, keywords and Slack channels are all empty, go straight to completion and cost no request. An empty summary alone is not enough: the keywords and channels are appended to it, so an entry recording a blank summary and one keyword still carries text and is still ranked. When the request is made, agent-project-ranking-model and agent-project-ranking-backend name the model and the gptel backend it runs on; they default to gemini-flash-lite-latest on the "Gemini" backend, and ranking requires gptel, which is otherwise an optional dependency. The request is sent from temporary-file-directory rather than the current buffer’s directory: a Slack callback runs in whatever buffer happens to be current, and when that buffer’s directory has been deleted, starting curl there fails. The model is asked for the candidate labels, comma separated, most likely first. A name it returns is matched to the label it equals, and failing that to the shortest label it starts, so a name the model shortened still finds its project while a label that is itself the start of another still names its own. The candidates it names move to the front of the completion list in the order it gave them, each appearing once however often it is named, and the rest keep their original order behind them, so an answer that names only some projects, repeats one, or invents a label never hides a project. A request that fails is reported in the echo area and leaves the original order standing, so a ranking failure costs order rather than the whole action.
Org TODO workflows
The agent-todo module has two options, both formerly named after Claude. agent-todo-log-directory names the directory under which agent-batch-todos writes one timestamped run directory per batch, holding the raw backend output for each entry; it replaces agent-claude-log-directory. agent-todo-in-progress-keyword is the Org TODO keyword agent-send-todo-at-point moves a heading to once it has been sent, and it replaces agent-claude-org-todo-in-progress-keyword. Both old names remain as obsolete aliases. The keyword defaults to nil, which leaves the TODO state alone, because Org ships with only TODO and DONE and has no intermediate state to move to; set it to whichever in-progress keyword your own org-todo-keywords declares.
Claude Code backend
The Claude backend options control account selection, session metadata, and non-interactive execution. agent-claude-accounts is an alist of account names to Claude configuration directories, either as bare paths or as plists with a :home and an optional :pool (Account model), and agent-claude-account-file stores the selected account across Emacs sessions. Use agent-claude-select-account or the shared account infix in agent-menu to switch accounts before starting a new session. Both commands are thin wrappers around agent-account-select, which persists the choice and syncs the account’s config home; agent-account-init prepares or repairs any backend’s account home.
Status polling is controlled by agent-claude-status-interval, which defaults to 5 seconds and reads status JSON emitted by the bundled statusline script. A status file caught empty or half-written is skipped until the next poll. agent-claude-interrupt-check-delay, 1.5 seconds by default, is how long after an escape the backend waits before checking the terminal for an interrupted turn (Session state machine). Usage polling is shared with the other backends (Account usage); agent-claude-usage-interval and agent-claude-usage-max-interval are obsolete aliases of agent-usage-interval and agent-usage-max-interval. The Claude fetcher reads the account’s OAuth token from the macOS Keychain, so accounts authenticated with an API key rather than an OAuth subscription—whose OAuth Keychain store is an empty JSON object—are silently skipped rather than treated as a broken lookup. The Keychain lookup runs from the temporary-file directory, so a poll that fires while the current buffer’s default-directory names a deleted worktree cannot break the subprocess spawn.
Batch execution options are passed to claude -p. agent-claude-batch-permission-mode defaults to "auto", agent-claude-batch-max-turns defaults to 30, and the allowed tool, model, and appended-system-prompt options let you narrow or expand what non-interactive Claude tasks may do.
Four options that used to live here now belong to the backend-agnostic layer, because the workflows they configure work with either backend: agent-claude-warn-kill-with-branches is now agent-warn-kill-with-branches and agent-claude-fork-worktree-directory is now agent-branch-worktree-directory (Shared session behavior), while agent-claude-log-directory is now agent-todo-log-directory and agent-claude-org-todo-in-progress-keyword is now agent-todo-in-progress-keyword (Org TODO workflows). Each old name remains as an obsolete alias, so an existing configuration keeps working and reports the new name.
The backend’s status line and hooks reach each Claude session it starts through Claude Code’s --settings switch, so neither Claude Code sessions started outside Emacs nor the user’s settings files are touched. While agent-claude-mode is on, every session started through claude-code--start — which every interactive and resume path in claude-code and this package uses — receives a --settings file in agent-claude-session-settings-directory holding them; the non-interactive claude -p runner gets none, since nothing observes its state. The file’s name is a hash of its contents and it is never rewritten, so its commands always name the checkout loaded in this Emacs, and Emacs instances sharing the directory but loading different checkouts each pass their own; files unused for a week are deleted when the mode is enabled. Claude Code honors only the last --settings switch, so one the user already passes, in claude-code-program-switches or a command’s extra switches, as a file or as inline JSON, is removed and its settings are merged into the file instead: hook lists are combined, and a status line the user sets is kept unless the package owns it. Set agent-claude-use-session-settings to nil to pass nothing. Copies of these hooks in the user’s own settings, written by earlier versions or by agent-claude-setup-config, would make each event arrive twice; agent-claude-remove-global-config removes them from agent-claude-settings-file and leaves everything else. It recognizes the package’s commands by its bundled helpers under any checkout path, and the generic claude-code-hook-wrapper stop command that earlier versions wrote only in that exact form, alone in its group. agent-claude-setup-config remains for anyone who wants the hooks in sessions started outside Emacs: agent-claude-settings-file names the settings file it and the individual ensure commands update. agent-claude-statusline-script points to the bundled statusline helper. agent-claude-hook-wrapper, the upstream claude-code hook wrapper earlier versions ran as the Stop hook, is obsolete: that hook waited for Emacs, so a stalled server held each turn open for up to Claude Code’s ten-minute hook timeout. The bundled statusline and hook helpers are located in the package’s source checkout, even when a package manager such as Elpaca loads byte-compiled files from a separate build directory that holds only the Lisp sources. agent-claude-status-directory controls where the helper writes per-buffer status JSON files. The statusline setup refreshes an existing agent-owned claude-code-statusline.sh command, which keeps the settings file correct after the package moves or is renamed, while preserving unrelated custom Claude statusline commands. The turn-lifecycle setup likewise replaces any notify-emacs-state.sh command under the same hook, so a rerun after switching checkouts leaves exactly one. Settings written this way still name a specific checkout and go stale when another one is loaded. Set agent-claude-settings-package-directory to a path through a symlink that follows the active configuration, and setup writes the helpers under that path instead; the session settings ignore it. Loading the backend does not modify Claude Code configuration. Each Claude session is started with EMACS_SOCKET_NAME naming this Emacs’s server socket, or EMACS_SERVER_FILE for a TCP server, so the hooks’ emacsclient calls reach the Emacs that started the session even when several Emacs instances run servers.
Codex backend
The Codex backend options control account selection, skill discovery, and non-interactive execution. agent-codex-accounts is an alist of account names to Codex home directories, either as bare paths or as plists with a :home and an optional :pool (Account model), and agent-codex-account-file stores the selected account across Emacs sessions. Account homes keep credentials such as auth.json and writable config.toml settings separate. A missing account config is copied from ~/.codex/config.toml once; an existing config symlink is replaced with an independent copy. Canonical-home path references in these initial copies are rebased to the destination home; existing regular configs are preserved. Later preference and browser configuration changes stay local to each account, while canonical hook trust is copied to the account config for its shared hooks. Installed plugins are also account-local: existing plugin symlinks are materialized once so browser services resolve inside the account home trusted by Codex. Missing plugin directories are copied from the canonical home once; later plugin updates stay account-local. The canonical home itself is left unchanged. Shared Codex state such as hooks.json, skills, sessions, and history.jsonl is symlinked from ~/.codex/ so hooks, skills, and chat history remain available after switching accounts; the Claude backend shares state between account homes the same way. When an account-local file conflicts with a shared item, it is backed up to a timestamped *.agent-backup-* sibling before the symlink is created, so no local state is silently lost. Use agent-codex-select-account or the shared account infix in agent-menu to switch accounts before starting a new session. The infix asks for the backend explicitly even when the menu was opened in a session buffer, preventing a choice intended for one backend from changing another. Both commands persist the choice and sync the account’s config home; agent-account-init prepares or repairs any backend’s account home.
agent-codex-skill-directories is a list of additional directories scanned for Codex skills. The backend also scans standard Codex skill locations and project-local skill directories.
agent-codex-exec-approval-policy, agent-codex-exec-sandbox-mode, and agent-codex-exec-skip-git-repo-check configure the non-interactive Codex calls used by skill workflows. The approval policy is passed as a top-level Codex CLI option before the exec subcommand, while the sandbox and repository-check options are passed to codex exec itself.
The gptel classifier requests used by backtrace debugging and project ranking ignore reasoning-block callback events and parse only the final response text. Backtrace debugging sends the model a truncated excerpt of the backtrace rather than the full contents: each line is cut at 400 characters and the excerpt is capped at 100,000 characters, since backtraces can embed multi-megabyte printed objects that exceed API payload limits, while the frame names at the start of each line suffice to identify packages. The agent session itself still receives the path to the complete backtrace file.
Chief-of-staff loop
The optional agent-chief module runs a periodic, auditable chief-of-staff workflow. Emacs owns the schedule, explicit state, and notifications. In the default session mode, the selected backend is an ordinary long-lived Claude Code or Codex session that receives heartbeat prompts and remains available for normal conversation.
agent-chief-backend selects codex or claude-code. agent-chief-mode defaults to session; set it to stateless to use the older one-shot JSON-decision path. agent-chief-interval controls the timer interval in seconds. agent-chief-directory is the working directory for chief sessions and one-shot calls; it defaults to user-emacs-directory. agent-chief-session-instance-name defaults to chief and is used as the instance name for the dedicated interactive session.
agent-chief-state-file is an Org file used as the durable, inspectable state store; agent-chief never writes opaque memory. Day plans and manual notes are appended there and, when a chief session is live, submitted into that session as conversational context.
State context treats only a Plan YYYY-MM-DD heading dated today as current. Older plan headings are supplied as historical context, not active obligations, unless restated in today’s plan.
agent-chief-record-model-state-updates controls whether model-proposed state updates from stateless ticks are appended to the state file. It defaults to nil because periodic model observations are not user-approved durable memory. Use explicit notes and day plans for ordinary state updates.
agent-chief-context-functions is the extension point for agenda, task, calendar, project, or custom status snippets. Each function is called with no arguments and should return a string. Nil and empty strings are ignored. The state file is included separately, truncated to agent-chief-state-max-chars characters. agent-chief-max-turns limits Claude Code ticks; Codex ticks use the existing codex exec settings from agent-codex.
In session mode, agent-chief-start-session sends only a short conversational kickoff asking the model to ask for today’s plan; it does not dump the state file or stateless JSON contract into the visible transcript. Heartbeat prompts ask the model to respond exactly No nudge. when no contact is warranted, or Nudge: MESSAGE when it should alert the user. The backend’s ordinary notification event tells agent-chief when the response is complete; agent-chief then inspects the session text since the heartbeat and calls agent-chief-notify-function only for a nudge. The default notification function logs the message and uses alert.el when available. agent-chief-system-prompt is the standing instruction that makes the loop selective and forbids inventing obligations not present in the supplied context.
In stateless mode, backend output may include echoed CLI transcript text before the answer. The chief loop scans that output and uses the last valid JSON object with a notify field as the decision, so echoed response-schema examples do not confuse the parser.
Commands
First-run setup
agent-setup checks what the package needs outside Emacs and reports the result in the *agent-setup* buffer. It looks for the claude and codex programs (claude-code-program and codex-program) and for emacsclient on the exec-path, and gives install instructions for any that are missing; it installs nothing itself. It offers to start the Emacs server when none is running, because the Claude Code hooks report session state through emacsclient. It then writes the settings file passed to each Claude session and reports where it is (Claude Code backend). When agent-claude-settings-file still holds the package’s status line or hooks, it offers to remove them with agent-claude-remove-global-config, since sessions would otherwise receive each event twice; declining leaves the file untouched and is reported, and a file without such entries is neither asked about nor written. Finally it offers to link the bundled skills into each tool’s skills directory (Bundled skills). Run the command again after fixing anything it marks as missing.
Bundled skills
The package ships three skills in etc/skills/, each written for both Claude Code and Codex:
handoffprepares the prompt for a fresh session and saves it whereagent-handoffreads it. That command needs this skill, or one of your own that writes the same file.live-session-statusreports what another live session is doing by reading its transcript.message-agent-sessionsends a message to another live session withbin/agent-send.
agent-setup offers to link them into the directories in agent-setup-skill-directories, by default the skills directories of the configuration homes Claude Code and Codex read: $CLAUDE_CONFIG_DIR and $CODEX_HOME when Emacs has them set, else ~/.claude/ and ~/.codex/. It asks once per tool and links nothing without a yes. A tool whose configuration directory does not exist is skipped. A skill of the same name that the package did not link is yours and is never replaced. The links point into the source checkout, so updating the package updates the skills; a link left pointing at another checkout is offered for relinking. Set agent-setup-package-directory to a stable alias of the checkout when its real path changes, for example between Emacs profiles.
The executable bin/agent-send submits a message, read from standard input, to another live session’s buffer, and reports success only when the recipient’s transcript records it. It prepends a header saying that the message comes from another agent session and was not typed by the user. agent-send --list lists the live sessions, and agent-send --help gives the full contract. Its library, lib/python/agent_session_lib.py, holds the guarded prompt delivery and transcript checks, and other tools can load it by path.
Shared commands
The command agent-menu opens the unified transient menu. Every entry in it works with either backend: session switching, waiting-session jumps, resuming a past session, creating a branch of the current session, switching between branches, handoff, restart, exit, history browsing, skill execution, one dispatcher that acts on the thing at point, and a Prompts section beneath Tools in the same column for prompt capture, captured prompt insertion, and batch TODO processing.
The Sessions section holds general session management: new session (n),
resume session (r), select session (s), jump to waiting, and open log.
The Account section beneath Options holds login (a l) and usage (a u).
The Current session column holds branching, toggle snooze (z), handoff,
restart (R), and exit commands. It labels
agent-exit (x) as “exit” and agent-exit-without-skills (X) as “exit immediately”.
Tools use the t prefix: act on thing at point (t .) and run skill (t r).
Prompts use p: capture (p c), insert (p i), and batch TODOs (p b).
Options put account selection first, then buffer protection and branch warnings,
followed by ready alerts and theme synchronization.
The command agent-history, labeled “open log” and bound to l in the Sessions column, opens the Agent Log menu for browsing, opening, searching, and resuming historical sessions across backends. Agent Log is an optional package: loading agent never requires it, agent-history loads it on demand, and invoking the command without Agent Log installed signals a clear error. Neither agent-menu nor the session switcher reads any historical catalog while opening; catalog work (which for Codex can start an app-server process and take seconds) happens only after an explicit Agent Log action.
In EAT-backed Claude and Codex session buffers, agent-setup-scroll-keys makes PageUp and PageDown send terminal mouse-wheel events, matching the TUI scroll path used by the mouse. PageUp and PageDown in other selected buffers retain their normal bindings and never target a visible AI terminal in another window. Terminal helpers require buffer-local EAT state, so native app-server sessions cannot inherit a stale terminal from a killed buffer.
The command agent-select-session opens the session switcher, which contains only existing session entries. If no sessions are active, it reports that there are none; use agent-start-new-session (n) to start one. Sessions receive stable home-row keys where possible and display backend icons next to their labels.
The switcher lists every session in one flat list ordered by home-row key, whatever account each session runs under. The account is a column of its own, between the session name and its annotation, shown dimmed in the agent-session-account face. A session is therefore found at the key it always answers to, rather than at a position that moves when a neighbor’s account changes. Sessions whose backend has no accounts configured leave the column blank, and when no session on screen records an account the column is left out altogether.
A session that is waiting for something you can only do later can be set aside with agent-toggle-snooze, bound to z in agent-menu; invoking it again unsnoozes. A snoozed session keeps its key and its buffer, but the switcher lists it after every active session in the dimmed italic agent-snoozed face instead of a state color, agent-jump-to-waiting skips it, and it fires no ready alert when its turn ends. When snoozing, the command reads a duration in hours from the minibuffer; leaving it blank snoozes indefinitely. A timed snooze ends by itself when the hours run out, and if the session is waiting for input at that point, the ready alert that snoozing suppressed fires then. The command acts on the current session, and prompts for one when the current buffer is not a session or with a prefix argument. The mark is a property of the buffer, so it ends with the session and is not carried over by a restart or a handoff.
To see every session’s state without opening the switcher, enable the global minor mode agent-tab-bar-mode. It adds the sessions to the tab bar, each preceded by its switcher key in the agent-tab-bar-key face, in the switcher’s order and with its faces: agent-waiting, agent-waiting-with-background, agent-unknown and agent-snoozed, and no state face for a busy session. Clicking a session switches to it. The segment goes into tab-bar-format before tab-bar-format-align-right, which the mode adds when the format lacks it. Some states, such as background work, are read from files or the terminal rather than announced by an event, so the mode checks the sessions every agent-tab-bar-refresh-interval seconds (default 1) and redraws the tab bar when one changed. The tab bar itself must be shown, for example with tab-bar-mode.
Sessions can also carry a one-line annotation after their name, shown dimmed in the agent-session-annotation face. The text comes from agent-session-annotation-functions, an abnormal hook: each function is called with a session buffer and returns a string, or nil to leave the session to the functions after it. The first non-nil answer is the one shown, and the functions after it are not called. agent renders whatever it returns and knows nothing about where the text comes from. Whitespace in the answer is collapsed to single spaces, so a multi-line answer cannot break the menu’s layout, and an answer that is blank or not a string counts as no annotation. Sessions without an annotation — including sessions every function declines to annotate — render exactly as they did before annotations existed. The default is nil, which shows session names alone; Agent Log adds a function that supplies each session’s stored one-line summary (agent-log), and with no such integration loaded the switcher looks unchanged. Because this is a hook, an integration adds itself with add-hook rather than taking a slot over: a function you installed yourself keeps working whichever loads first, and remove-hook takes either one back out.
The switcher keeps the session summary and adds two columns after it: the
age of the current state and a short state description published by the agent.
Ages use seconds, minutes, hours, or days (12s, 2m, 1h, 3d), measured
from the last observed state transition. Repeated events reporting the same
state do not reset the age; neither do changes to either summary. An
unobserved transition has no age. Ages refresh every five seconds while the
menu is open.
The state description is explicitly authored by the working agent, for
example “Testing pagination fix” or “Waiting for your decision about bindings”.
It is not extracted from the chat and requires no separate summarizer.
Until an agent publishes an update, the column is blank. A prompt submitted
from Emacs marks the previous description stale rather than clearing it:
it stays in the column in the face agent-session-stale-status (italic by
default) until the agent publishes again. Most prompts start turns too short
for the agent to publish in, so clearing the description on each one left
most sessions blank. A changed native session identity clears it, since it
belongs to another conversation. A turn that starts without a prompt, such
as a reply to a background-task notification, keeps it current, as does a
completed turn. Publication does not alter the state color or
its age, and a claim of completion remains the agent’s report.
The executable bin/agent-status accepts a status string of at most 160
characters (an empty string clears it). It routes through CODEX_BUFFER_NAME
or CLAUDE_BUFFER_NAME, requiring --backend codex or
--backend claude-code if both are present. Codex requires CODEX_THREAD_ID to match the backend’s current native
identity, including before the first submission has cached that identity.
A registered identity resolver is authoritative even when it returns no id;
only backends without one use their recorded identity. Claude requires AGENT_SESSION_UUID to match its process token,
so an old process cannot update a replacement session with the same name. The helper
rejects absent or mismatched sessions, passes text as encoded JSON rather
than executable Lisp, and reports failures. When a briefly stalled Emacs
refuses the connection, the helper retries for about eight seconds before
reporting the failure. --socket-name (or
EMACS_SOCKET_NAME) selects a non-default Emacs server. Only the main agent
should publish status: subagents can inherit their parent’s buffer variable.
Instruct agents to publish a factual phrase of roughly 5–12 words at task start, meaningful phase changes, blockers, and before their final response. Installing this instruction affects sessions that read it; already running agents need to receive the instruction before they will publish updates.
The public function agent-session-set-status accepts a buffer name, backend
symbol, status text, optional native session id, and optional process token. The hook agent-session-status-token-functions supplies backend process tokens.
The function validates the target and text, stores the update in that buffer, and refreshes an open switcher.
agent-session-state-summary-functions supplies the column using the same
cheap, first-non-nil convention as the session-summary hook. The built-in
provider reads the published status without transcript I/O or model calls.
The public function agent-session-state-summary returns a buffer’s
current description as a single line, for display elsewhere such as the
mode line; publishing a status or marking it stale updates the mode line.
Names, accounts, and annotations each line up in one column: names are padded to the widest name in the menu and accounts to the widest account, measured in display columns, so a name containing double-width characters does not push the columns after it to the right. Padding that nothing follows is trimmed, so a session with no annotation ends at its account, and a session with neither ends at its name.
The session summary and state-description columns are aligned across rows and
truncated with an ellipsis to fit the frame. After reserving the name,
account, graphical icon width, age, separators, and menu margins, each text
column can use half
the remaining width; space unused by a short update is available to the
session summary, and space unused by a short summary is available to the
update. agent-session-annotation-max-width caps only the session summary.
A zero cap hides that summary without hiding the age or state description.
When neither new column has data, the original annotation layout is used.
A function added to agent-session-annotation-functions must be cheap and free of side effects: building the switcher calls it more than once per session, because aligning the annotations needs every label’s width before any label is final. Reading a value from memory is appropriate; reading a file or contacting a process is not.
The command agent-run-skill discovers skills from every loaded backend that implements skill discovery. It disambiguates same-named skills by backend label, prompts for declared arguments when a skill frontmatter requests them, and dispatches the invocation to the selected backend. Claude and Codex backends scan global and project-local programmatic skill directories, using both project-current and nearby tool directories as project-root signals, so batch-only skills can be run from this command without being loaded into ordinary interactive agent sessions.
The command agent-capture-prompt opens a session-specific Org file and appends a new prompt entry. The command agent-insert-captured-prompt reads pending nonempty entries for the current session, prompts for one with a short body preview in the completion candidate, inserts its text into the backend prompt without submitting it, and removes the selected Org entry from the capture file. With a prefix argument, it includes legacy entries already marked as inserted. When a session still has pending captured prompts, agent-exit, agent-exit-without-skills, agent-restart, and agent-handoff ask for confirmation before closing or replacing that session.
The command agent-act-on-slack-message, called on a message in a Slack room
buffer, starts a session in the project that message is about and inserts its
permalink into the prompt without submitting it. With a prefix argument it
inserts the permalink into a running session instead. Its extractor,
agent-slack-context, answers with the message text and no directory, since a
message names no project of its own: the project is read from the account’s
sources and ranked by that text (Project sources).
A message that contains a Claude Code resume command, claude --resume followed
by a session id, is handled differently: it names a session, not a task. Such a
message is typically an alert reply posted for a background session that has
stopped to ask for a decision. It is a thing at point of its own, checked before
an ordinary Slack message, so agent-act-on-thing-at-point asks for no backend
and no project: the command switches to that session’s buffer when one is live,
and otherwise resumes the session in a new buffer in the directory its
transcript records, so the conversation continues where it stopped. The prefix
argument makes no difference here. A resume command whose transcript cannot be
found signals an error rather than starting a new session.
The command agent-act-on-forge-notification is the Forge counterpart of
agent-act-on-slack-message. Called on a notification in
forge-list-notifications, it starts a session in the working tree of the
repository that notification belongs to and inserts the issue or pull request
URL into the prompt without submitting it. It also accepts the topic at point,
so it works from a topic buffer or a Magit status buffer, and with a prefix
argument it sends the URL to a running session instead of starting one. Not
every notification is about a topic: Github also notifies about check suites,
commits and releases, and Forge records no topic for those. Such a notification
is routed as itself, because it names a repository to work in and a URL to
read; its title goes into the prompt with the URL, since the URL of a failed
workflow run names the page listing every run of the repository and the title
is what says which one this is. The
command is a thin wrapper over the routing layer described below: its
extractor, agent-forge-context, reads the subject at point and answers with a
context suggesting the working tree, so the project reader opens on that clone
already selected: a bare RET starts there, and the same prompt is where a
different project is chosen instead. Routing a topic therefore ends in the same
choice as routing a Slack message or a backtrace, rather than starting a session
outright.
A Slack message has to be classified to find its project, whereas a Forge
notification already records its repository. Where that repository is cloned is
a second question, and it is asked in three steps. Forge answers first, from the
worktree it recorded; it records one only for a repository it has seen from a
local clone, and it forgets one whose path stopped being a worktree, which is
what moving a repository does to every path at once. The account’s project
sources answer next, with agent-project-repository-directory: they are where
the clones live, so they know the current path of a repository that moved and
the path of one Forge never saw. When neither knows, the context carries no
directory and the core reads a project from its text, which leads with the
repository’s slug so that ranking puts the right project first. Nothing is
refused for want of a clone.
The command agent-act-on-email, called on a message in a mu4e headers or
view buffer, starts a session in the project that message is about and inserts
the message’s maildir path into the prompt without submitting it. With a
prefix argument it sends the path to a running session instead. The payload is
the path rather than the text of the mail because an agent can open that file
itself and read the headers, the body and any attachments from it, which no
excerpt pasted into a prompt would carry. Like Slack routing and unlike Forge
routing, the context names no directory: a message belongs to no project of its
own, so its extractor, agent-mu4e-context, answers with the subject and the
sender as the text the project is ranked against (Project sources).
The sender comes from the message’s :from field, which mu returns as a
list of (:email EMAIL :name NAME) plists — one per sender, with the name
absent when the sender gave none — and reads as Name <address>, or as the
bare address when there is no name. Like its Slack and Forge counterparts, the
command loads its foreign package itself and says so when it is missing, since
the command is autoloaded and can therefore be called from a buffer that has
never loaded mu4e.
It signals an error when the message records no path — mu4e reports a missing string field as the empty string rather than as nil, so an empty path counts as no path — and when the recorded file cannot be read. A maildir file name encodes the message’s flags, so reading a message, or another mail program touching it, renames the file, and mu’s index can name a path that no longer exists until it is reindexed. As with a stale Forge clone path, that is an ordinary failure, and catching it here is better than handing the agent a path it cannot open.
The menu binds no routing command directly. agent-act-on-thing-at-point, bound to t . in the Tools column, walks agent-at-point-things and runs the extractor of the first entry whose predicate matches the current buffer: a backtrace in a *Backtrace* buffer, a Slack message in a Slack room buffer, a Forge notification or topic, an email in a mu4e headers or view buffer, or an org TODO heading. Every predicate answers from the buffer alone — the buffer name, the buffer-local variable slack sets on room buffers, the major mode, the org TODO state — so dispatch never loads a module to ask whether it applies, and the Forge and mu4e lookups run only in a buffer whose major mode can hold what they look for. A Magit buffer is one of those, and it can be current with forge not yet loaded; since forge’s lookups are not autoloaded, the Forge predicate also checks that they are defined, and falls through to the next thing rather than failing when they are not. The predicate claims any notification, not only one about a topic, because the extractor routes the notification itself when Forge records no topic for it. The mu4e modes need no such check, because the files that define those modes require the one that defines the lookup. When nothing matches, the command signals an error naming those five kinds, in the order the table holds them.
Recognizing the thing, choosing the session, and delivering the payload are three separate layers. An extractor takes a continuation rather than returning a value, because extraction can require a network round-trip, and calls it with a context plist: :directory when the thing carries a working directory, as an absolute path, :suggested-directory when it worked one out and the user should still be asked, :text when it carries only text to choose a project from, :payload for what to send, :submit for whether to submit it, and an optional :after thunk run once the payload is delivered. Before extraction begins, the core resolves the backend and captures that backend’s selected account as one pair. A later account switch cannot change which project sources this invocation reads, which matters for asynchronous extractors such as Slack. A context that names a directory starts its session there; one that suggests a directory, or names none at all, has its project read from the captured account’s sources with agent-project-read, led by the suggestion or ranked by the text. With a prefix argument the payload goes to a running session chosen by completion instead of a new one, so the same key routes a thing either way.
A running session already has a directory, so on that path the core tells the extractor not to resolve one, and an extractor that has to work for its directory leaves it out. That saves what the work costs: routing a backtrace to a running session asks no model which packages it implicates and prompts for none, and routing a Forge topic there no longer refuses a repository that was never cloned locally. The extractors that carry only text are unaffected, since the project they would otherwise pick is chosen after the session is, and only for a new one.
What each kind sends differs, because the payloads differ. A Slack permalink, a Forge topic URL and an email’s maildir path are references to something the agent should read, so they arrive unsubmitted and you type the request next to them. A backtrace and an org TODO already are the request, so they submit.
A payload marked :submit reaches a new session as its initial prompt, handed to the backend at launch rather than typed into the terminal afterwards. Claude passes it on the CLI’s command line, and Codex does too except when it is resuming a session or running through its app server, where it sends the prompt itself once the session is up. Either way the payload never races the terminal: nothing sequences typed input against a session whose process has only just been spawned, and these sessions often start in a directory the CLI has never opened, where it greets the user with a trust prompt that the typed text would answer instead of the model. A payload the context does not submit is typed in on both paths, since it is meant to sit in the prompt for the user to edit. A submitted payload reaching an already-running session is typed in and then submitted, because that session is past its startup and needs no hand-off at launch.
Because extraction can be asynchronous, the continuation may run long after the command was invoked, in whatever buffer the user has moved to since. The dispatcher therefore captures the invocation buffer and resolves the backend up front, before the extractor runs: the session adopts the backend of the buffer the thing was read from rather than of wherever the callback lands, no backend prompt appears in the middle of an asynchronous wait, and the :after thunk runs back in the invocation buffer, which starting a session has displaced as the current one. A thunk whose buffer has been killed meanwhile is dropped, since the thing it would act on is gone.
The commands agent-handoff, agent-debug-backtrace, agent-act-on-slack-message, agent-exit, and agent-restart resolve a backend from the current session buffer or by prompting. This lets the same keybinding operate on Claude or Codex sessions without separate wrapper commands. The old per-backend command names (agent-claude-handoff, agent-codex-restart, and so on) remain available as obsolete aliases. agent-exit, bound to x, kills the session buffer after any configured before-exit skills finish. agent-exit-without-skills, bound to X, uses the same exit path but inhibits the configured before-exit skills. It still checks for pending captured prompts and runs other before-exit functions. Neither command deletes the Emacs window that displayed the session.
agent-handoff reads the backend’s entry in agent-handoff-files, kills the current session buffer, and starts a new session with the handoff prompt. By default the new session keeps the source directory. A handoff file can select another directory with YAML front matter:
---
target-directory: ../other-project
---
Continue from this handoff.
The target can be absolute or relative to the source session directory, and it must name an existing directory. agent-handoff resolves and validates it before it kills the source session. Lisp callers can pass the optional TARGET-DIRECTORY argument instead; an explicit argument takes precedence over file metadata, and file metadata takes precedence over the source directory.
agent-handoff-from-emacsclient accepts the source buffer name as its first emacsclient argument and the optional target directory as its second argument. This lets agent-authored handoff commands close the requesting session even though Emacs evaluates the form outside that buffer. If the source buffer name is unavailable, handoff closes the single existing session buffer for the source directory before starting the replacement session, which avoids the CLI’s instance-name prompt in unattended loops.
agent-restart kills the current session buffer and resumes the same session in place, preserving the session’s directory, instance name, and any launch state the backend’s :restart-options function captures. It reroutes sessions within the selected account pool without prompting. Otherwise it prompts when the active account differs from the session account, and it leaves the account unset when neither the session nor the global selection names one, so a session that ran under the backend’s ambient default home is restarted there rather than pushed onto an account it never used. Every precondition is checked before the old session is killed: restart refuses when a named account is no longer configured, when the account is logged out, or when no resumable session identity exists, and in each case the running session survives untouched. agent-audit-project prompts for a project directory and runs each skill in agent-audit-skills sequentially through the backend’s non-interactive prompt runner. agent-debug-backtrace saves the current Emacs backtrace, asks gptel to identify likely packages, and submits instructions to fix the bug to a session started in the selected package source directory, or to a running session under a prefix argument. Its extractor, agent--backtrace-context, schedules the identification to run after the current command rather than doing it inline, because saving the backtrace kills the *Backtrace* buffer, which exits the debugger’s recursive edit and unwinds the calling frame. agent-setup-kill-on-exit arranges for a session buffer to be killed when its process exits, consulting the backend’s :before-kill-check slot first. A process that exits with a non-zero code keeps its buffer, with a message, because the buffer holds the error that explains the failure — a CLI that cannot start would otherwise vanish without a trace; a process ended by a signal is killed as usual. It is not a menu entry: each backend installs it, along with agent-disable-scrollback-truncation and, for Codex, agent-fix-rendering, from its session-start hook, so the menu carries no buffer-maintenance commands.
agent-restore-sessions, bound to O, resumes the sessions that were live when the last Emacs died, whether it quit, crashed, or went down with the machine. Each Emacs records its live sessions in agent-session-registry-file whenever a session starts, learns its native id, or ends, so the record survives deaths that skip Emacs’s exit hooks; batch Emacs records nothing. An Emacs counts as dead when no process has its pid and start time, because a reboot reuses pids. The first save after a restart turns the sessions of every dead Emacs into the restorable set, replacing the previous one unless the dead Emacs had no sessions. The command asks once before resuming each session with its backend, account, directory, instance, session id, and the launch state its backend’s :restart-options captured. It skips sessions already live and keeps failed ones recorded so it can be run again. A session is recorded only once its native id is known, so one killed before its first status update cannot be restored.
agent-before-exit-functions can delay agent-exit for cleanup work.
Each function receives the resolved backend and session buffer; returning nil
aborts the exit.
When a turn is active, Agent waits for it to finish before it submits the first
before-exit skill, so the skill cannot steer or abort the user’s turn. A
backend that reports both a stop and a later idle-prompt notification for one
turn advances the chain only once.
Once a forced session close proceeds, Agent kills the target buffer without
running buffer-local kill-query prompts, so stale session buffers do not survive
restart or exit workflows.
Set agent-before-exit-skill-name (a single skill) or
agent-before-exit-skill-names (an ordered list) to submit interactive skills
before exiting matching sessions. Each entry in agent-before-exit-skill-names
is either a skill-name string or a list whose car is the skill name and whose
cdr is a plist accepting :directories (restrict where that skill runs,
overriding agent-before-exit-skill-directories) and :args (a string
appended to the submitted command). Set :receipt non-nil when the skill must
confirm its closeout result before the chain can continue. For example:
(setq agent-before-exit-skill-names
'("session-learning-capture"
("update-log" :directories ("~/work")
:args "--auto" :receipt t)))
agent-run-skill-before-exit uses each backend’s registered skill command
prefix so Claude sessions receive slash commands and Codex sessions receive
dollar-prefixed skill commands, then submits the first applicable skill. The
skills run in order, each submitted after the previous one finishes, and the
session exits after the last. These submissions do not display the session, so
an exit chain continues in the background after the user switches away;
ordinary ready notifications are left unchanged while the chain runs. When
agent-before-exit-skill-names is nil, agent-before-exit-skill-name is used
as a single-skill fallback. Leave agent-before-exit-skill-directories nil to
run an unscoped skill before exiting every session.
For a :receipt entry, Agent creates a private temporary directory and appends
--receipt-file PATH to the submitted command. The skill must write one JSON
object to that path. Its status must be exactly success, no-op, or
failure; a failure receipt can include a message. Agent advances the chain
or exits only after success or no-op. A missing file, invalid JSON, unknown
status, or failure stops the chain, keeps the session open, and shows a
warning; a missing file is ignored while the backend’s idle probe still shows
the session working, since that completion event belongs to an earlier turn. Agent removes the temporary receipt directory after reading it or
tearing down the session. Receipt gating is per entry: plain entries keep the
stop-event behavior. This lets a learning-capture entry run independently
before a receipt-gated bookkeeping entry. Repeating agent-exit while a chain
is active does not bypass it; it reports the chain’s state in the echo area,
and agent-exit-without-skills is the explicit close that skips configured
skills. When agent-exit is called on a session that is still responding, the
chain arms in a waiting state and starts its first skill on the next stop
event; a message says so at arming time, since nothing else is visible until
the turn ends.
agent-exit runs every applicable configured skill regardless of session
duration. agent-exit-without-skills is the only exit command that bypasses
the skill hook; it still runs other before-exit functions.
The chain is driven by session stop events and guarded by a watchdog. Each
submitted skill gets up to agent-before-exit-timeout seconds (default 600) to
finish; the watchdog restarts when the next skill begins. If the current skill
exceeds that interval, the watchdog warns but keeps the exit request armed. The
chain checks the result when the skill eventually finishes instead of losing
the requested exit because the skill ran for more than ten minutes. A gated
entry still needs an accepted receipt at that point.
Backends can veto that automatic close while the submitted skill invocation is
still visible as unaccepted prompt input. Codex uses this to avoid closing a
session when the dollar-prefixed skill name was inserted but not submitted,
while ignoring submitted command scrollback, visible placeholder text, and
autosuggestion text. The readiness check recognises the standard Codex prompt
markers (› and ❯); a chain that stalls after the first skill usually means
the running Codex renders an unrecognised prompt marker.
A chain stays armed across stop events until its queue drains, so a chain
whose command was typed but never accepted would otherwise fire its next skill
into whatever the user does in that buffer later. A prompt the user sends from
the terminal therefore cancels any armed chain in that buffer, with a message.
Claude reports such prompts as a user-submit session event from its Return
key paths; Codex’s submission hook runs for programmatic sends too, so it
reports plain submit events and cannot cancel a chain this way.
Backends can provide an atomic submit operation for before-exit commands.
Claude and Codex both target the resolved session buffer directly, insert the
skill command, and submit it before the terminal buffer is displayed again.
Codex delegates this to codex.el’s target-buffer submit helper. Claude
inserts the command, waits agent-claude-submit-delay seconds with
sleep-for (pending keyboard input cannot shorten it, as it would a
sit-for, and a Return that arrives too soon is folded into the inserted text
as a newline), and sends Return. It then checks the prompt line and re-sends
Return up to agent-claude-submit-retries times while the command is still
sitting there unsubmitted, since Claude Code occasionally swallows the first
one; the prompt marker’s trailing space is a no-break space under eat.
Phone remote control
The global minor mode agent-remote-mode, in agent-remote.el, serves a touch-friendly control page so a phone can watch and unblock sessions. The page lists every live session with its state, backend, account and time in that state, waiting sessions first. Tapping a session shows the last agent-remote-output-lines lines of its buffer, refreshed every 1.5 seconds. A text box submits a reply through agent-submit, and a row of keys sends single keystrokes to the session’s terminal: escape, digits, arrows, return, tab, shift-tab, y, n and C-c. An escape sent to a Claude session goes through claude-code-send-escape, so an interrupted turn is detected as it would be from Emacs. Because the page drives the session buffers themselves, it reaches Claude and Codex sessions under every account. Keys need a terminal session; a session without an EAT terminal accepts only text.
The session view’s Restart button restarts the session under another account, for a session that has hit its usage limit while you are away. It lists the backend’s pools and accounts with each account’s latest weekly and session usage, marking accounts at their limit and disabling logged-out ones. Choosing one calls agent-restart-with-account, which does what selecting the account with agent-account-select and then choosing it at the agent-restart prompt does: the choice becomes the selection for new sessions, a pool is routed to one of its members, and the session resumes its conversation under the new account. It never prompts in Emacs; it refuses a session with captured prompts, which the account change would orphan.
The server listens on agent-remote-listen-address and agent-remote-port (default 8787). The listen address defaults to every IPv4 interface, because the macOS Tailscale app does not deliver connections to a socket bound to the Tailscale address alone. Instead, the server closes without a response every connection whose peer is outside agent-remote-allowed-networks, which defaults to 100.64.0.0/10, the range Tailscale assigns to tailnet devices; devices on the local network cannot use it. agent-remote-host is the address the phone uses. When nil, it is this machine’s Tailscale IPv4 address, obtained from agent-remote-tailscale-program when the first request arrives, so the mode can be enabled while Tailscale is disconnected; until the address is known, every request is refused as misdirected. Two checks keep web pages in a browser on the same machine from using the server: every request must name agent-remote-host and the port in its Host header, which defeats DNS rebinding, and every request that sends input must carry an X-Agent-Remote header, which a cross-origin page cannot add without a preflight the server never approves. There is no further authentication, so do not add networks that untrusted devices can connect from.
The page is etc/agent-remote.html, served from the source checkout. The android/ directory holds a single-purpose Android app, “Agents”, for phones without a browser. It shows only this page and cancels any navigation to another origin. android/build.sh install builds it with the Android command-line tools, without Gradle, and installs it over adb; AGENT_REMOTE_URL sets the page address it is built for.
Branch navigation
A branch is a session forked from another one: it starts with the parent’s whole conversation and then diverges. Branching lets you try a different approach without losing the state you built up, and the three commands here work the same way whichever backend the session belongs to.
agent-create-branch, bound to b n, forks the session in the current buffer and opens the fork in its own buffer under an instance name derived from the time of the fork. By default the branch shares the parent’s working tree, which is what starting a second session in the same project would give you. With a prefix argument it also creates a git worktree on a fresh branch under agent-branch-worktree-directory and runs the fork there, so that destructive git operations in one branch cannot disturb the other. The worktree starts at the parent’s HEAD, so uncommitted parent changes are not carried into it; use the isolated form when concurrent git work across branches is a real concern, and the default otherwise. Before the fork starts, the backend’s :prepare-fork slot makes the parent session visible from the new directory, which is what a backend that stores its transcripts per project directory needs in order to resume a session recorded elsewhere.
agent-switch-branch, bound to b s, shows every session sharing a fork ancestor with the current one as an indented tree, each entry carrying its first prompt and its timestamp and the session you are in marked with an asterisk, and switches to the selected session: to its live buffer when it still has one, and otherwise by resuming it in a new buffer. It reports that there is nothing to navigate when the current session has no relatives.
agent-resume, bound to R, resumes a past session of the current backend, or of a backend it asks for when no session buffer names one, and passes its prefix argument through to the backend, where it means the most recent session.
Both backends supply the lineage these commands read, through the :session-headers slot, and each derives it from what its CLI already records. Claude Code writes one transcript per session inside a per-project directory, so the scan reads the first line of each transcript in the project the current session belongs to. That line is read whole however long it is, since a session started with a queued prompt records the entire prompt there. Codex writes one rollout file per session, all under a single sessions directory, so the scan reads the session_meta record opening each rollout, keeps the ones whose recorded working directory matches, and takes the parent from forked_from_id. Because that directory holds every Codex session on the machine, the scan is bounded by a start timestamp when the caller only needs one session’s descendants.
Codex records subagent threads with the same forked_from_id field, and those are deliberately left out of the tree. They are forks as far as the file format is concerned, but they are not branches of your conversation: they are threads the model spawned to do work on its own, and offering to switch into one would be offering something a user never chose to create.
Org TODO commands
The agent-todo module turns Org TODO entries into work for an AI session, and both of its commands work with either backend.
agent-send-todo-at-point formats the heading and body of the TODO at point as a prompt and submits it to a session started for it; it is not a menu entry of its own, since the menu reaches the same routing through agent-act-on-thing-at-point. With a prefix argument it submits the prompt to a running session chosen by completion. Like a Slack message, a TODO names no directory, so the project is read from the account’s sources and ranked by the TODO’s own text (Project sources). It no longer looks for the session belonging to the current file’s project: an Org file of tasks rarely sits in the repository those tasks are about. When agent-todo-in-progress-keyword is set, the heading moves to that keyword once the prompt has been delivered. The heading is remembered as a marker, so the move lands on the heading the prompt came from however far point has travelled while the project was being chosen. A heading deleted in the meantime is left alone: the state change happens after the session exists but before it is shown, so an error there would leave a running session unshown.
agent-batch-todos, bound to p b, collects several TODO entries and runs them one after another without an interactive session. It infers its scope by itself — the region when one is active, the subtree when the buffer is narrowed, and the whole buffer otherwise — then asks for a working directory, and confirms the number of entries before starting unless you selected them as a region, where the selection is confirmation enough. Each entry runs through the resolved backend’s :exec-prompt slot, its raw output is written to a numbered file under a timestamped directory in agent-todo-log-directory, and a summary buffer collects every entry’s status, duration, cost, and output when the queue is empty.
Cost appears per entry only for backends whose CLI reports it. Claude’s non-interactive runs report a dollar figure and it is shown; codex exec reports none, so Codex entries render an em dash in place of a cost and contribute nothing to the total.
Chief-of-staff commands
The command agent-chief-start-session starts or reuses a dedicated Claude Code or Codex session using agent-chief-session-instance-name, sends the chief-of-staff operating contract into the session, asks the model to request today’s plan there, and starts the heartbeat timer. The command agent-chief-switch-to-session jumps back to that session so the user can reply normally.
The command agent-chief-start starts only the repeating Emacs timer. Without a prefix argument it also schedules an immediate tick; with a prefix argument, it waits agent-chief-interval seconds before the first tick. agent-chief-stop cancels the timer. agent-chief-tick runs one check using agent-chief-mode. agent-chief-session-heartbeat forces a session-mode heartbeat, while agent-chief-stateless-tick forces one stateless JSON-decision check.
The command agent-chief-open-state opens the auditable Org state file. agent-chief-add-note appends a manual note, and agent-chief-set-day-plan appends today’s plan. When a chief session is active, both commands also submit the new context into that session. These commands are intentionally simple: the durable source of truth remains ordinary text that can be reviewed, edited, and audited outside the model.
Claude Code commands
The Claude commands expose workflows that are specific to Claude Code; handoff, restart, exit, resume, branch navigation, skill execution, backtrace debugging, TODO workflows, and Slack-message routing use the unified agent-* commands described above. agent-claude-start-or-switch starts Claude under the selected account or opens the unified switcher. agent-claude-select-account persists an account choice, and agent-claude-init-account prepares a new account directory with shared configuration files and symlinks.
The branch and TODO commands that used to be Claude’s own are now the shared agent-create-branch, agent-switch-branch, agent-batch-todos, and agent-send-todo-at-point; agent-claude-create-branch, agent-claude-switch-branch, agent-claude-batch-todos, and agent-claude-send-todo-at-point remain as obsolete aliases.
agent-claude-start-status-polling and agent-claude-stop-status-polling start and stop status polling for the current Claude buffer by hand. Polling is started and stopped for you with the session, so these are maintenance commands rather than part of a normal workflow; they are available through M-x and are not in agent-menu.
The setup command agent-claude-setup-config ensures that Claude’s statusline, Stop hook, Notification hook, and turn-lifecycle hooks are present in ~/.claude/settings.json, for sessions started outside Emacs; sessions this package starts receive them through --settings without it. agent-claude-remove-global-config removes them again. agent-claude-ensure-state-hook-config installs only the turn-lifecycle hooks listed in agent-claude--state-hook-events. The backend does not run setup automatically when loaded; run the setup command explicitly when installing or repairing hook configuration.
Codex commands
The Codex commands expose workflows that are specific to the Codex CLI; handoff, restart, exit, skill execution, backtrace debugging, and Slack-message routing use the unified agent-* commands described above. agent-codex-start-or-switch starts Codex under the selected account when no Codex sessions exist, otherwise it opens the shared switcher. agent-codex-select-account persists an account choice by name and applies it to new interactive sessions and non-interactive codex exec runs. Skills run non-interactively with codex exec, using local skill metadata when available and falling back to a slash invocation otherwise; additional programmatic skill directories are configured through agent-codex-programmatic-skill-directories.
When agent-restart operates on a Codex session, it resumes codex.el’s canonical session identity under the chosen account and current default codex-terminal-backend; app-server sessions use the native app-server resume path and terminal backends use codex resume. Changing codex-terminal-backend and then restarting a Codex session switches the relaunched session to that backend. The unified commands cover both Codex resuming and Codex forking: agent-resume resumes a past session of whichever backend the context names, and agent-create-branch forks the current session of either backend, reaching Codex through codex fork on the terminal backends and thread/fork on the app-server backend. agent-codex-resume and agent-codex-fork still expose the CLI’s own subcommands and remain available through M-x, though neither is in agent-menu any more. agent-codex-fork is not redundant: it forks a session you pick from Codex’s own history, whereas agent-create-branch forks the session you are sitting in.
Functions
Backend registration and display
The function agent-register-backend registers an AI tool by constructing an agent-backend struct from keyword arguments. Backend modules call it at load time. Keywords are validated against the struct’s slot names, and the required slots :buffer-p, :find-all-buffers, and :start-session are checked so the shared session layer can rely on a consistent interface.
The full slot set is: identity and presentation (name, label, icon, program), buffer discovery (buffer-p, find-all-buffers, find-buffers-for-dir), session lifecycle (start-session, session-identity, restart-options), send primitives (send-string, send-return, submit), session state probes (waiting-p, busy-p, background-tasks-p, status-unknown-p, display-name-suffix, notify), accounts (account-env-var, accounts, account-file, shared-config-items, canonical-home, account-init, credential-file, login-args, usage-fetch), skills (run-prompt, exec-prompt, skill-roots, skill-command-prefix), session history (session-headers, session-prompt, resume, prepare-fork), integration hooks (sync-theme), and exit safety (before-exit-ready-to-close-p, before-kill-check, idle-p).
The :start-session slot starts a new session parameterized by an agent-session identity struct, optionally with an initial prompt or a resume id. The :session-identity slot returns a live session buffer’s CLI session id, used by agent-restart to resume in place. agent-start-session is the core dispatcher that starts a session through its backend’s :start-session function, passing any extra backend-specific options through. The :send-string slot inserts text into a session’s prompt without submitting it, :send-return submits the pending prompt, and :submit atomically inserts and submits; the core wrappers agent-send-string, agent-send-return, and agent-submit dispatch through these slots and emit the corresponding session events.
The slots that carry the unified session commands are all optional, and a backend that omits one simply does not support the commands dispatching on it. :resume resumes a past session, receiving the command’s prefix argument, and backs agent-resume. :session-headers returns a hash table mapping session id to a plist of :session-id, :forked-from and :timestamp, called with a session buffer and optionally with the one session whose descendants the caller cares about, so a backend that keeps every session in one directory can bound its scan; :session-prompt enriches one such header with the session’s :first-prompt. Together they back agent-switch-branch and the branch check before a kill. :prepare-fork makes a session recorded under one directory visible from another, which is what lets an isolated branch run in a fresh worktree. :exec-prompt runs a prompt non-interactively and calls back with a plist of :exit-code, :duration, :text and :raw; agent-batch-todos runs through it.
agent-display-name returns a human-readable label for a session buffer. It starts with the project name, adds an instance name when needed to disambiguate duplicates, and lets backends append their own suffixes. The Claude backend uses this to expose branch identity in the modeline and switcher.
agent-parse-skill-frontmatter reads SKILL.md frontmatter shared by Claude and Codex skill runners. It recognizes names, descriptions, argument hints, argument sources, argument choices, defaults, multiplicity, user-invocable flags, and model overrides.
agent-svg-icon renders SVG strings as inline images, using the current face foreground when the SVG uses currentColor. Backends use this for modeline and transient display and can fall back to text abbreviations when SVG rendering is unavailable.
Modeline and status helpers
doom-modeline is optional. When it is loaded, whether before or after agent, agent-doom-modeline.el defines the ai-session modeline: the backend icon, the session name, the model, the reasoning effort, the account, and the status the session’s agent last published, each separated by a bar. A field with no value is left out. agent-doom-modeline-session-status returns that text for the current buffer, for use in another modeline. Without doom-modeline, the file is never loaded and session buffers keep the standard mode line.
The Claude and Codex status helpers are intended for modeline integrations such as doom-modeline. Session buffers use doom-modeline’s ai-session modeline when one is defined. Turning doom-modeline-mode on sets the main modeline in every buffer, and once the mode has been toggled interactively, every theme load turns it on again; both backends therefore set ai-session again in each session buffer whenever the mode is turned on. Claude status functions read the current session status JSON and expose model, reasoning effort, cost, context, duration, and token information. Claude usage functions expose account-level utilization returned by the Anthropic usage endpoint. Codex status functions expose the current session model, reasoning effort, and elapsed session duration; the model helper falls back to the configured model when the session has no live model value. When Codex has no explicit reasoning-effort override, the status helper reports Codex’s default medium effort. The session switcher uses backend busy, input-readiness, and background-work detectors to avoid showing actively responding sessions as waiting, and to distinguish truly idle waiting sessions from sessions that are accepting input while background terminals, agents, or tasks are still running. For Claude sessions the display comes from Claude Code’s own report of each session, read with claude agents --json, the supported way to read session state from outside a session. It lists the live interactive sessions of one config directory with a status of busy, waiting or idle, and busy includes background work left running after a turn ends. Idle or waiting shows the session waiting (:waiting-p), and busy once the turn has ended shows background work (:background-tasks-p); hooks still mark when a turn starts and ends. A query (agent-claude-agents-command) runs in the background, once per config directory. Its result is stored from a timer once the process exits, never inside the process sentinel, and a further query for the same directory waits until that result is stored. A query whose process cannot start counts as a failed query. Claude Code takes a moment to register a change: a query started the instant a prompt is sent still reads idle. So a session event, which marks a turn boundary such as a prompt sent from Emacs or a stop (agent-session-event-functions runs for every session event, including one that changes no state), makes every earlier result stale for that session and schedules a query agent-claude-status-settle-time seconds later. A result counts as current for a session only when its query began at least that long after the session’s latest session event; until then the session-event state machine decides. A prompt sent from Emacs has to be handled as a session event because no hook reaches Emacs for it: the state hook drops activity events while the state file already reads busy. Other hook traffic, such as a subagent’s tool calls, marks no turn boundary: it refreshes the result, no more often than agent-claude-agents-status-min-interval seconds, without making it stale, since a teammate working under an idle lead would otherwise keep every result stale for as long as it worked. A display also queries again when the result is older than agent-claude-agents-status-max-age seconds. A session is shown as unknown (:status-unknown-p), never guessed, while no result exists for its config directory, when the latest query failed (which also raises a warning), or when a current result does not list it. After each query, every session whose display differs from what the hooks alone would show is logged to *Messages* once per change, so the two sources stay comparable. What the hooks alone show, which decided the display until October 2026 and is kept for that comparison, is built from Claude Code’s own list of the session’s background tasks, which its Stop and SubagentStop hook payloads carry as background_tasks, with each shell, Monitor, background agent, and teammate and its status, including those started by subagents. The bundled record-background-tasks.sh hook, installed by agent-claude-ensure-background-hook-config (part of agent-claude-setup-config), writes each payload to a file in agent-claude-status-directory keyed like the statusline file, and agent-claude--background-tasks counts the tasks it reports as running. A task that ends while the session waits wakes Claude with a task notification, so the turn that follows ends with a fresh list; a task killed without such a turn keeps counting until the next one. A payload without the field raises a warning instead of passing as no tasks. Agent-team teammates are the exception: the list reports a teammate as running until it is shut down, including while it sits idle waiting for a message. The same hook, also installed on SubagentStart and TeammateIdle, therefore keeps one file per working subagent or teammate in a .agents directory keyed like the statusline file: SubagentStart, which fires each time a teammate takes a message, creates it, and SubagentStop or TeammateIdle removes it. Teammate tasks count only while that directory has an entry. The list does not say which teammate a task is, so while any one works, every teammate task counts. A Monitor that Claude Code arms by itself when the session publishes an artifact, to watch it for comments (described as live updates for artifact … (auto-armed on publish) and carrying no command), does not count either: it runs until the session ends, so it would show every finished session that published an artifact as busy. A claude -p run from a session’s shell inherits the session’s environment, so its hooks would report as the session; its Stop would mark the session waiting in the middle of a turn. agent-claude--record-session-id therefore writes the session id the status line reports to a .sid file keyed like the statusline file, and the bundled hook scripts (through session-owner.sh) and agent-claude--foreign-hook-p drop a payload naming another session. A payload without a session id, or one arriving before the status line has reported the id, is accepted. The Remote Control task UI also counts. The footer’s task counts are not used, because its ← N agents indicator counts agents across every session on the machine. A session counts as waiting only when it is blocked on the user, meaning it has stopped and will not proceed until the user submits something. Being able to queue text mid-turn is not waiting, since both CLIs accept typed input at any point. Codex app-server sessions learn turn boundaries from the JSON-RPC stream, so an inactive turn on a live app server is authoritative for waiting and an active turn is authoritative for busy, regardless of whether the prompt accepts input. Terminal sessions have no such signal and fall back to the session-event state machine and to scanning rendered status text. For Claude, agent-claude--terminal-waiting-p reports idle when the rendered tail shows the input prompt and no running-turn spinner line (a glyph, a verb ending in an ellipsis, and a parenthesised elapsed time, e.g. ✢ Billowing… (4m 35s); the older esc to interrupt hint and the background-task esc to give additional instructions prompt also count as running). It is registered as the backend’s :idle-p — not :before-exit-ready-to-close-p, whose veto runs on every chain step, where the screen’s redraw lag behind the Stop hook would strand the chain — and consulted only by the before-exit chain and the escape interrupt check described in Session state machine: when arming, a busy session whose terminal affirms idleness arms immediately, and the watchdog advances a chain stranded in waiting-for-idle whose stop event never arrived — a turn the CLI accepts and then drops, for instance at the context limit, fires no Stop hook. Screen scraping is not reliable enough to drive the switcher on its own, so display state stays with the session-event state machine; the interrupt check consults the screen only right after the user sends an escape. A busy state younger than agent-claude-terminal-idle-grace seconds is trusted over the screen, since the spinner takes a moment to appear after a submission. Codex prompt submission clears the previous waiting marker at the target buffer, so a session does not remain colored as waiting after the user sends a new prompt.
Transient menu
agent-menu is a single static prefix defined in agent.el, laid out in four columns: Sessions with Account beneath it, Current session, Tools with Prompts beneath it, and Options. Nothing in the layout depends on which backends are loaded, because every command in it is backend-agnostic. Each one resolves a backend from the current session buffer, or asks when nothing names one, and then dispatches through that backend’s slots, so the same key operates on a Claude session and a Codex session alike. There is no longer any mechanism for a backend module to add entries to the menu, and this is deliberate: a backend contributes an implementation for a slot, and the command that calls that slot lives in agent.el.
In the Options group, -c selects an account. It acts on the current or prompted backend, while its value shows every registered backend’s current account at once, so one entry replaces the per-backend account selectors the menu used to carry. The infix shares its implementation with the per-backend account commands in agent-account.el, and syncs the selected account’s config home as soon as the selection is made.
Boolean menu infixes use a shared transient class that toggles the bound variable each time it is invoked. The theme-sync infix additionally calls agent-sync-theme-now when enabling theme sync, so backend configuration files are updated immediately.