v0.8.7-beta.5 is out · 5 Novsee the release
Browse guides / Agents
DOCS

Agents

docs/harnesses.md on GitHub

An agent is one coding CLI and everything Aether needs to know to launch it: how to start it in Standard and Background mode, where its login state lives, where its configuration lives, and which environment variables carry an API key. The code calls this definition a harness; the registry is internal/harness, a map and a few functions - not a plugin system.

Two rules shape everything below:

  1. Aether installs an agent only when a member asks. The member runs the displayed vendor install command in their Environment - their own persistent container shell, opened with aether terminal or from the dashboard - or asks the server to run it there (agent.install). The command should install the executable into ~/.local/bin. Once a shipped agent is installed there, Aether keeps it current; see Updates before launch.
  2. Aether does not copy vendor credentials to clients or synchronize them. Logins happen through the vendor's own flow in an Aether terminal. Credentials remain in the member home; an explicit account share mounts the agent's login path (the Login state column below) from that home into a recipient's run, a whole directory for omp, and no other credential. For the read-only subscription quota indicator, the server may read supported native Claude Code and Codex subscription credentials in that home and call the vendor's fixed HTTPS usage endpoint. Credential bytes and provider responses are never sent to the browser or a run.

The quota reader supports native OAuth subscription logins for Claude Code and Codex only. API-key logins, pi, omp, opencode, copilot, gemini, cursor, antigravity, fake, and deployment custom agents are not quota sources. The dashboard explains an unsupported source rather than treating missing usage as zero. Native vendor reauthentication remains a member action in the Environment; Aether does not refresh or rewrite OAuth files.

Subscription quota

The dashboard requests account.usage with {"account_member_id":"<member-id>","refresh":false}; an omitted or empty account selects the authenticated member. The result always has Claude and Codex rows. Each row reports only percentages and reset times returned by that provider. The internal provider endpoints are vendor APIs and may change without notice; a row can therefore be unauthenticated, unsupported, unavailable, stale, or error instead of inventing a number.

Successful results normally remain cached for 60 seconds. refresh:true bypasses that success TTL but still observes a 10-second request floor; errors are retried no faster than 60 seconds and provider retry deadlines are honored. Stale data is retained only for the same credential identity, always carries its error and stale status, and is not shown as current after its reset window has passed without a new measurement.

Usage fallback reads the same cache, never refreshing it. An account counts as exhausted when any window a run draws on that has not reset is at 95% used or more (fallback.ExhaustedPercent in internal/fallback): five_hour and seven_day for claude, primary and secondary for codex. Claude's per-model weekly windows are ignored, because a run's model can change mid-run and an unset model is the agent's own choice. A row other than ok means unknown usage, never exhausted.

Usage-limit signals

A run moves to another account mid-run (teams.md) only when its agent reports that the account's subscription usage limit stopped it. Claude Code's usage-limit messages start with one of its exported USAGE_LIMIT_ERROR_PREFIXES ("You've hit your", "You've reached your", ...; the full list is in internal/acphost/usagelimit.go).

Agent Mode Signal Covered
claude Enhanced session/prompt error with data.errorKind rate_limit and a usage-limit prefix after Internal error: yes
claude Background (claude -p) A rate_limit_event line with status rejected and no overage in use, then an error result line with a usage-limit prefix yes
claude Standard StopFailure hook with error rate_limit and a usage-limit prefix in last_assistant_message, sent as run.report with usage_limit:true yes, while lifecycle reporting is on
codex Enhanced, or Background over ACP session/prompt error with data.codexErrorInfo usageLimitExceeded yes
codex Background (codex exec --json) text only no: Aether does not parse codex exec output
codex Standard none: notify reports turn ends only no
opencode Enhanced, or Background over ACP (V2) session/prompt error with data.errorName provider.quota matched, but Aether cannot read opencode usage, so no account qualifies and the run stops with the no-fallback reason
opencode V1, any mode a generic error after retries no
pi any text headless; over ACP the error is dropped no
omp any text no: its sessions live in the shared ~/.omp/agent, so none can resume on another account
copilot, gemini, cursor, antigravity any none matched no

Transient 429s share Claude's rate_limit kind ("Server is temporarily limiting requests (not your usage limit)") and are not matched, nor is opencode's provider.rate-limit. A Background codex, omp, opencode, copilot, gemini or cursor run uses ACP when its server is installed (enhanced-runs.md). A signal a vendor changes falls back to the behaviour without fallback: the run fails or ends its turn with the vendor's error.

Shipped harnesses

--agent CLI Login state Configuration root API key env Launch env Status Steering Env setup Enhanced Switch
claude Claude Code ~/.claude/.credentials.json ~/.claude ANTHROPIC_API_KEY IS_SANDBOX=1 hooks (--settings) PTY yes, native installer adapter @agentclientprotocol/claude-agent-acp@0.86.0 yes
codex OpenAI Codex CLI ~/.codex/auth.json ~/.codex OPENAI_API_KEY - notify (-c notify=[...]) PTY yes, npm adapter @agentclientprotocol/codex-acp@2.1.1 no
pi pi ~/.pi/agent/auth.json ~/.pi ANTHROPIC_API_KEY, OPENAI_API_KEY - extension (-e) PTY yes, npm adapter pi-acp@0.0.34 no
omp oh-my-pi ~/.omp/agent (directory) ~/.omp ANTHROPIC_API_KEY, OPENAI_API_KEY - extension (-e) PTY yes, vendor script native (omp acp) yes
opencode opencode ~/.local/share/opencode/auth.json ~/.config/opencode ANTHROPIC_API_KEY, OPENAI_API_KEY - plugin (V1 inline config / V2 discovery) PTY (\r\r) yes, vendor script native (opencode acp) no
copilot GitHub Copilot CLI ~/.copilot/config.json - COPILOT_GITHUB_TOKEN COPILOT_ALLOW_ALL=true plugin (--plugin-dir) PTY yes, npm native (copilot --acp) no
gemini Gemini CLI ~/.gemini/oauth_creds.json, ~/.gemini/.env - GEMINI_API_KEY GEMINI_CLI_TRUST_WORKSPACE=true - PTY yes, npm native (gemini --acp) no
cursor Cursor CLI (runs as agent) ~/.config/cursor/auth.json - CURSOR_API_KEY - - PTY yes, vendor script native (agent acp) no
antigravity Antigravity CLI (runs as agy) ~/.gemini/antigravity-cli/antigravity-oauth-token - GEMINI_API_KEY - - PTY yes, vendor script none no
fake a script you name - - - - - PTY no none no
custom deployment-supplied - - - - - PTY no only with ACPArgs no

Paths are inside the run container, relative to the run user's home (/root, or /home/aether for a non-root image user). Login state is the path an account share mounts from the owner's home into a recipient's run (harness.Profile.CredentialPaths). Everything else there is the launcher's, except the owner's installation of the agent when the launcher has no ~/.local/bin/<executable> of their own: the owner's ~/.local/bin and ~/.local/lib mounted read-only at ~/.aether/account/bin and ~/.aether/account/lib, last on PATH, and each directory in harness.Profile.InstallPaths read-only at its own path (claude: ~/.local/share/claude, where the native installer keeps the versions ~/.local/bin/claude links to; cursor: ~/.local/share/cursor-agent, for ~/.local/bin/agent). A borrowed claude login also marks setup complete in the launcher's own ~/.claude.json (hasCompletedOnboarding), because Claude Code's setup wizard asks to sign in even when a login is present; Claude Code then records the owner's account email there itself. omp shares a directory because its login is a SQLite WAL database beside its settings, MCP configuration, extensions, and sessions. omp loads extensions and MCP server commands from that directory, so a recipient's run can plant code that runs in the owner's own omp sessions, and the owner's extensions run in the recipient's run; share an omp account only with someone you would give your home to (security.md).

The config.roots response carries the selected agent's effective credential_names and runtime_ignores. Credential names include shared denials and the agent's DenyNames; they match any path component case-insensitively. *.pem is also excluded. Runtime paths match exact root-relative paths or component prefixes case-sensitively; trailing slashes are presentation-only. Both lists apply before browser byte reads, including when the selected directory has been renamed. They cannot be overridden in the browser. A server without credential metadata must be upgraded before this importer can prepare a selection.

The Env setup column says how an agent installs into the member's Environment. Every shipped agent carries a vendor install command (InstallScript): Set up on the Agents page or onboarding's Agent step runs it through agent.install, then types the agent's login command into the Environment. fake and custom agents have none; install their executable into ~/.local/bin yourself in aether terminal. The local gateway's env.agents check of this machine's PATH covers only claude, codex and pi (harness.SetupHarnesses).

The Enhanced column is how the agent serves the Agent Client Protocol (ACP): JSON-RPC over the agent's stdio, through which an Enhanced run reads the agent's messages, tool calls, and permission requests instead of its terminal. enhanced-runs.md covers launching and operating one; Enhanced mode adapters covers installing what serves it.

The Switch column marks the agents whose running session can move between Standard and Enhanced (agent.list switchable); see Switching a running agent.

Enhanced mode adapters

A native agent serves ACP from its own CLI (omp acp, opencode acp, copilot --acp, gemini --acp, agent acp), so installing the agent installs it. An adapter agent needs a separate npm package that drives the CLI. Aether pins one version of each, taken from the ACP registry and recorded in internal/harness/acpregistry.json, a snapshot of only the entries Aether uses. A custom definition is enhanced only when it names its ACP server's argv: ACPArgs in --agent-definitions, acp_args in agent.register, or --acp on aether agent add. ACPMode / acp_mode / --acp-mode names the session mode a new Enhanced session starts in. Set the mode that acts without asking: an agent left in its default mode asks before every tool, and a swarm worker waits on that prompt. It needs ACPArgs and holds no whitespace.

An adapter installs into the member home like an npm agent:

CODE
npm install -g --prefix "$HOME/.local" @agentclientprotocol/claude-agent-acp@0.86.0

which links ~/.local/bin/claude-agent-acp (codex-acp, pi-acp) to the package in ~/.local/lib/node_modules. Install the agent and its adapter in one step:

CODE
aether agent add claude --enhanced

types the agent's install command followed by that line into the Environment (or prints it when no terminal opens). The server RPC agent.install with {"name":"claude","enhanced":true} runs the same command in the member's Environment, starting it if needed, waits up to 10 minutes, and answers with the last 8 KiB of output, the exit status, and whether the agent and its adapter now resolve. The dashboard's agent setup calls it. One install runs per member at a time. It first waits for a pre-launch update writing into the same home, and a launch skips that update while an install runs. The server wraps the command in mktemp and timeout, so the environment image must provide both (coreutils or busybox); without them the install fails with timeout: not found and exit status 127.

The pre-launch update brings an installed adapter to its pinned version after the agent's own update, through the same staged exchange as codex. It never installs a missing one: a member who never asked for Enhanced mode downloads nothing. On another member's shared account, a launch that borrows the owner's installation borrows the owner's adapter with it, read-only.

