A harness is one agent CLI and everything Aether needs to know to launch
it: how to start it in interactive and headless mode, where its login state
lives, where its configuration lives, and which environment variables carry an
API key. The registry is internal/harness, a map and a few functions - not a
plugin system.
Two rules shape everything below:
- Aether does not install agents for you. A member runs the displayed
vendor install command in their environment terminal. The command should
install the executable into
~/.local/bin. - 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 that whole home into a recipient's run. 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, fake, and deployment
custom harnesses 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 terminal; 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.
Shipped harnesses
--agent |
CLI | Login state | Configuration root | API key env | Launch env | Status | Steering | Env setup |
|---|---|---|---|---|---|---|---|---|
claude |
Claude Code | ~/.claude |
~/.claude |
ANTHROPIC_API_KEY |
IS_SANDBOX=1 |
hooks (--settings) |
PTY | yes |
codex |
OpenAI Codex CLI | ~/.codex |
~/.codex |
OPENAI_API_KEY |
- | notify (-c notify=[...]) |
PTY | yes |
pi |
pi | ~/.pi |
~/.pi |
ANTHROPIC_API_KEY, OPENAI_API_KEY |
- | extension (-e) |
PTY | yes |
omp |
oh-my-pi | ~/.omp |
~/.omp |
ANTHROPIC_API_KEY, OPENAI_API_KEY |
- | extension (-e) |
PTY | no |
opencode |
opencode | ~/.local/share/opencode |
~/.local/share/opencode |
ANTHROPIC_API_KEY, OPENAI_API_KEY |
- | plugin (OPENCODE_CONFIG_CONTENT) |
HTTP TUI API | no |
fake |
a script you name | - | - | - | - | - | PTY | no |
custom |
deployment-supplied | - | - | - | - | - | PTY | no |
Paths are inside the run container, relative to the run user's home (/root,
or /home/aether for a non-root image user).
The config.roots response used by the dashboard carries a runtime_ignores
list for each configuration root. These are root-relative paths, and the
browser applies them before reading selected file bytes with case-sensitive
exact or component-prefix matching; trailing slashes are presentation-only.
This policy is destination-specific: a directory whose basename is renamed or
ambiguous must be assigned to a destination before the import preview can be
read. Credential names remain globally excluded, independent of this runtime
list.
The Env setup column marks harnesses that can participate in agent setup:
the dashboard can open the member's environment terminal for installation and
login. Exactly claude, codex, and pi qualify; everything else stays
launchable for runs but is not offered in that setup flow.
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. CLI availability does not grant
run identity or enable disabled coordination.
Staging failure refuses creation rather than silently omitting the CLI.
No harness receives an automatic Aether MCP registration flag or config.
Supported harnesses receive a short native per-launch discovery hint;
aether-internal skill loads live assignment-specific guidance. Containers
without run identity receive only general guidance, not borrowed authority.
The startup switches follow the vendor references: Claude CLI reference, Codex configuration reference, pi CLI reference, omp CLI reference, and OpenCode configuration. 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 the launch table below for why claude needs one. A reporter that
rides in the environment rather than on the command line (opencode) is
not in this column: it is set on interactive runs alone and is dropped by
the same things that drop the reporter, so it lives under "Status
reporting".
Either way, a variable the server sets itself replaces a workspace environment variable of the same name rather than merging with it.
- TUI. Container PID 1 supervises the harness and opens a login shell after
any normal harness exit. Exiting that shell opens another, so the run and
container remain
runninguntil an explicit Close, Kill, or Delete. - Headless. The harness is the container's main process. When it exits,
Aether commits and publishes the branch, records
completedorfailed, and destroys the container immediately. It never opens a replacement shell and is never relaunchable.
Close a TUI run explicitly:
aether close <run> --outcome merged
aether close <run> --outcome abandoned
Close pauses the container and retains the exact container, checkout, run row,
member account, and coordination surfaces for --run-container-ttl. The
default is 1h; 0 uses that default and a negative value disables retention
and cleans up immediately. Kill and Delete remain immediate cleanup operations.
Relaunch is available only for an explicitly closed, retained TUI run while its retention deadline has not passed:
aether relaunch <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 relaunch. 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.
Status reporting
Needs you means the agent is waiting for you, or the run stalled. The first half comes from the agent itself.
A harness 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 interactive launch of each is:
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"]'
pi "<task>" -e /run/aether/status.ts
omp --auto-approve "<task>" -e /run/aether/status.ts
opencode has no flag for a plugin, so its launch command is untouched and
the plugin is named in the environment:
OPENCODE_CONFIG_CONTENT={"plugin":["file:///run/aether/opencode-status.js"]}
opencode --prompt="<task>"
Every one of them ends up running the same command inside the container:
/opt/aether/aether-server report claude # hook event JSON on stdin
/opt/aether/aether-server report codex '<payload>' # the notify argument
/opt/aether/aether-server report pi --event <name> # from the extension, pi and omp
/opt/aether/aether-server report opencode --event session.idle
The report travels back over the run's own coordination socket, so no token enters the container and nothing new is mounted. The server turns it into a run status straight away:
| The agent says | The run becomes | Reason shown |
|---|---|---|
| the turn ended, or it has been idle at its prompt | needs-attention |
waiting for your input |
| it is asking permission | needs-attention |
waiting for your permission |
| it is asking a question | needs-attention |
waiting for your answer |
| it started a turn, ran a tool, or got its answer | running |
agent resumed |
Anything else the harness reports - a session opening, a reply streaming
in, a compaction - is ignored rather than guessed at, and so is a subagent's
own turn: opencode gives one a session of its own, and that session going
idle is not the run's turn ending. The rule holds the other way round too.
The run is running while any of its sessions is, so an opencode
background subagent still working after the turn that spawned it ended
keeps the run off your queue until it finishes - something there is still
working.
opencode never announces the resume after a permission or a question of its
own accord - its session stays busy for the whole tool call the prompt
interrupted - so the member's answer is what returns the run to running.
A run can have several prompts open at once, one per session, and only the
answer to the last of them returns it: until then the run stays parked. If
the turn that asked ended while the prompt was still open, that answer
parks the run at needs-attention instead - nothing is working any more -
and the next turn the agent starts is what returns it to running.
codex only says when a turn ends. It never says a new one started, so its
run comes back to running the way a harness 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 TUI repainting while you type is not
work.
The last report is recorded with the run, so it survives a server restart: a run the agent parked comes back parked, and only what would have released it before releases it now. See failure-handling.md.
For a harness with a Status of -, nothing changes: the run is judged
on silence alone and parks at needs-attention after --stall-threshold
with a reason that leads with stalled:. See
failure-handling.md.
Four things turn the reporter off:
- Headless runs.
--mode headlessnever gets the reporter: the agent exits when it is done and never waits for anyone. --conflict-coordination=false. There are no mounts, so there is no socket to report on and no directory to write the assets into.- An argv override. A
--harness-definitionsentry that redefines a shipped harness drops the status arguments, status environment, and taskless discovery mechanism - nothing checks the overridden command is still that CLI. OPENCODE_PUREin the workspace environment. opencode loads no external plugin at all when that variable is set, Aether's included, and Aether does not take it away from you. The run launches and works normally; it reports nothing, and is judged on silence like a harness with no reporter.
The asset files are server-written, read-only, and live in /run/aether,
never in the worktree or the member's synced profile. Each applies for that
launch alone and merges over what the member already has: --settings
layers one settings document over Claude Code's own, -e loads one more pi
extension beside the ones you already have, -c overrides your
~/.codex/config.toml notify for this run alone - so if you use notify
for something of your own, it keeps working everywhere except in an Aether
run - and OPENCODE_CONFIG_CONTENT is merged into opencode's config with
the plugin lists concatenated, so the member's own plugins still load.
What merges is opencode's own config, not a second value of that variable:
an interactive opencode run reserves OPENCODE_CONFIG_CONTENT for the
plugin, and a workspace environment variable of that name is replaced
rather than combined. Inline config a workspace needs on every run goes in
a file the workspace names with OPENCODE_CONFIG, which Aether never
sets.
Steering delivery
run.inject writes a message, then the harness's submit sequence, to the
run agent's PTY. Most TUIs send the message on one Enter (\r); opencode
accepts steered text into its editor on the first Enter and sends on the
second, so its profile ends the write with \r\r. The sequence lives in the
harness profile (SteerSubmit in internal/harness), not in the caller:
the scheduler and the coordination radar both resolve it from the run's
harness 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 headless runs
produce typed tool-call and token events. Everything else degrades to the PTY
transcript plus the diff timeline, which is always enough. Adding an adapter is
adapters.md.
How Aether launches them
--mode 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 harness 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 harness failure records failed and cleans up without a
replacement shell.
--mode 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. Headless runs never open a
replacement shell and are not relaunchable.
Full-permission flags are applied by default in both - the agent is in a
container, and the container is the boundary (security.md).
The task prompt is optional in tui 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. Headless mode has no interactive
surface, so it still requires a task.
Where conflict coordination is on and the launch has a task, Aether adds this
short discovery instruction before substituting {task}:
Use `aether-internal skill` to read this run's live assignment; use `aether-internal` to coordinate. Report a terminal outcome only after the assigned work is finished.
For a taskless launch, the same instruction is delivered through the vendor-native startup mechanism, without inventing an initial user prompt:
| CLI | Taskless discovery mechanism |
|---|---|
claude |
--append-system-prompt |
codex |
one-launch -c developer_instructions="..." |
pi, omp |
--append-system-prompt |
opencode |
ephemeral instructions file in OPENCODE_CONFIG_CONTENT |
The instruction is short and runtime-scoped. The skill command loads the
version-matched assignment, role guidance, and workflow from the staged CLI.
The server stages that /usr/local/bin/aether-internal CLI and the run's
/run/aether/coord3.sock automatically. No manual skill install, identity
flag, repository instruction, or persistent member-home write is needed.
The agent should run aether-internal skill before acting, then use the CLI
for coordination. Report a terminal outcome only after the assigned work is
finished. The co-author rule still asks the agent to read
/run/aether/co-authors before each commit. Only the prompt
the harness receives changes: the stored task, branch slug, and every CLI and
dashboard surface keep what the member typed. See coordination.md.
| 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} |
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. Environment terminals carry no
harness, so they get none of this - the member's own shell is not launching
an agent.
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
terminal. The installed executable lives in that member's environment home.
An argv override replaces the shipped template wholesale, so a registry fix
never reaches it: a deployment's --harness-definitions entry that redefines
a shipped harness has to be updated on its own. It keeps the registry's key
passthrough and launch env for that name, since neither is part of the
command line.
Setting up an agent
Once per person, per agent:
aether agent add <name>
For a shipped name, the local dashboard's Agents step opens the live environment terminal dock and types the vendor install script for you. In the CLI, run:
aether terminal
Install the executable into ~/.local/bin, complete the vendor login in that
terminal, and return to the dashboard. The login and executable are in your
member home, so every container for that member sees them. A member-defined
name also records a launch definition under that member.
The local dashboard's I've installed and logged in button checks agent.list
before confirming installation. The Agents page shows Installed or
Not installed for your account. These checks verify the executable;
the agent verifies its vendor login when it starts. Shipped agents need no
separate registration record. Other members can use the installation only
after you share your account; see teams.md.
For an unshipped name the command asks for interactive and headless launch
templates first (<name> {task} and <name> -p {task} by default). Install the
executable into ~/.local/bin using the vendor's documented procedure, then
complete its login.
The environment terminal 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 terminal:
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.
- 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 and remain
in the member's persistent home. The browser configuration import skips known
credential names before upload.
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. The browser configuration import skips known credential
names before upload.
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.
The browser configuration import skips known credential names before upload.
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 profile sync. 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. The row stays where it is - there is no
command that removes one, and aether agent list keeps printing it as
agent omp member next to agent omp shipped. 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.
fake
The deterministic test harness. 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/aether
The {task} placeholder expands to the run's task text; omit it if the
script does not need it.
This is how the quickstart proves the whole lifecycle without any vendor account, and it is the harness the end-to-end tests drive.
Custom agents
Custom launch definitions come from two places, resolved in this order:
- Server configuration (administrator):
--harness-definitionsor theAETHER_HARNESS_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.
Both forms carry the same fields and pass the same validation. The
administrator JSON is an object keyed by harness name. Each definition must
name the executable and provide both interactive and headless 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.
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 profile 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", "{task}"],
"HeadlessArgs": ["omp", "-p", "{task}"],
"Executable": "omp",
"ProfileRoot": "/home/aether/.omp",
"CredentialPaths": ["/home/aether/.omp"],
"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 import, and launch definitions remain separate concerns: installation and login state live in the member home, the one-time browser import writes selected configuration there, and the definition resolves argv for that member. The terminal is the only setup transport for installation and login.
Agent configuration: import and Files
The local dashboard (aether gui) does not watch a laptop directory or run an
AI inventory. During the Agents step, choose one directory such as
~/.claude, ~/.codex, ~/.pi, or ~/.omp with the browser directory picker.
The browser waits for config.roots and a known destination before it reads
any file bytes. A unique basename selects its destination automatically; an
unknown or ambiguous basename must be assigned explicitly. The preview then
shows the files that will be sent and the paths left out before upload. Import
is explicit and one-time: after it succeeds, the import control is gone. The
server-hosted dashboard has no onboarding picker; use local aether gui for
this step.
Credential names in any path component and *.pem files are always skipped
before upload. Runtime/history exclusions come from the selected root's
runtime_ignores metadata, which matches exact root-relative paths or
component prefixes case-sensitively after trailing slashes are trimmed. This
policy applies to renamed directories too. Changing an ambiguous destination
clears the prior preview and re-reads the local file handles with the newly
selected policy; a stale read cannot replace the current preview. These local
exclusions are not overridden by .aether-profile-ignore in browser import.
agent/skills/, agent/extensions/, and agent/npm/ remain configuration and
are imported.
Remaining bytes are uploaded and scanned by the server; do not assume all
secret-looking content stays on the laptop. A complete response reports
accepted counts and server exclusions. If the server stops after writing files,
the dashboard reports an incomplete result with exact committed paths, counts
and the real error, and warns that copied files remain. If the RPC response is
lost, the outcome is unknown and some files may have been copied; inspect
Files before retrying. There is no watcher or automatic retry: selecting
the directory and importing again is explicit.
An import can include empty files and arbitrary binary bytes. It is limited to
1 MiB per file, 20 MiB decoded total, and 2,000 files. Browser
imports create new files with mode 0644; the browser cannot preserve
executable mode or symlinks, so a script may need chmod in the remote
terminal.
The imported files are written into your authenticated member's persistent configuration home. That home is mounted read-write in your environment terminal and in runs using your account, so the change is immediately visible to existing and future runs (an agent may need to reload its configuration). An account share gives another member's run the same home; it does not create an isolated per-run profile. 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 import, open Files to browse your own member configuration alongside
workspace base and live-run files. 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
Configuration files are complete UTF-8 text up to 64 MiB. Binary and oversized
files are read-only. New configuration files accept nested relative paths and
never overwrite an existing file. A stale save keeps the draft; reload from the server only when
you want to discard it and replace it with current content.
Every config.* method requires Launch and addresses only the authenticated
member's own home; an admin cannot select another member.
For workspace files, Commit to
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>
These commands read the local harness root named by the selected agent. They are separate from browser directory import and from editing the persistent member home in Files. Use the member-home editor when a change should be visible immediately to the shared home.
Adding a harness
The registry is one map entry: argv templates for both modes, credential paths, profile root, denylist, API key passthrough, the taskless discovery mechanism, and the status reporter - what the harness can report, which is what declares a reporter at all, plus the arguments or environment variables that point the harness at it and any asset files those name. An adapter is a separate, optional file. Both are covered in adapters.md.


