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:
- 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 terminalor 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. - 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:
npm install -g --prefix "$HOME/.local" @agentclientprotocol/claude-agent-acp@0.86.0which 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:
aether agent add claude --enhancedtypes 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, recordscompletedorfailed, and destroys the container immediately. It never opens a replacement shell and can never be reopened.codex,omp,opencode,copilot,geminiandcursorrun their background task over ACP instead when their ACP server is installed, and finish the same way after its one turn; Claude keepsclaude -pbecause its adapter runs on the Claude Agent SDK (enhanced-runs.md).
Close a Standard or Enhanced run explicitly:
aether close <run> --outcome merged
aether close <run> --outcome abandonedClose 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:
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:
aether-internal skill --hooks
aether-internal hook file
aether-internal hook file claude.jsonFor 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:
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:
/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.jsonmerges the workspace's inline config with file URLs for/run/aether/opencode-v1.jsand/run/aether/opencode-status.jsand with/run/aether/instructions.mdunderinstructions, preserving unrelated fields and existing entries. The wrapper supplies it throughOPENCODE_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 stagesopencode-v2.jsandopencode-status-v2.jsin/run/aetherand mounts them read-only at/.opencode/plugins/aether-mailbox-<run-id>/index.jsand/.opencode/plugins/aether-status-<run-id>/index.js. Native ancestor discovery loads these packages before user enable/disable directives.OPENCODE_CONFIG_CONTENTremains unchanged. The stable plugin IDs areaether-mailboxandaether-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:
{"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 nextStop,/clear, a resume) replaces it. The inboxUserPromptSubmitandPostToolBatchhooks delete the file, so a busy session has none; a replaced waiter exits within a second. Esc fires noStop, 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 asStop hook blocking error from command "Stop": .... That wording is Claude Code's framing, not an error. - On
SessionStartall unread mail counts as unannounced, so a resumed idle session with mail waiting wakes at once. OnStopthe 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.
/exitand 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:
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.tsOpenCode's reporter-only/taskless profile uses V1's
OPENCODE_CONFIG_CONTENT plugin mechanism:
OPENCODE_CONFIG_CONTENT='{"plugin":["file:///run/aether/opencode-status.js"]}' opencodeTask-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:
/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 V2Stateful 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
PermissionRequestexplicitly omitstool_use_id, andNotificationpermission/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, thedirectoryquery, and documented HTTP Basic authentication fromOPENCODE_SERVER_PASSWORD/OPENCODE_SERVER_USERNAME(default usernameopencode). 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.listbut does not exposesession.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-developmentquestion.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-uimode 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 backgroundnever 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 enhancedneeds 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-definitionsentry 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_PUREin the workspace environment.1or case-insensitivetruedisables OpenCode's external plugins, Aether's included;0andfalsedo 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.
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 listEdit, 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:
scheduler: agent "custom" has no model flag; launch it without a modelA model that is not a single argument is refused before launch:
model "x y" must be a single argument: no whitespace or control characters, not starting with "-", at most 128 bytesModel 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:
codex exited 127: sh: 3: codex: not foundThe 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:
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:
aether terminalInstall 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:
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 guior 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 inboxreads, 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:
npm install -g --prefix "$HOME/.local" @github/copilotLog 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:
npm install -g --prefix "$HOME/.local" @google/gemini-cliWith 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:
curl -fsS https://cursor.com/install | bashThe 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:
curl -fsSL https://antigravity.google/cli/install.sh | bashagy 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:
agy1.4.0 has no ACP mode:agy help acpanswers "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.jsoninstead of reusingagy'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 manualsettings.jsonedit.
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.
AETHER_FAKE_AGENT="sh /workspace/agent.sh {task}" aether-server serve --data-dir /var/lib/aetherThe {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:
- Server configuration (administrator):
--agent-definitionsor theAETHER_AGENT_DEFINITIONSenvironment variable. These pin a name for every member and always win. - 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 namescustomandfakecannot be registered. If a stored row later shares a name with a shipped agent,agent.listskips 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:
{
"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/binof 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:
starting the installed version 2.1.288 (Claude Code) while claude updatesThe 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:
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:
aether terminalInstall 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:
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:
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.