agent.list reports, per agent, enhanced (native, adapter, or none), enhanced_installed (the adapter, or the native CLI, resolves for that launch from the same ~/.local as the CLI), login_found (a non-empty Login state file or directory exists in the home the launch signs in with: the owner's on a shared account; it checks for the path, not for a working session), default_mode: acp for codex, omp, opencode, copilot, gemini and cursor once their ACP server is installed, tui for everything else, and enhanced_default: true for those six whether installed or not. Claude Code stays on its terminal by default because its adapter runs on the Claude Agent SDK, whose terms favour API keys.

Every newly created managed runtime container receives the verified /usr/local/bin/aether-internal CLI, including taskless runs, custom images, member terminals, and verification containers. Runs also receive their own identity socket, including when conflict coordination is disabled. Member terminals and verification containers do not inherit a run identity. Staging failure refuses creation rather than silently omitting the CLI.

Standard and non-ACP Background launches get no Aether MCP registration; Enhanced runs and ACP Background launches get the MCP bridge over ACP. Shipped agents other than gemini, cursor and antigravity carry the run's agent instructions in their system prompt (How Aether launches them); aether-internal skill loads live capability and assignment guidance. Containers without run identity receive only general guidance, not borrowed authority. OpenCode's instructions do not depend on the optional lifecycle-status plugin.

The startup switches follow the vendor references: Claude CLI reference, Codex configuration reference, pi CLI reference, omp CLI reference, OpenCode configuration, Copilot CLI command reference, Gemini CLI configuration, and Cursor CLI parameters. Aether passes these only for the one launch that owns the coordination socket.

The Status column is how the agent itself tells Aether it is waiting for you, rather than leaving the server to guess from silence. See "Status reporting" below.

The Launch env column is what the server sets in the run container because the CLI will not start without it. It is applied after the workspace's own variables, so a workspace cannot leave the agent unable to run. See Launch argv for why claude, copilot and gemini each need one. Optional reporter environment (OpenCode V1's OPENCODE_CONFIG_CONTENT) is not a required Launch env value; see "Status reporting" for its Standard-only scope.

Fixed launch values replace workspace values of the same name; OPENCODE_CONFIG_CONTENT is merged key by key instead. Native OpenCode coordination has version-specific merge/discovery behavior, described under managed loading.

  • Standard (tui). Container PID 1 supervises the agent and opens a login shell after any normal agent exit. Exiting that shell opens another, so the run and container remain available until an explicit Close, Kill, or Delete. Reporting success or failure marks the task's outcome without ending the session; follow up in the same terminal or message box (see Report an outcome).
  • Background (headless). The agent is the container's main process. When it exits, Aether commits and publishes the branch, records completed or failed, and destroys the container immediately. It never opens a replacement shell and can never be reopened. codex, omp, opencode, copilot, gemini and cursor run their background task over ACP instead when their ACP server is installed, and finish the same way after its one turn; Claude keeps claude -p because its adapter runs on the Claude Agent SDK (enhanced-runs.md).

Close a Standard or Enhanced run explicitly:

CODE
aether close <run> --outcome merged
aether close <run> --outcome abandoned

Close pauses the container and retains the exact container, checkout, run row, member account, and coordination surfaces for --run-container-ttl. The default is 168h (7 days); 0 uses that default. A negative value disables retention and cleans up immediately. Kill and Delete remain immediate cleanup operations.

Reopen (aether reopen) is available for a retained Standard or Enhanced run after Close, while its retention deadline has not passed:

CODE
aether reopen <run>

It resumes the same row, container, checkout, member account, and coordination surfaces; it performs no new launch, checkout, container, branch, or disk-floor admission. Expired or unavailable runs cannot be reopened. A deployment-supplied argv override receives no registry-only flags, because nothing checks that the override is still the registered CLI. See failure-handling.md.

Incoming coordination hooks

Coordination uses the durable run mailbox, not terminal keystrokes. An inbox integration tells the agent to read mail; a status reporter tells Aether what the agent is doing. They are separate even when both load through the same native extension/plugin mechanism.

Agent CLI --agent Mail at next boundary Native idle wake Enhanced run wake Copyable asset
Claude Code claude yes yes, managed Standard launches yes claude.json
Codex codex yes, managed Standard launches, after you trust the hooks once no yes codex.json
Native pi pi yes yes, owning session only yes pi.ts
OMP omp yes yes, main agent only yes omp.ts
OpenCode V1 opencode yes yes, selected root only yes opencode-v1.js
OpenCode V2 opencode yes, V2 API only yes, selected root only yes opencode-v2.js
Copilot CLI copilot yes, managed Standard launches no yes copilot.json
Gemini CLI gemini yes, once installed by hand no yes gemini.json
Cursor CLI cursor yes, once installed by hand no yes cursor.json
Antigravity CLI antigravity no packaged hooks no no none
Fake fake explicit inbox only no with ACPArgs none
Other/custom deployment or member definition adapter required native API required with ACPArgs generic.sh

An overlap notice tells the agent to run aether-internal status and, if the overlapping run is listed there, send it its plan before editing shared files. The notice comes from the hook's overlaps list, so it can appear without a matching peer in status. Inbox notices call peer messages attributed data, not instructions; see Send a message.

“Yes” requires the matching integration to be loaded and executing in the owning live session. It is not a promise that a copied file, a registry entry, or a status reporter activates inbox delivery. Native idle wake is for eligible Standard runs, not a way to restart Background or exited processes.

Enhanced run wake needs no asset: in an Enhanced run the server sends the idle ACP session at most one prompt per set of new unread messages (enhanced-runs.md). The container sets AETHER_ENHANCED=1, and there every aether-internal hook exits without output, so hooks the agent loads from the member's own settings do not announce the same mail again. A native wake receiver in such a run stops with hook wake: this is an enhanced run, whose agent Aether wakes with a session prompt; stop this receiver.

Installation and activation

Inside the run, inspect configuration and export the asset for only the current agent:

CODE
aether-internal skill --hooks
aether-internal hook file
aether-internal hook file claude.json

For JSON, append missing Aether entries to each hooks event array; preserve unrelated settings and hooks. Do not install duplicates at user and project scope. Copilot and Cursor snippets use version: 1; resolve an existing version conflict rather than overwriting it. Commands invoke /usr/local/bin/aether-internal directly, without jq or another script. For TypeScript/JavaScript, export the matching file to the destination below only if absent; inspect/diff an existing file before editing. No executable bit is needed. For example, a new manual OMP installation is:

CODE
mkdir -p "$HOME/.omp/agent/extensions"
test ! -e "$HOME/.omp/agent/extensions/aether.ts" &&
  aether-internal hook file omp.ts > "$HOME/.omp/agent/extensions/aether.ts"

When the launch loaded the hooks (managed loading), aether-internal skill prints Inbox hooks (<agent>): loaded by this run's launch; do not install a manual copy. instead of the file check, and skill --hooks prints that line first. Its file checks cover manual copies only.

Do not add a manual copy merely because a managed run-scoped extension is absent from the checker’s fixed paths. skill is read-only: configured means present on disk, not loaded, trusted, or executed. It checks standard user and current-directory project paths, not every ancestor, package, explicit CLI path, or runtime override. Restart/reload as below, inspect the native load errors, and verify a real event in the intended root session. Never edit trust records or remove an intentional disable setting to make the checker green.

For OMP, unverified (modified/unknown source at ...) describes only the named manual file's mismatch with the CLI's embedded asset. An older context-only extension can produce that result while a managed launch names -e /run/aether. Inspect the live launch arguments and /run/aether/aether.ts separately; neither the manual mismatch nor a matching managed file proves which handlers executed. Compare with aether-internal hook file omp.ts, preserve custom code and disable settings, then verify a real context boundary and idle wake before claiming activation.

Managed native loading

For the shipped claude, codex, copilot, pi, omp, and opencode agents, Aether loads native mailbox hooks for coordinated, task-bearing Standard launches. Taskless, Background, disabled-coordination, and custom/overridden argv launches do not get this automatic mailbox installation. Manual integrations remain usable where the agent supports them; a boundary hook in a Background process does not grant it idle wake.

Claude Code accepts one --settings value, so the server writes one document, /run/aether/claude-settings.json, and adds --settings /run/aether/claude-settings.json. It merges the status reporter hooks, the claude.json inbox hooks except Stop, and the idle-wake waiter. No user or project settings file changes. Claude Code runs an identical command hook from several settings sources once, so an unmodified manual claude.json does not double the managed hooks (observed with Claude Code 2.1.301); a modified copy runs too. A manual copy's Stop hook does nothing inside a managed launch, which sets AETHER_MANAGED_HOOKS=1. Taskless launches keep the status-only document; Background and Enhanced launches load neither.

Codex takes the codex.json hooks as four -c arguments, hooks.SessionStart, hooks.UserPromptSubmit, hooks.PostToolUse and hooks.Stop, each a TOML inline table passed as one argv element. They are active only after the user trusts them once; see Codex inbox integration.

Copilot CLI loads hooks from a plugin directory, so the server stages /run/aether/plugin.json (a manifest named aether-inbox) beside /run/aether/copilot-hooks.json, the copilot.json hooks, and adds --plugin-dir /run/aether. No user or project file changes. Copilot reads the hooks only in a trusted folder; the profile's COPILOT_ALLOW_ALL=true trusts the working directory, so no trust dialog appears.

Gemini CLI and Cursor CLI stay manual. Gemini has no per-launch path: GEMINI_CLI_SYSTEM_DEFAULTS_PATH replaces the image's system-defaults file, Gemini skips system files not owned by root, and extensions load only from ~/.gemini/extensions. Cursor's --plugin-dir loads only when logged in and needs a path at least three directories deep; it is not verified live, so it stays manual for now.

Antigravity CLI has no packaged inbox hooks. agy reads hooks.json from ~/.gemini/config/hooks.json or .agents/hooks.json, but has no per-launch flag or environment variable to load one, and Aether ships no asset for it.

Every managed launch, whether it loads a file or arguments, sets AETHER_MANAGED_HOOKS=1 in the run environment, which switches the skill output described above.

For pi, the server stages /run/aether/aether.ts and adds -e /run/aether/aether.ts. For OMP it stages that same filename but adds -e /run/aether, using the native directory loader. Both retain the separate /run/aether/status.ts reporter. Managed loading skips --no-extensions / -ne and does not change persistent user/project configuration. Current manual/managed copies share one receiver rather than starting independent watchers. If an older context-only copy is also loaded, it can still execute its own context helper; it does not acquire the current receiver's shared lifecycle. A manual-source warning is not an instruction to add another copy or overwrite that file.

Pi's explicit-file loader would otherwise bypass resource exclusions, so the managed copy reads settings through pi's public SettingsManager. Any negative ! or - extension selector in global/project settings or a package's extension selectors conservatively disables automatic mailbox loading, with a warning; unreadable settings also fail closed. A separately loaded, explicitly authorized manual copy is unaffected. OMP instead uses its native disabledExtensions directory filter, described below.

OpenCode's managed wrapper preserves the original argument boundaries:

CODE
/bin/sh /run/aether/opencode-native-launch.sh /run/aether \
  opencode --prompt="<task>"

The wrapper checks the installed opencode --version: major 1 selects V1, major 2 selects V2, and another major refuses managed startup with an error. A custom launch definition remains the explicit alternative. The API targets below are specific releases, not a guarantee for every future release in those majors.

  • V1: /run/aether/opencode-native-v1.json merges the workspace's inline config with file URLs for /run/aether/opencode-v1.js and /run/aether/opencode-status.js and with /run/aether/instructions.md under instructions, preserving unrelated fields and existing entries. The wrapper supplies it through OPENCODE_CONFIG_CONTENT.
  • V2: the wrapper adds --standalone, selecting a private TUI-owned server rather than the member home's detached service. This keeps server and receiver lifetime tied to this run and avoids cross-container service registration collisions. The server stages opencode-v2.js and opencode-status-v2.js in /run/aether and mounts them read-only at /.opencode/plugins/aether-mailbox-<run-id>/index.js and /.opencode/plugins/aether-status-<run-id>/index.js. Native ancestor discovery loads these packages before user enable/disable directives. OPENCODE_CONFIG_CONTENT remains unchanged. The stable plugin IDs are aether-mailbox and aether-status, regardless of the run-specific directory.

OPENCODE_PURE=1 or OPENCODE_PURE=true (case-insensitive true) suppresses managed OpenCode mailbox and status additions; 0 and false do not. V2's native plugins: ["-aether-mailbox"] disables only mailbox automation; "-aether-status" disables its reporter and "-*" disables all discovered plugins. Preserve these choices. Aether writes neither the worktree nor member configuration and does not mount over an existing user plugin path.

Claude Code inbox integration

Managed. A coordinated, task-bearing Standard launch of the shipped claude loads the inbox hooks and the idle-wake waiter through /run/aether/claude-settings.json (managed loading). Nothing needs installing. A taskless launch's status-only document is not an inbox installation.

Manual. Export claude.json; merge into ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR/settings.json). Project alternatives are .claude/settings.json and .claude/settings.local.json. Restart Claude Code and review /hooks in its normal workspace-trust UI. Honor disableAllHooks, allowManagedHooksOnly, and managed settings such as /etc/claude-code/managed-settings.json. claude.json has no waiter, so a manual copy announces mail at boundaries only: under claude -p an asyncRewake hook runs in the foreground and would block a Background run.

The supported events are SessionStart, UserPromptSubmit, PostToolBatch, SubagentStart, and Stop from the current Claude hook API. Older releases may lack these events or context outputs. Child-agent events do not take ownership of the run mailbox; SubagentStart tells the subagent never to report and how to note its edits to overlapping peers. In a manual copy, Stop may request one continuation for pending mail or an integrator refresh.

The managed document leaves out the inbox Stop hook. Claude Code runs Stop hooks in parallel, so its block for unread mail would continue a turn the status reporter already ended. Instead, at turn end the waiter below waits for mail no hook has announced and wakes a new turn for it. The Stop hook of a manual claude.json copy exits without output when AETHER_MANAGED_HOOKS=1, so it does nothing inside a managed launch and still checks mail at turn end in a Background claude -p run that shares the same ~/.claude. SessionStart has no matcher, so it fires at startup, resume, compaction and clear. The launch's system prompt carries the role card, so the hook prints only the pending notices, all of them again after compaction or clear.

Idle wake. The managed document adds this asyncRewake hook to SessionStart and Stop:

CODE
{"type":"command","command":"/usr/local/bin/aether-internal hook claude wake","async":true,"asyncRewake":true,"timeout":604800}
  • The waiter claims the session by writing its PID to $TMPDIR/aether-hook-<run>-claude-wake, then repeats bounded 30-second server waits. A newer waiter (the next Stop, /clear, a resume) replaces it. The inbox UserPromptSubmit and PostToolBatch hooks delete the file, so a busy session has none; a replaced waiter exits within a second. Esc fires no Stop, so after an interrupt wake pauses until a turn completes.
  • When the server admits a wake for mail the hooks have not announced, the waiter writes the trusted inbox instruction (never peer text) to stderr, adds the IDs to $TMPDIR/aether-hook-<run>-claude.json, and exits 2. Claude Code then starts a turn with no human input. The terminal shows Stop hook feedback; the model receives the instruction inside a background task notification as Stop hook blocking error from command "Stop": .... That wording is Claude Code's framing, not an error.
  • On SessionStart all unread mail counts as unannounced, so a resumed idle session with mail waiting wakes at once. On Stop the waiter wakes only for mail missing from that record: mail a hook already announced in the turn does not start another. A wake admitted while a turn runs joins that turn as one more pointer.
  • Nothing else exits 2. Errors exit 1, after up to five retries with 1 to 16 second backoff for transport errors; Claude Code shows them only in claude --debug. The next completed turn starts a new waiter.
  • The hook timeout is 604800 seconds (7 days); the waiter exits a minute before it. A session idle longer is not woken until a turn completes.
  • /exit and SIGTERM stop the waiters. If Claude Code is killed, the waiter sees its stderr hang up and exits within a second. Subagent events, an Enhanced run, and a missing coordination socket make it exit 0 at once.
  • Protection, human takeover, and swarm authority suppress the wake; the server rechecks them on every 30-second wait.

Codex inbox integration

Managed. A coordinated, task-bearing Standard launch of the shipped codex loads the codex.json hooks through one -c hooks.<Event>=[...] argument per event, next to the -c notify=[...] status reporter. Codex has no hooks flag; a -c override is read as a session-flags source, which Codex lists but does not treat as managed.

Trust once. Codex runs a hook only after you trust it. The first Standard run opens Hooks need review: choose Trust all and continue, or open /hooks and press t. Codex stores the approval in $CODEX_HOME/config.toml (~/.codex/config.toml) under [hooks.state."<key>"]. The key names the session-flags source, not a path, and the stored hash covers the event and handler, so one approval covers every workspace and worktree until Aether's hook definitions change. Choosing Continue without trusting asks again at the next start. A swarm worker can be the first Standard Codex run; open its terminal to answer. If the list shows hooks you don't recognize, choose Review hooks. Inside the run, aether-internal skill says whether a hook has run yet.

Aether does not bypass or pre-write this trust: --dangerously-bypass-hook-trust also skips review of your own hooks, /etc/codex managed hooks cannot be disabled and apply to the whole container, and writing hooks.state would approve on your behalf.

A manual copy in ~/.codex/hooks.json has a different key and would run every hook twice in a managed run. Remove it.

Manual. For other launches (Background, taskless, or an overridden argv), export codex.json; merge into ~/.codex/hooks.json (or $CODEX_HOME/hooks.json), alternatively .codex/hooks.json. Restart Codex, then use /hooks to review and trust the exact hook definitions; repeat after changing them. Project trust is not hook trust. Honor managed policy and features.hooks / features.codex_hooks controls in the applicable config.toml.

Supported events are SessionStart, UserPromptSubmit, PostToolUse, and Stop, filtered against child-agent events. The built-in notify reporter only reports turn completion: it neither installs these hooks nor wakes later-idle Codex. No idle wake exists for Codex: no supported mechanism could be verified, so mail waits for the next hook boundary or an explicit inbox read. Use a version implementing the current Codex hook contract.

Native pi inbox integration

The native API target is pi 0.87.1. For manual installation, export pi.ts to ~/.pi/agent/extensions/aether.ts, or $PI_CODING_AGENT_DIR/extensions/aether.ts; the project alternative is .pi/extensions/aether.ts. Restart pi or use /reload, inspect extension load errors, and preserve configured extension/resource exclusions and --no-extensions.

The extension adds guidance at the per-model context event. Native wake uses sendMessage with triggerTurn: true and deliverAs: "followUp", but does not proactively queue during busy work. Pi suspends the idle wake receiver for an active turn; context hooks continue, and new mail stays durable and unnotified. Successful native agent_settled plus isIdle rearms a fresh helper snapshot and admission before dispatch, rather than restarting a wake helper after every model call. This avoids leaving a custom hint queued through a retry or compaction Stop; a successful automatic retry still rearms wake normally.

Pi owns one root session and follows its new/resume/fork lifecycle, not independent multi-root SDK hosts. Stop/abort, failed native settlement, or compaction abort pauses wake until an accepted non-extension user message (not merely an input callback) or an explicit session replacement. The configured interrupt key is observed without consuming it. UI prompts suspend observation until resolved; extension continuations do not undo Stop.

OMP inbox integration

The native API target is OMP 18.3.1. Export omp.ts to ~/.omp/agent/extensions/aether.ts, or .omp/extensions/aether.ts for the project. A profile can change the user destination: run omp config path and use its active agent directory, not a guessed default. The checker accounts for OMP_PROFILE / PI_PROFILE and PI_CODING_AGENT_DIR; CLI --profile, --config, and -e overrides require checking the live launch. Restart OMP after installation.

Only the main agent handles context and native wake. sendMessage uses triggerTurn: true and deliverAs: "followUp" only while idle. Busy work suspends the wake receiver; context hooks still point to the durable inbox. Mail read and acknowledged during that work must not leave a follow-up queued after its final response. Mail still unread at settlement receives fresh server admission before a single deferred wake.

OMP 18.3.1's extension agent_end can precede prompt cleanup and carry willContinue: true for native retries or todo reminders. It is not the idle boundary. The receiver observes the owning AgentSession's public terminal agent_end, waits for waitForIdle() outside that callback, rechecks ExtensionContext.isIdle() and session generation, then requests an immediate fresh mailbox snapshot. It does not use pi's agent_settled events. Stop/abort cancels the receiver and any pending settlement observation until actual non-extension input starts a turn or an explicit session switch/branch replaces it. Tool-approval prompts suspend observation until resolved; their resolution does not authorize queuing a turn while work remains busy.

Honor --no-extensions and disabledExtensions: [extension-module:aether]. On OMP 18.3.1, directory loading (-e /run/aether) applies that disabled-module filter; explicit-file loading (-e /run/aether/aether.ts) bypasses it and must not be used to circumvent the user's choice.

OpenCode V1 inbox integration

The V1 API target is OpenCode 1.18.32. Check opencode --version, then export only opencode-v1.js to ~/.config/opencode/plugins/aether.js (or $XDG_CONFIG_HOME/opencode/plugins/aether.js); a project alternative is .opencode/plugins/aether.js. An explicit OPENCODE_CONFIG_DIR may supply another plugin directory. Restart OpenCode and inspect plugin load errors.

The plugin binds the first explicitly prompted root session. Child sessions are ignored; prompting a different root retires the receiver until the plugin host restarts, rather than transferring the mailbox. The context hook supplies a trusted inbox instruction. Native wake uses client.session.promptAsync with a synthetic text part, only after a successful turn completes and the root is idle. Mail arriving during busy work stays durable until a fresh helper admission at that boundary. V1's prompt API joins active work; it is not a next-turn queue and has no Aether followUp argument.

Stop or error pauses wake until the next explicit prompt in the same root. An unclassified idle event (for example, shell/setup cancellation) does not rearm wake; a successful model completion is required. Preserve an intentional OPENCODE_PURE=1 or true, which disables external plugins (0 and false do not). Do not load the V2 file.

OpenCode V2 inbox integration

The V2 API target is OpenCode 2.0.18 (@opencode/cli). Check opencode --version; export only opencode-v2.js to .opencode/plugins/aether-mailbox/index.js. The user alternative is ~/.config/opencode/plugins/aether-mailbox/index.js under the active XDG config root. The asset default-exports the native {id, setup} definition and uses only Node built-ins; neither a package.json nor an @opencode/plugin runtime installation is needed. Restart the V2 plugin host and inspect load errors. For a manual Aether launch, use opencode --standalone to keep its server owned by the run's TUI, not a detached service shared through the member home. Its stable ID is aether-mailbox, so honor native "plugins": ["-aether-mailbox"] or ["-*"] disable directives. OPENCODE_CONFIG_DIR relocation is unverified for V2; do not infer it from V1's behavior.

V2 also supports legacy auto-discovered plugins/aether.js files in these roots; its pinned loader distinguishes these from explicitly configured local plugins, which use directory packages. Inspect any existing flat file before adding the recommended package. The first exported aether-mailbox ID wins, so an older manual V2 copy can shadow the managed integration without running a second receiver. Update or migrate an authorized existing copy rather than installing both. A V1 asset at that filename is not a V2 integration.

V2 startup caveat: in 2.0.18, an initial task passed with --prompt can remain prefilled in the composer instead of submitting. If the TUI is ready and that task has not started, press Enter once to submit it. This is human submission of the initial prompt, not mailbox delivery: Aether does not auto-press Enter or add a PTY workaround. Later admitted mailbox wake uses the native session API after the owning root has completed a turn.

V2 uses ctx.session.hook for prompt/context and ctx.session.prompt with delivery: "queue" for the trusted wake text. Despite that queue API, this integration defers busy mail until successful completion and settlement: V2 reports interruption after cleanup, too late to safely enqueue during a human Stop. It rechecks the root outcome and obtains fresh helper admission before dispatch; it does not steer active work.

The first explicitly prompted root owns the receiver. Children are ignored; prompting another root retires it until plugin-host restart. Stop/error pauses wake until the next explicit prompt in the same root; deleting, archiving, or moving the root ends its ownership. Do not load both versioned files. A V1 status reporter or V1-compatible argv does not establish V2 plugin activation. Preserve plugin disable/trust policy and verify a real session event.

Copilot CLI inbox integration

Managed. A coordinated, task-bearing Standard launch of the shipped copilot loads the inbox hooks through --plugin-dir /run/aether (managed loading). Nothing needs installing.

Manual. Export copilot.json; merge into ~/.copilot/hooks/aether.json (or $COPILOT_HOME/hooks/aether.json), alternatively .github/hooks/aether.json. Quit and restart in the intended trusted repository. Honor disableAllHooks, allowManagedHooksOnly, enterprise policy, and session trust; --config-dir may require a different destination.

Supported events are sessionStart, postToolUse, and agentStop, using the current Copilot hook API. Config-file userPromptSubmitted output is not used for context delivery. There is no later-idle wake.

Gemini CLI inbox integration

gemini does not load this file on its own in Standard mode; install it by hand. Export gemini.json; merge into ~/.gemini/settings.json, or .gemini/settings.json for the project. GEMINI_CLI_HOME is the parent of .gemini, not that directory itself. Fully exit and restart Gemini, then inspect /hooks list.

The supported events are BeforeAgent, AfterTool, and AfterAgent in the current Gemini hook API. Honor folder trust, system settings, hooksConfig.enabled, and hooksConfig.disabled (the hook name is aether-inbox). An intentional disable is not permission to run /hooks enable automatically. These are boundary hooks, not an idle receiver.

Cursor CLI inbox integration

cursor does not load this file on its own in Standard mode; install it by hand. Export cursor.json; merge into ~/.cursor/hooks.json or .cursor/hooks.json. Use /quit or /exit, restart in the intended workspace, respect workspace trust, and inspect /logs while exercising an interactive boundary. CURSOR_CONFIG_DIR and XDG_CONFIG_HOME do not establish a documented relocation rule for hooks.json; verify custom paths in the running CLI.

Events are sessionStart, postToolUse, and stop in the current Cursor hook API. Older --print releases, including 2026.08.11-e8db854, omit stop; headless parity is not guaranteed. Another stop hook can supersede followup_message. Aborted turns and repeated stop continuations do not receive another Aether continuation. There is no later-idle wake.

Fake inbox support

fake is a deterministic scheduler/test agent, not a vendor CLI. It receives the run-mounted CLI and identity socket, but has no shipped context hook, status reporter, trust UI, or native wake API. When coordination is enabled, it must read and acknowledge the inbox explicitly.

Custom and unlisted agents

A launch definition only supplies argv; it does not prove a CLI has loaded an inbox integration. Export generic.sh for a plain-stdout native context boundary, or adapt its output with the host's documented serializer. There is no universal destination or activation command. Native idle wake requires a real session API and lifecycle/Stop integration; do not emulate it with PTY input. Follow the unsupported-harness authoring guide for runnable examples and the complete contract.

Shared delivery and wake limits

While actively coordinating, use aether-internal inbox --wait 30; an active inbox waiter has priority over native observers. Native wake carries only a trusted inbox pointer, never peer text at system/developer priority. The agent reads attributed bodies through inbox, processes the batch, and explicitly acknowledges its returned token. Sending, hinting, and native API acceptance are not acknowledgement or proof the model read anything.

Protection and human takeover suppress new wake admission without losing mail. Current run/swarm authority is checked again when the server dispatches the admitted response, not from a cached eligibility flag. A wake already accepted before a later hold may still be processed. Native receivers cancel stale session generations and respect Stop/abort and approval waits; none starts a dead process or attaches to an arbitrary TUI. Without a loaded working integration, use explicit inbox reads; there is no silent terminal fallback.

Native receiver errors appear in the agent's own warning UI or stderr. Only recoverable helper/transport failures get bounded backoff; unsupported protocols and nonretryable helper errors stop observation rather than spinning. After exhausting helper retries, pi/OMP require a reload/restart; OpenCode pauses until another explicit same-root prompt; Claude Code's waiter exits and the next completed turn starts a new one. Native synchronous send rejection instead permits lifecycle-driven recovery with fresh admission, without marking the rejected IDs notified or blindly resending. Fix the underlying error first; reloading never erases or acknowledges durable mail.

Command stop hooks may request one continuation for pending mail or an integrator's final swarm refresh, even with an empty inbox. Native repeat-stop guards prevent loops. Automatic hook checks have a separate bounded allowance, so they cannot consume explicit coordination capacity. See delivery semantics for acceptance, acknowledgement, and retries.

Status reporting

The reporter sets execution: running, or needs-attention for settled, failed, and stalled execution. An input request is separate: it requires an unresolved, correlated question, form, extension dialog, or permission request, and a running run can have one open. The dashboard folds both into its run state and shows needs-attention or an open request as Needs you; see dashboard-frontend.md. Turn end, silence, notification text, and a question in the final response do not establish a pending request.

An agent with a Status entry can run a command on its own lifecycle events. Aether points each one at the staged server binary inside the container, through whatever the CLI's own mechanism is, for that launch alone - by flag where the CLI has one, by environment where it does not - and where that mechanism needs a file, the file is written into the run's coordination directory. The status-only argument additions are shown below; task-bearing native launches also use managed loading, which for claude adds the inbox hooks and idle-wake waiter to the same claude-settings.json:

CODE
claude --dangerously-skip-permissions "<task>" \
  --settings /run/aether/claude-settings.json

codex --dangerously-bypass-approvals-and-sandbox "<task>" \
  -c 'notify=["/opt/aether/aether-server","report","codex"]' \
  -c 'hooks.SessionStart=[{hooks=[{type="command",command="/usr/local/bin/aether-internal hook codex SessionStart",timeout=5}]}]'
  # ...and the same -c for UserPromptSubmit, PostToolUse and Stop

pi "<task>" -e /run/aether/status.ts
omp --auto-approve "<task>" -e /run/aether/status.ts

OpenCode's reporter-only/taskless profile uses V1's OPENCODE_CONFIG_CONTENT plugin mechanism:

CODE
OPENCODE_CONFIG_CONTENT='{"plugin":["file:///run/aether/opencode-status.js"]}' opencode

Task-bearing managed launches instead select a version-matched V1 or V2 status plugin alongside the mailbox plugin. V2 translates native execution, permission, and form events into the same canonical reporter commands. Taskless launch wiring is not a V2 activation guarantee; use a matching manual integration/custom launch and inspect the actual plugin host.

Every reporter uses the existing staged binary and coordination socket:

CODE
/opt/aether/aether-server report claude                 # hook JSON on stdin
/opt/aether/aether-server report codex '<notify JSON>'
/opt/aether/aether-server report pi --json '<report JSON>'       # pi and OMP
/opt/aether/aether-server report opencode --json '<report JSON>' # V1 and V2

Stateful extensions/plugins send state (working or the legacy waiting wire value), an optional reason, and input_updates. A waiting report may set usage_limit: true, with the agent's usage-limit message as reason, to ask for a move to another account; Claude's StopFailure hook is the only shipped reporter that sets it, and it is ignored on any other state. Claude's hooks, Codex's notify and the pi/omp extension also carry the agent's own session id (session_id, Codex's thread-id), which a Standard run stores so a mode switch can resume it; OpenCode's plugins carry none. Their replace update contains the complete current request list, including [] when empty. Claude's stateless hooks send matching open/close updates. Each request contains only id, session_id, and kind (question, permission, form, or extension_ui). Identity includes all three: answering one request cannot clear another session's request with the same ID. An input-only report leaves execution unchanged. No prompt, answer, tool arguments, transcript, URL, or credential is included in metadata. There is no second report transport and no new way to answer prompts; use the existing terminal or native approval/question surface.

Reporter Execution evidence Input request evidence and limits
Claude command hooks UserPromptSubmit and the main agent's tool activity report Working. A subagent's tool hooks (agent_id set) and SubagentStart report subagent only: they keep the run from stalling and change no execution state; SubagentStop reports nothing. Root Stop reports waiting unless background_tasks holds a task other than a local_bash shell, which reports Working with working in background: <n> tasks counting those tasks: state waiting-agents when one is a subagent, workflow or teammate, else waiting-tools with tools. StopFailure reports waiting, not an input request: with usage_limit and the agent's message as the reason when error is rate_limit and the message is a usage limit (usage fallback), else with the reason agent failed (state failed). AskUserQuestion opens by tool_use_id; matching completion/failure closes it. MCP Elicitation/ElicitationResult open/close a form only when elicitation_id is present. Child identity includes agent_id. SessionEnd clears the terminated scope; ordinary Stop/SubagentStop does not clear requests.
OpenCode V1 1.18.32 Tracks all native busy sessions; one child becoming idle cannot park another active session. permission.asked/question.asked preserve id and sessionID; replies/rejections close the matching requestID. Idle reconciles that session's known requests with the released /permission and /question lists, including interruption cleanup without a reply event.
OpenCode V2 2.0.18 Tracks session.execution.started until succeeded, failed, interrupted, or session deletion. permission.asked/replied and released form.created/replied/cancelled preserve request and session IDs. Terminal execution and permission replies reconcile known permissions through ctx.permission.list. Session deletion removes only that session's requests.
pi 0.87.1 Starts report Working; agent_settled or confirmed idle after agent_end reports waiting. A late finalized message cannot restart settled execution. Named ask/AskUserQuestion tools retain toolCallId until tool execution ends. The documented ui_prompt_start/end pair tracks blocking extension UI with one process/session-scoped ID; Pi emits this pair around the outermost dialog, including dismissal/rejection. Titles and answers are discarded.
OMP 18.3.1 Uses its public main-session terminal event and waitForIdle() so owned background work can drain before it reports waiting; automatic continuation is not settlement. Named ask tools retain toolCallId. tool_approval_requested/resolved retain sessionId and toolCallId, including rejection. The pinned native TUI API has no generic ui_prompt_start/end equivalent.
Codex legacy notify agent-turn-complete reports waiting only. No correlated input evidence in this integration. No app-server migration or inference from the final message.

Pi, OMP, and OpenCode forward native Working reports even when the request snapshot is unchanged. Pi/OMP tool callbacks, V1 busy status callbacks, and V2 concurrent execution starts refresh liveness without inventing input or status transitions. Actual settlement still reports waiting; silence without further activity can still trigger the stall detector.

OpenCode keeps execution and requests independent: a native busy session stays Working while its tool awaits input, and a background session can remain Working after the requesting session becomes idle. Closing a request does not invent a new busy session. An unresolved request survives ordinary settlement; matching completion, authoritative native reconciliation, or actual session/run termination clears it.

Failed or invalid OpenCode request-list reads retain the observed requests and schedule retries with a one-second delay. Successful reconciliation stops retrying that snapshot, even if some requests remain live. Matching replies, session deletion and plugin shutdown cancel obsolete work; an older read cannot clear a newly opened request. Both versions use a four-second query timeout. V1 serializes retries with lifecycle events; V2 allows one query or delay per affected session.

The public CLI surfaces have limits:

  • Claude PermissionRequest explicitly omits tool_use_id, and Notification permission/elicitation alerts have no correlated resolution identity. Neither creates a durable input request. Elicitations without IDs are likewise unsupported. Claude has no general command-hook interrupt/request-list API; an interrupted request whose completion hook is absent can remain until session termination. Root Stop can observe background work, but these stateless hooks cannot infer root completion from a later child-only stop. Scheduled future wakeups (session_crons) alone are not current work.
  • V1 reconciliation uses the public plugin serverUrl, the directory query, and documented HTTP Basic authentication from OPENCODE_SERVER_PASSWORD / OPENCODE_SERVER_USERNAME (default username opencode). Unreachable or rejected native APIs retain the last-known requests and report the real error through OpenCode's log; they do not guess that input was resolved.
  • The released V2 2.0.18 plugin context has permission.list but does not expose session.form.list. Forms rely on their native reply/cancel and session-deletion events; dropped form events cannot be reconstructed by an invented list API. Current-development question.v2.* events are not this release's contract.
  • Native plugin event streams are live, not a durable replay log. A plugin reload or missed event can lose adapter evidence; persistence on Aether's side cannot reconstruct a callback the native host never delivered.
  • OMP's separate rpc-ui mode and Codex app-server can expose other input APIs, but Aether does not change native CLI hosting mode to use them. Gemini, Copilot, and Cursor inbox hooks remain inbox integrations, not status/input reporters. Default permission-bypass/auto-approve flags suppress many ordinary permission prompts.

Source contracts: Claude command hooks, V1 1.18.32 public SDK endpoints, OpenCode HTTP authentication, V2 2.0.18 form events, V2 released plugin session API, Pi 0.87.1 UI prompt lifecycle, and OMP 18.3.1 extension events.

codex only says when a turn ends. It never says a new one started, so its run comes back to running the way an agent with no reporter does: on agent output or a file change. Everything drawn in the terminal counts there, the echo of your own typing included, so a long prompt typed into a parked codex run can read as running before you send it. claude, pi and omp report both ends, and their runs stay parked until the agent itself says it is working again - a terminal repainting while you type is not work.

Execution and the current pending-request set are persisted separately, so both survive an Aether server restart for a still-live run. The server emits run.input only when the request set changes; it does not replay old input deltas as new opens. Actual run termination or reopen clears the old lifetime's requests. See failure-handling.md.

copilot, gemini, cursor and antigravity have no reporter in Standard mode.

For an agent with a Status of -, nothing changes: the run is judged on silence alone and parks at needs-attention (Needs you) after --stall-threshold with a reason that leads with stalled:. See failure-handling.md.

The following disable or exclude automatic reporting:

  • Background runs. --mode background never gets the reporter: the agent exits when it is done and never waits for anyone. A background run over ACP reports its turn's start from the session host.
  • Enhanced runs. --mode enhanced needs no reporter: the session host reports both ends of every turn itself (enhanced-runs.md).
  • --conflict-coordination=false. Lifecycle reporting is disabled, but the run identity socket, canonical CLI and agent instructions, without report lines, remain available.
  • An argv override. A --agent-definitions entry that redefines a shipped agent drops the status arguments, status environment, and system-prompt instruction mechanism - nothing checks the overridden command is still that CLI.
  • Truthy OPENCODE_PURE in the workspace environment. 1 or case-insensitive true disables OpenCode's external plugins, Aether's included; 0 and false do not. Aether preserves the member's choice. With plugins disabled, the run still launches but reports nothing and is judged on silence like an agent with no reporter.
  • V2 plugin directives. plugins: ["-aether-status"] excludes the managed V2 reporter without excluding the mailbox; ["-*"] excludes both.

Assets are server-written and read-only, staged in /run/aether; managed V2 also receives the run-specific discovery mounts described above. None is written into the worktree or member's persistent configuration. --settings layers over Claude Code's settings; -e adds a pi/OMP status extension; -c overrides Codex's notify for this launch only, so a member's own notify command still works outside that Aether run.

OpenCode config handling depends on the launch. A task-bearing managed V1 launch preserves existing inline fields/plugins and adds its two plugins; V2 keeps that environment value unchanged and uses native discovery. The older reporter-only/taskless profile still reserves OPENCODE_CONFIG_CONTENT, replacing a workspace value rather than merging it. For configuration needed in both paths, use a file named by OPENCODE_CONFIG, which Aether does not set. Do not assume V1 and V2 plugin configuration schemas are interchangeable.

Message delivery

A message a member sends to a Standard run's agent is separate from automated mailbox wake. The latter uses the native APIs above and never falls back to this PTY path.

run.inject writes a message, then the agent's submit sequence, to the run agent's PTY. Most TUIs send the message on one Enter (\r); opencode accepts the text into its editor on the first Enter and sends on the second, so its entry ends the write with \r\r. The sequence lives in the registry (SteerSubmit in internal/harness), not in the caller: the scheduler and the coordination radar both resolve it from the run's agent before writing. Aether records the delivery only after the complete stdin write succeeds and renders the attribution without terminal control bytes.

Only claude has a structured-output adapter today, so its Background runs produce typed tool-call and token events. Everything else degrades to the PTY transcript plus the recorded file changes, which is always enough. Adding an adapter is adapters.md. A background run over ACP has the session item log instead, and the adapter does not read it.

How Aether launches them

--mode standard (wire name tui, the default) runs the agent's native interactive TUI in a persistent server-side PTY: aether attach <run> puts you in it from the CLI, and the dashboard navigates there automatically on launch. Container PID 1 supervises the TUI process and, after any normal agent exit, opens a login shell; exiting that shell opens another. The run and container remain running until an explicit Close, Kill, or Delete. A signal or other non-normal agent failure records failed and cleans up without a replacement shell. --mode background (wire name headless) runs the agent's machine-readable mode as a one-shot: on exit Aether commits and publishes the branch, records completed or failed, and destroys the container immediately. Background runs never open a replacement shell and cannot be reopened. For codex, omp, opencode, copilot, gemini and cursor with their ACP server installed, the one-shot is a single ACP turn instead (enhanced-runs.md). Full-permission flags are applied by default in both - the agent is in a container, and the container is the boundary (security.md).

--mode enhanced (wire name acp) runs the agent over the Agent Client Protocol in the Standard container shape: the supervised PID 1 runs a login shell and the agent's ACP server runs beside it. It is retained on Close and reopens like Standard; see enhanced-runs.md.

The task prompt is optional in Standard and Enhanced mode: launch without one and you land in the agent's interactive TUI with an empty composer, exactly as if you had started the CLI yourself, and type the first prompt there. Every argv token that carries the prompt is then dropped, so opencode --prompt={task} leaves whole rather than dangling an empty flag. Background mode has no interactive surface, so it still requires a task. An Enhanced run without a task waits for the first message.

Every Standard launch of a shipped agent, with or without a task, and every Background launch that does not use ACP carries the run's agent instructions (its role card and the co-author rule) for that launch only:

CLI Mechanism
claude, pi, omp --append-system-prompt "<instructions>"
codex -c developer_instructions="<instructions>", which adds to Codex's built-in instructions. The setting replaces any developer_instructions from config.toml, so Aether puts the top-level value of ~/.codex/config.toml in your Aether home first. Aether does not follow a symlinked or hard-linked config.toml, so its value is replaced too. A value from a Codex profile, a project .codex/config.toml or a CODEX_HOME set in the workspace is not read and is replaced for that launch; put such instructions in AGENTS.md
opencode /run/aether/instructions.md appended to instructions in OPENCODE_CONFIG_CONTENT, after any entries your workspace variable already lists; managed V1 merges it into /run/aether/opencode-native-v1.json
copilot /run/aether/aether.instructions.md, loaded through COPILOT_CUSTOM_INSTRUCTIONS_DIRS=/run/aether after the repository's own instructions; a directory list your workspace variable already sets is kept ahead of it
gemini none: a task-bearing launch carries the instructions after the task, and a taskless one gets none. Gemini puts only ~/.gemini/GEMINI.md in its system prompt, and GEMINI_SYSTEM_MD would replace the whole prompt. Loading /run/aether through --include-directories would make every Standard launch stop on Gemini's "Do you trust the following folders" prompt, because GEMINI_CLI_TRUST_WORKSPACE trusts only the working directory
cursor none: a task-bearing launch carries the instructions after the task, and a taskless one gets none
antigravity none: a task-bearing launch carries the instructions after the task, and a taskless one gets none

The system prompt survives compaction; instructions after the task do not. The task prompt stays exactly what the member typed, as do the stored task, branch slug, and every CLI and dashboard surface. Switching an Enhanced run to Standard carries the instructions to the new launch. Enhanced runs receive them over ACP instead (enhanced-runs.md).

The card tells the agent to run aether-internal skill before acting. skill loads the version-matched assignment, role guidance, and workflow from the staged /usr/local/bin/aether-internal, which reaches the run through /run/aether/coord3.sock. No manual skill install, identity flag, repository instruction, or persistent member-home write is needed.

Custom agents, fake, and argv-overridden shipped agents get no guessed vendor flags. A task-bearing launch gets the instructions appended after the task; a taskless one gets none, so its author must call aether-internal skill manually or through the agent's own startup mechanism. They still get the staged CLI and the run identity socket. Aether does not create permanent repository settings or mutate member configuration. Standalone help works without a socket; standalone skill gives short capability-neutral guidance, never a borrowed run identity.

Agent development workflow

aether-internal status reports the live capability allow-list. aether-internal skill preserves the swarm (aether-internal mission), task, inbox and report workflow and offers skill terminal, skill browser, and native skill git only when the relevant development execution/observation capability is advertised. The Git topic does not claim that GitHub CLI, credentials, push permissions or image tools are installed.

The command families are terminal, browser, control, and artifact. Each operation has command-specific offline help and accepts a typed JSON object via --params-file FILE or --params-file - (stdin); --json is optional because results already use the v3 envelope. Parameters are capped at 48 KiB before/after encoding, control frames at 64 KiB; identity/unknown fields are refused. There is no generic arbitrary RPC, run-ID selector or socket override.

CODE
aether-internal terminal list
aether-internal terminal start --help
printf '%s\n' '{"command":["npm","run","dev"],"name":"app"}' |
  aether-internal terminal start --params-file -
aether-internal skill browser
aether-internal browser status
aether-internal browser open --help
aether-internal artifact list

Edit, run, observe, interact, correct and verify the changed path. A terminal command starts a real owned PTY process without a viewer. terminal output reads history; terminal screen reads the current styled cell grid; terminal screenshot returns a private artifact path. They are not interchangeable. Acquire the exact terminal incarnation's control lease before input/resize/stop, use its returned controller generation and release when finished. Detach/release does not stop the app.

The browser companion is lazy and sandboxed on a standard headless Ubuntu server, without X11, Wayland, Xvfb, a desktop session or host browser. First browser open sends a URL, nonempty control_session_id, generation zero and no session_id; the broker creates the companion and acquires its surface. Retain returned page and control. Later page mutations use the session/page/revision plus controller fence; coordinate input also uses the observed viewport identity. Use browser snapshot for semantic nodes, browser inspect for the element at a viewport point, browser console/network for diagnostics and browser screenshot for an image artifact. See the complete commands and bootstrap contract.

Only claim visible screenshot evidence after an actual image-consuming agent tool reads the returned artifact path. If no such tool exists, report that exact limitation and the text/DOM checks actually performed; text alone is not visual proof. Capture and verify before resource cleanup or a terminal worker success/failure report. Do not automatically upload screenshots publicly.

Use native git and gh, not a second agent Git engine. Inspect the checkout origin separately from the workspace mirror import source; for fork PRs, explicitly choose base repository/branch and head owner/branch. Preserve existing signing/author configuration and coauthors, stage exact intended paths, inspect existing PRs before creating one, and follow assignment approval boundaries instead of automatically merging. See native Git guidance.

Launch argv

--agent Standard (tui) Background (headless)
claude claude --dangerously-skip-permissions {task} claude -p --output-format stream-json --verbose --dangerously-skip-permissions {task}
codex codex --dangerously-bypass-approvals-and-sandbox {task} codex exec --json --dangerously-bypass-approvals-and-sandbox {task}
pi pi {task} pi -p {task}
omp omp --auto-approve {task} omp -p --auto-approve {task}
opencode opencode --prompt={task} opencode run {task}
copilot copilot --allow-all --interactive={task} copilot --allow-all --prompt={task}
gemini gemini --approval-mode=yolo --prompt-interactive={task} gemini --approval-mode=yolo --prompt={task}
cursor agent --force --trust {task} agent -p --force --trust {task}
antigravity agy --dangerously-skip-permissions --prompt-interactive={task} agy --dangerously-skip-permissions --print={task}

Every claude run also gets IS_SANDBOX=1. Runs execute as root on the standard image, and Claude Code refuses --dangerously-skip-permissions as root; that variable is a vendor internal, not a supported interface, and the vendor's own answer is to run the container as a non-root user. It is a stopgap until the standard image ships one. The Environment launches no agent, so it gets none of this.

A managed copilot Standard launch appends --plugin-dir /run/aether to that argv. Every copilot run gets COPILOT_ALLOW_ALL=true and every gemini run GEMINI_CLI_TRUST_WORKSPACE=true. --allow-all leaves Copilot's "Confirm folder trust" dialog up; the variable, set to exactly true, also trusts the working directory. Gemini in a folder it does not trust silently drops --approval-mode=yolo to default and ignores ~/.gemini/.env.

No shipped agent asks for permission by default, in any mode. Enhanced runs start Claude Code in session mode bypassPermissions and Codex in agent-full-access, and run omp acp --auto-approve. Copilot runs with --allow-all, Gemini in approval mode and session mode yolo, Cursor with --force, and Antigravity with --dangerously-skip-permissions. OpenCode has no flag for it, so every opencode launch - Standard, Background, Enhanced and resume - gets {"permission":"allow"} in OPENCODE_CONFIG_CONTENT. The workspace's own value of that variable is merged over it key by key, so a workspace that sets permission keeps its rules; Aether's plugin and instruction overlays are merged over both. pi never asks. TestEveryShippedAgentRunsWithoutPermissionPrompts in internal/harness names each agent's setting.

A model chosen at launch (aether run --model <model>, the Model field in the dashboard) is passed to the agent as is; empty keeps the agent's default. Each agent's flags, read from its CLI help on 2026-10-11 (copilot, gemini and cursor on 2026-10-16, antigravity on 2026-10-17):

--agent Standard, Background, resume Enhanced
claude --model <model> (alias such as opus, sonnet, or a full name) model option, else session/set_model
codex -m, --model <MODEL> on codex, codex exec, codex resume model option, else session/set_model
pi --model <pattern> (provider/id, optional :<thinking>) model option, else session/set_model
omp --model=<value> (fuzzy: opus, gpt-5.2, openai/gpt-5.2) omp acp has no model flag; model option, else session/set_model
opencode -m, --model provider/model on opencode and opencode run, plus model in OPENCODE_CONFIG_CONTENT model in OPENCODE_CONFIG_CONTENT, then the session option
copilot --model <model> model option, else session/set_model
gemini --model <model> model option, else session/set_model
cursor --model <model> model option, else session/set_model
antigravity --model <model> none: no Enhanced mode
custom definition refused model option, else session/set_model
fake refused refused

The flag goes into argv before the task, so a CLI that stops parsing options at the prompt still reads it; a resume command has no task, so the flag is appended. opencode acp has no model flag, so every opencode launch also gets model in OPENCODE_CONFIG_CONTENT, merged after the workspace's variables: a per-run model beats a workspace default. An Enhanced run sets the model over ACP after the session opens; see Enhanced runs, for a custom definition too. A Standard or Background launch of a custom definition or fake has no model flag, and a launch with a model is refused:

CODE
scheduler: agent "custom" has no model flag; launch it without a model

A model that is not a single argument is refused before launch:

CODE
model "x y" must be a single argument: no whitespace or control characters, not starting with "-", at most 128 bytes

Model discovery

agent.models lists the models the installed CLI offers on an account's login, so the dashboard can offer them instead of a free-form field. The server asks the agent itself, in the account's environment terminal (started if needed for your own account), with the account's home as HOME and working directory:

--agent How When it fails
claude Pipes one initialize control request into claude -p --safe-mode --input-format stream-json --output-format stream-json --verbose --no-session-persistence and reads models from the response opus, sonnet, haiku
codex Sends initialize and model/list to codex app-server (25 second limit); hidden models are dropped The models listed in ~/.codex/models_cache.json (visibility list, by priority), else none
every other agent Not discovered None; type the model

The probe passes the same server API-key variables a run gets (the harness's EnvPassthrough names that are set and non-empty in the server's environment).

The executable is the first word of the agent's tui argv, so an --agent-definitions override of claude or codex is the binary that gets probed.

Each model's id is the value a launch passes as the model, in every mode: Claude's aliases and full names and Codex's model slugs are accepted as is by --model and by both Enhanced adapters' model option. The CLI's own default entry is not a model; its description becomes default_label, and an empty model still means the agent's default.

source says where the list came from: agent (the CLI), cache (Codex's own cache file), fallback (Claude's aliases, which every Claude Code version accepts), or none. error carries the failed discovery's error as the CLI or shell printed it, for example:

CODE
codex exited 127: sh: 3: codex: not found

The server keeps each account's list per agent for an hour; refresh discovers again. A failed refresh keeps the last list and reports the new error. Concurrent requests for the same list share one discovery, and a discovery that takes over 30 seconds fails.

On a shared account, discovery runs in the account owner's environment with the owner's login and installation, since that login decides which models a launch can use. It needs the Launch permission and the launch's own checks pass first; otherwise agent.models answers with the launch's refusal. It never starts the owner's environment: if the owner's terminal is not running, nothing runs and the result is the fallback above with an error saying so.

A fallback or none result is cached for a minute, not an hour, so the next request retries discovery. A failed discovery keeps the last list only when it came from agent; a cache result reads models_cache.json again.

These are the vendors' own flags, and vendors rename them and tighten how they combine - claude now refuses --output-format stream-json unless --verbose comes with it. If a launch fails with the CLI rejecting its own arguments, the installed CLI has drifted from the registry. Update the registry or install a compatible CLI in the member's Environment. The installed executable lives in that member's home. An argv override replaces the shipped template wholesale, so a registry fix never reaches it: a deployment's --agent-definitions entry that redefines a shipped agent has to be updated on its own. It keeps the registry's key passthrough, launch env, OpenCode's permission overlay and Enhanced starting mode for that name, since none is part of the command line. It also keeps that agent's model flag, since the override runs the same CLI.

Setting up an agent

Once per person, per agent:

CODE
aether agent add <name>

In either dashboard, Set up on onboarding's Agent step or the Agents page runs three numbered steps. Choose how runs show it compares Standard and Enhanced for that agent and starts on the mode last chosen for it, else Enhanced when enhanced_default is set, else default_mode. Install <agent> calls agent.install with enhanced set when Enhanced is chosen, then shows the command's output, and on failure Install failed with the exit status. Once the agent is installed the Environment terminal opens with its login command typed (claude then /login, codex login, pi then /login, omp, opencode auth login, copilot login --device-code, NO_BROWSER=true gemini, NO_OPEN_BROWSER=1 agent login). Check reads agent.list again and shows Installed, Enhanced installed and Login found or No login found, from installed, enhanced_installed and login_found; a custom agent with no login path shows only Installed. A gateway without agent.install types the install command into the terminal instead. In the CLI, run:

CODE
aether terminal

Install the executable into ~/.local/bin, complete the vendor login in that terminal, and return to the dashboard. The login and executable are in your member home, so every container for that member sees them. A member-defined name also records a launch definition under that member; Add agent… registers one with a Standard, a Background and an optional Enhanced command, an optional Enhanced start mode and an optional login file.

The Agents page shows Installed or Not installed and Login found or No login found for your account. These checks look for the executable and the login file; the agent verifies its vendor login when it starts. Shipped agents need no separate registration record. Sharing your account lends other members your login, and your installation to a recipient who has none of their own; a recipient need not install the agent (teams.md).

agent.list with account_member_id set to a shared account reports the caller's own agents, installed when the executable is in the caller's ~/.local/bin or the account owner's, and says why a launch of one on that account would be refused, resolving the name as a launch does: login_missing: true when the account owner has no login at its Login state path (a missing or empty file), own_account_only: true when it resolves to the caller's own member-defined agent, which runs only on the caller's own account, and unavailable with the launch's own error when the login is there but cannot be shared (a symlink in its path, a file with another hard link, or neither a file nor a directory). At most one is set. A member-defined name that is also a server-wide definition resolves to the server-wide one, so it can report login_missing. The launch dialog does not offer a refused agent on that account, and the server refuses such a launch.

aether agent add <name> --enhanced also installs a shipped agent's enhanced-mode adapter.

For an unshipped name the command takes the Standard and Background templates from --standard <argv> and --background <argv>, or prompts Standard command [<name> {task}]: and Background command [<name> -p {task}]:; --acp <argv> records the command that serves ACP, split on spaces; a shipped name refuses it. Install the executable into ~/.local/bin using the vendor's documented procedure, then complete its login.

The Environment has no browser. Open the URL it prints in your own browser. In the dashboard, clicking an OAuth URL with a loopback redirect starts the matching callback forward before the authorization page opens. With the CLI, start it explicitly before completing the browser flow:

CODE
aether forward terminal <callback-port>

Device-code flows do not need a callback forward.

Setup details

The login commands below run in the Environment. Three things to know:

  • There is no browser in the container. Open the printed URL on the machine running aether gui or the CLI.
  • Logins belong to one member account. They reach another member's run only through the account owner's explicit grant described in teams.md, and then only the Login state path.
  • If you skip the login part, the agent's own login prompt simply appears in the run's PTY. Attach with aether attach <run> and complete it there; it persists the same way.

Claude Code

Inside aether terminal, start the CLI and use its /login slash command, which prints a URL to open in your own browser and takes a code back. /status shows which credential is active. Credentials land in ~/.claude/.credentials.json and remain in the member's persistent home. aether config push never uploads known credential names. Once you share your account, /logout in your own containers revokes the login but leaves the file in place; see security.md.

For an API key instead of a subscription, set ANTHROPIC_API_KEY in the server's environment (/etc/aether/aether-server.env with the shipped systemd unit) and skip setup entirely. Note that Aether passes through ANTHROPIC_API_KEY only; a CLAUDE_CODE_OAUTH_TOKEN from claude setup-token is not in the passthrough list, so use the terminal for subscription auth.

Codex

Inside aether terminal, run the CLI's login command and choose the device-code option. Codex writes auth.json under ~/.codex, which remains in your member home. aether config push never uploads known credential names.

Your first Standard run opens Codex's Hooks need review screen for Aether's inbox hooks: choose Trust all and continue. See Codex inbox integration.

OPENAI_API_KEY in the server environment is the API-key alternative.

pi

Inside aether terminal, start the CLI and use its /login command to pick a provider. Tokens land in ~/.pi/agent/auth.json under the member's home. aether config push never uploads known credential names. OPENAI_API_KEY in the server environment is the API-key alternative.

omp

oh-my-pi is a fork of pi with its own executable and its own home. Install it with the vendor's command, curl -fsSL https://omp.sh/install | sh, which puts omp in ~/.local/bin. Inside aether terminal, start the CLI and log in through its own flow; credentials land in the agent database under ~/.omp/agent/, which is excluded from configuration uploads. ANTHROPIC_API_KEY or OPENAI_API_KEY in the server environment is the API-key alternative.

omp is a shipped name, and a shipped name always wins over a member's own definition of the same name. If you ran aether agent add omp before Aether shipped it, your stored definition is ignored from now on and runs use the launch template in the table above. aether agent list skips the stored row, which stays in the server's database - there is no command that removes one. To launch your own build, register it under a name Aether does not ship.

opencode

Inside aether terminal, run opencode auth login and pick your provider. Credentials are written to ~/.local/share/opencode/auth.json in the member home.

aether config, Files, and the manual aether profile commands use ~/.config/opencode, the default native configuration home for the V1 and V2 targets above. OpenCode loads settings and plugins there, not from its login directory. Aether's configuration root is fixed relative to the member home; it does not follow XDG_CONFIG_HOME, OPENCODE_CONFIG_DIR, or OPENCODE_CONFIG overrides. Native custom-root consumption requires separate verification; V2 OPENCODE_CONFIG_DIR relocation remains unverified.

If an earlier import wrote to ~/.local/share/opencode, upload the configuration again with aether config push opencode <path>.... This writes ~/.config/opencode without moving or deleting old files. Do not copy the data directory wholesale or move/delete ~/.local/share/opencode/auth.json; native login and account sharing still use that file.

Copilot, Gemini and Cursor

copilot, gemini and cursor are Enhanced by default (enhanced_default), and a Background run of each uses ACP once its server is installed. In Standard mode they lose three things:

  • No status reporter. A run's working or waiting state is inferred from silence alone (Status reporting).
  • No automatic inbox hooks. Mail arrives only through explicit aether-internal inbox reads, unless you install the exported hook file by hand: Copilot, Gemini or Cursor.
  • No discovery hint on a taskless launch. A task-bearing launch carries the run instructions in its prompt.

An Enhanced run has full state and the server's mail wake (enhanced-runs.md). The three are shipped names, so a definition you stored with aether agent add under the same name is ignored, as described under omp. None has a configuration root, so aether config push has nothing to upload for them.

GitHub Copilot CLI

Install it into ~/.local from aether terminal; it needs npm in the Environment:

CODE
npm install -g --prefix "$HOME/.local" @github/copilot

Log in with copilot login --device-code, open the printed URL in your own browser and enter the code. A container has no system keychain, so Copilot asks Store token in plaintext config file? (y/N); answer y, and the token lands in ~/.copilot/config.json. Copilot also writes that file on its first start, before any login, so Login found for copilot only means Copilot has started once.

Copilot replaces config.json by rename, like Claude Code its login, but Aether does not pin it the way it pins Claude's (security.md): whether Copilot can write the file when it is a mount is unverified. A recipient's run on a shared account keeps the copy of the owner's file it started with.

COPILOT_GITHUB_TOKEN in the server environment is the API-key alternative, and takes precedence over a stored login. Copilot accepts fine-grained personal access tokens with the "Copilot Requests" permission, not classic ghp_ tokens.

Runs set COPILOT_ALLOW_ALL=true, which trusts the working directory, so Copilot loads that directory's skills, plugins, MCP servers and hooks. Enhanced runs start copilot --acp --allow-all; Copilot's changelog says a baseline granted by --allow-all launch flags stays even when an ACP client turns allow-all off, so a run cannot take those permissions back.

Gemini CLI

Since 2026-06-18 Gemini CLI serves only paid Gemini API keys and Gemini Code Assist Standard or Enterprise. Google moved Google AI Pro, Ultra and free personal accounts to Antigravity CLI (announcement).

Install it from aether terminal; it needs npm in the Environment:

CODE
npm install -g --prefix "$HOME/.local" @google/gemini-cli

With an API key, put GEMINI_API_KEY=<key> in ~/.gemini/.env from aether terminal, or set GEMINI_API_KEY in the server environment. Gemini reads only the first .env it finds walking up from the run's directory, so a .env in the checkout or above it is read instead of ~/.gemini/.env.

With Code Assist Standard or Enterprise, run NO_BROWSER=true gemini, choose Sign in with Google, open the printed URL and paste the code back. Also put GOOGLE_CLOUD_PROJECT=<project> in ~/.gemini/.env. The login lands in ~/.gemini/oauth_creds.json. Gemini replaces that file by rename, so once you share your account Aether mounts it in place in your own containers (security.md).

Runs set GEMINI_CLI_TRUST_WORKSPACE=true; see Launch argv. Gemini's ACP session modes are default, autoEdit, yolo and plan. An Enhanced run starts in yolo, and the mode stays switchable in the run.

Cursor CLI

Cursor's CLI runs as agent (also linked as cursor-agent). Its name in Aether is cursor: pick that at launch. AETHER_HARNESS and the inbox hooks key on it too.

Install it from aether terminal:

CODE
curl -fsS https://cursor.com/install | bash

The installer links ~/.local/bin/agent by absolute path to ~/.local/share/cursor-agent/versions/<version>, so a shared account borrows that directory read-only along with the executable.

Log in with NO_OPEN_BROWSER=1 agent login and open the printed URL; it writes ~/.config/cursor/auth.json in place. CURSOR_API_KEY in the server environment is the API-key alternative.

Cursor needs no launch env: --force approves everything in agent mode. Its ACP modes are agent, plan and ask, and Aether sets none. In an Enhanced run, a Cursor team whose admin enables auto-run controls without "Run Everything" overrides --force, and the agent asks again.

Antigravity CLI

Google moved personal Gemini CLI users to Antigravity CLI on 2026-06-18 (announcement). Its executable is agy; its name in Aether is antigravity: pick that at launch. AETHER_HARNESS keys on it too.

Install it from aether terminal; the script puts a plain file at ~/.local/bin/agy:

CODE
curl -fsSL https://antigravity.google/cli/install.sh | bash

agy has no login subcommand. Run agy, choose Google OAuth or a Google Cloud project, open the printed URL and paste the authorization code back; no browser is needed in the container. The login lands in ~/.gemini/antigravity-cli/antigravity-oauth-token.

For an API key, set GEMINI_API_KEY in the server environment or the Environment, and put "modelProvider":"gemini" in ~/.gemini/antigravity-cli/settings.json. For Google Cloud application default credentials, set AGY_ADC_AUTH=true.

Standard and Background launches pass --dangerously-skip-permissions, so the agent never asks. In Standard mode it has no status reporter and no packaged inbox hooks (see Inbox integrations), and a taskless launch carries no run instructions.

Antigravity has no Enhanced mode, so agent.list reports enhanced: none and an Enhanced launch is refused:

  • agy 1.4.0 has no ACP mode: agy help acp answers "unknown subcommand: acp".
  • Google's ACP server is a separate package, antigravity-acp. It is a 336 MB download that unpacks to 1.06 GB, and keeps its own login in ~/.gemini/antigravity-acp/acp_token.json instead of reusing agy's.
  • Its Google sign-in redirects to http://127.0.0.1:<random port>/ and has no paste-code fallback, so it cannot finish inside a run container. Only a Gemini API key or a Google Cloud login works there, after a manual settings.json edit.

Not verified: the agy binary contains a "Do you trust the contents of this project?" dialog, and whether --dangerously-skip-permissions skips it needs a logged-in check. Whether agy rewrites its token file by rename is also unverified, so Aether does not pin it.

fake

The deterministic test agent. It has no login and no fixed command: the server reads its argv from the AETHER_FAKE_AGENT environment variable at launch time, so it runs whatever you name - typically a script committed to the repo, since the run's checkout is mounted at /workspace.

CODE
AETHER_FAKE_AGENT="sh /workspace/agent.sh {task}" aether-server serve --data-dir /var/lib/aether

The {task} placeholder expands to the run's task text; omit it if the script does not need it.

This is how the quickstart proves the whole lifecycle without any vendor account, and it is the agent the end-to-end tests drive.

Custom agents

Custom launch definitions come from two places, resolved in this order:

  1. Server configuration (administrator): --agent-definitions or the AETHER_AGENT_DEFINITIONS environment variable. These pin a name for every member and always win.
  2. Member registration: aether agent add <name> stores a definition scoped to the registering member, over the normal control channel; no server restart. A member's definition shapes argv only inside that member's own containers and never affects anyone else. Shipped names and the reserved names custom and fake cannot be registered. If a stored row later shares a name with a shipped agent, agent.list skips the row and a launch uses the shipped agent.

A member's own definition runs only on that member's own account. On a shared account, only a shipped agent or a server-wide definition is launched, and its CredentialPaths are the login paths the share mounts from the owner's home. A launch is refused when a credential path is the home itself. A definition declares no install directories, so a recipient without its executable borrows only the owner's ~/.local/bin and ~/.local/lib.

The name need not equal the command. aether agent add mycursor --standard "agent {task}" registers mycursor with executable agent, the first word of the Standard command, and a Background default of agent -p {task}. The name is what you choose at launch and what coordination (AETHER_HARNESS, inbox hooks, swarm choices) keys on; the executable is what runs. Inbox hooks ship for a fixed set of names, so a differently named agent gets none and aether-internal skill says no packaged integration.

aether agent add also takes --acp-mode <id> and a repeatable --login <path>, such as --login ~/.cursor/auth.json. A ~/ path is stored as /root/...; when your shell expands ~ first, the CLI turns a path under your local home back into ~/. With a login path, agent.list reports login_found; without one, setup never shows the agent as logged in.

Both forms carry the same fields and pass the same validation. The administrator JSON is an object keyed by agent name. Each definition must name the executable and provide both Standard (TUIArgs) and Background (HeadlessArgs) argv. {task} is replaced as one argv value, never passed through a shell. Profile and credential paths are explicit absolute container paths under /root or /home/aether; credentials must be inside the profile root when one is configured. Deny names are basenames only. A definition under a name Aether does not ship has no model flag, so a Standard or Background launch of it with a model is refused; an Enhanced launch sets the model over ACP.

For example, an administrator can point omp at a different build for every member. A shipped name is the one case a member cannot register themselves, so an administrator definition is the only way to change one. A definition replaces the shipped definition rather than extending it, so it carries the deny names too - omp keeps its provider keys in agent.db, which no generic denylist knows about:

CODE
{
  "omp": {
    "Name": "omp",
    "TUIArgs": ["omp", "--auto-approve", "{task}"],
    "HeadlessArgs": ["omp", "-p", "--auto-approve", "{task}"],
    "Executable": "omp",
    "ProfileRoot": "/home/aether/.omp",
    "CredentialPaths": ["/home/aether/.omp/agent"],
    "DenyNames": ["agent.db", "agent.db-wal", "agent.db-shm"]
  }
}

The server validates that the executable is a name rather than a host path, that argv starts with that executable, and that profile, credential, and deny-name policies are safe. An invalid administrator definition rejects server startup; an invalid member registration is refused at the RPC. Agent installation, login state, configuration uploads, and launch definitions remain separate concerns: installation and login state live in the member home, aether config push writes selected configuration there, and the definition resolves argv for that member. The terminal is the only setup transport for installation and login.

Updates before launch

Before it launches a run, the server updates the shipped agent installed in the member home's ~/.local/bin. It runs the agent's own update command in a short-lived container with the run's image, user, environment, and member home. The first launch per member home and agent checks for an update; after that, a launch checks again once 6 hours have passed, or 15 minutes after a failed update. Concurrent launches on one home, such as a swarm, share one update.

Agent Update command
claude claude update
codex reads the latest version with npm view @openai/codex version; when codex --version differs, runs npm install -g --prefix <stage> "@openai/codex@<version>"
pi reads the latest version with npm view @earendil-works/pi-coding-agent version; when pi --version differs, runs npm install -g --prefix <stage> --ignore-scripts "@earendil-works/pi-coding-agent@<version>"
omp omp update
copilot reads the latest version with npm view @github/copilot version; when copilot --version differs, runs npm install -g --prefix <stage> "@github/copilot@<version>"
gemini reads the latest version with npm view @google/gemini-cli version; when gemini --version differs, runs npm install -g --prefix <stage> "@google/gemini-cli@<version>"
cursor agent update, which installs the new version under ~/.local/share/cursor-agent/versions and prints Already up to date when current
antigravity agy update, which exits 0 when current

For claude, codex, and pi, an installed enhanced-mode adapter is then brought to its pinned version when ~/.local/lib/node_modules/<package>/package.json names another.

For codex, pi, copilot and gemini, npm installs the new version into a stage directory beside the old one in ~/.local/lib. Aether resolves its server executable path before passing the read-only mount to Docker, so /proc/self/exe cannot select Docker's executable. That helper exchanges the installed and staged package directories with Linux's atomic directory-exchange operation: the installed path is never removed. If the filesystem rejects the exchange, the update reports that error and leaves the installed package in place.

Their own updaters are not used: codex update installs into the image's global npm prefix, outside the member home, and pi update --self replaces files in place. A codex, pi, copilot or gemini in ~/.local/bin that npm did not install is not updated, and the update reports, for example, codex in ~/.local/bin was not installed with npm, so Aether cannot update it.

Nothing else is touched: no agent configuration, plugins, extensions, or release channel. These are never updated:

  • an agent defined by a member, or a shipped name an administrator overrides with --agent-definitions;
  • opencode, because an upgrade can cross a major version that the managed OpenCode wrapper refuses (see Managed native loading);
  • an agent installed in the image rather than in ~/.local/bin of the member home;
  • an account owner's installation a recipient's run borrows: it is mounted read-only, and the owner's own launches update it.

A launch waits at most 25 seconds for the update. If it is still running, the agent starts on whatever is installed at that moment, the update finishes in the background, and the run's timeline in the dashboard shows:

CODE
starting the installed version 2.1.288 (Claude Code) while claude updates

The update's result appears on the timeline of the run that started it, also when it finishes after the agent started. A new version reads updated <agent> from <old> to <new>, or to an unknown version when the new --version cannot be read; an unchanged one adds nothing. A failure, including a vendor release server that cannot be reached, never stops a launch; the timeline shows the real cause:

CODE
could not update claude from 2.1.288 (Claude Code): the updater exited 1: <updater output>

An updater still running after 10 minutes is stopped and reported as the updater did not finish within 10m0s. Killing the run or closing the dashboard does not stop an update.

A newer CLI may migrate its own state in the member home on first start, and Aether does not roll an update back. Reopening a retained run reuses its container and does not update. omp and Claude Code keep the files of previous versions in the member home; Aether does not prune them.

To turn updates off for the whole server, start it with aether-server serve --agent-update=false.

Agent configuration: import and Files

Upload agent configuration covers the prompt under Agents > Agent config files and the aether config commands a local agent runs with it. This section covers what an upload and a Files edit change on the server.

aether config push reads each agent's credential_names and runtime_ignores from config.roots and leaves those paths out before reading them. The server enforces the same names, the runtime paths, .aether-profile-ignore, and the secret scanner again on every config.import request. Rules inside .aether-profile-ignore apply only to aether profile push.

aether config ls prints each agent's runtime paths. Beyond transcripts, history, and caches, they cover what the agent's own account sync, plugin install, or package manager recreates on the server: Claude Code's plugins/cache/, plugins/marketplaces/, plugins/synced/, and skills/synced/; Codex's plugins/cache/, skills/.system/, and root-level *.sqlite state databases; OpenCode's node_modules/ and .git/. Reinstall those plugins and packages on the server instead. aether profile push still carries them.

For oh-my-pi, the runtime policy excludes:

Root-relative paths in ~/.omp Contents
stats.db, stats.db-wal, stats.db-shm Usage statistics derived from session logs
agent/history.db, agent/history.db-wal, agent/history.db-shm Prompt history
agent/models.db, agent/models.db-wal, agent/models.db-shm Model/provider cache
agent/sessions/, agent/terminal-sessions/, agent/cache/, agent/blobs/ Session history, cache, and blobs
natives/, cache/, logs/, run/, collab/, puppeteer/, webcache/, wt/, remote-host/, ssh-control/ Downloaded runtime components, caches, logs, worktrees, and process state

These names follow OMP's statistics storage and path definitions. The policy does not exclude all databases or dependency directories: agent/config.yml, MCP settings, agent/skills/, agent/extensions/, and agent/npm/ remain eligible, including an extension's own stats.db. Empty files and binary assets are preserved.

For a required asset larger than 64 MiB, open your persistent member-home shell:

CODE
aether terminal

Install or download the asset there into the agent's directory, following the asset's installation instructions. The manual profile push command is not a large-file alternative: it allows 1 MiB per file and 20 MiB per snapshot.

Uploaded files are written into your authenticated member's persistent configuration home. That home is mounted read-write in your Environment and in runs you launch, so the change is immediately visible to existing and future runs (an agent may need to reload its configuration). It is not an isolated per-run profile. A share of your account does not expose it, except the ~/.omp/agent directory an omp share mounts: a recipient's run uses the recipient's configuration. A snapshot pin records launch provenance, not an isolated writable copy or a promise that home changes wait for later runs. Changing configuration does not rebuild the installed-agent image.

After an upload, open Files and unfold Home to browse your whole home, the upload included, alongside workspace base and live-run files (environment-home.md). The editor supports JSON, JavaScript, TypeScript, Markdown, Python, and TOML syntax highlighting, plus find/replace. Edits stay as in-memory dirty tabs while you navigate. Save explicitly with Save, Commit to <branch>, or Ctrl/Cmd-S; there is no autosave or force-save. Browser navigation warns before unloading dirty buffers.

Configuration files are complete UTF-8 text up to 64 MiB. Binary and oversized files are read-only. A new file never overwrites an existing one. A later push does not replace an already open editor buffer. Use Reload from server when you want current remote bytes, including after a stale save; it deliberately discards the local draft. Every config.* and home.* method requires Launch and addresses only the authenticated member's own home; an admin cannot select another member.

For workspace files, Commit to <branch> makes a one-file commit on the workspace base branch and does not push upstream. Live-run writes change the run's uncommitted checkout. Workspace saves require Push; live-run saves require the Steer capability. The Files protocol defines revision and concurrency rules.

Manual profile commands

The explicit CLI profile surface remains available for operators who need content-addressed snapshots or rollback:

CODE
aether profile push --agent claude
aether profile status --agent claude
aether profile rollback --agent claude <snapshot-id>

profile push is manual; it is not run by a local watcher. Its exact optional flags are repeatable --skip-secret <file> and --allow-secret <file>. The latter requires an explicit --workspace <workspace> for audit attribution:

CODE
aether profile push --agent claude --skip-secret <file>
aether profile push --agent claude --allow-secret <file> --workspace <workspace>

profile push reads the selected agent's local configuration root and records a content-addressed snapshot; status reports recorded snapshot metadata, not a live inventory of the home. aether config push and Files edits do not create or update this CLI snapshot history. Configuration persists in the member HOME even when no snapshot exists.

CLI built-in runtime exclusions match from the profile root. .aether-profile-ignore keeps gitignore syntax and can reinclude runtime paths with !, but cannot reinclude credential names. These overrides do not apply to aether config push.

Manual push and rollback overlay the snapshot's files into that same shared HOME, making those writes visible to the member's active and future runs. They do not create isolated per-run copies. Rollback is not an exact-tree restore: files absent from the chosen snapshot are not deleted. A run's snapshot pin is optional launch provenance, not a guarantee that its current home still matches those bytes. No path automatically synchronizes later local changes.

Adding an agent

The registry defines argv templates for both modes, credential/configuration roots, denylist, API-key passthrough, system-prompt instructions (InstructionArgs/InstructionEnv), status reporting, the install command (InstallScript), and the optional pre-launch update command (UpdateScript), which must be a cheap no-op when the CLI is current and must update only the program. An output adapter is optional; see adapters.md. Inbox support is a separate capability: follow the harness integration guide for the durable CLI, boundary adapter, and optional native receiver. Only a version-matched native implementation should opt a shipped agent into managed mailbox loading; a custom argv override must not inherit that assumption.