Zero to a finished agent run in about ten minutes, solo.
Two machines are involved, though they can be the same one:
- the server box - a Linux machine with Docker and git. Agents run here.
- your machine - Linux, macOS, or Windows. Where you type.
Your machine needs git and, unless both machines are on a tailnet
(see step 3), an SSH key. Aether uses
~/.ssh/id_ed25519 and your ssh-agent. For a key somewhere else, pass
aether link --key <path>; for a passphrase-protected one, ssh-add it
first. Windows paths and the OpenSSH agent service are in
install.md.
1. Install
On the server box and on a Linux or macOS machine:
curl -fsSL https://raw.githubusercontent.com/3xDevOps/Aether/main/scripts/install.sh | sh
The script asks what this machine is. Answer server on the server box and client on your own machine; Enter takes the sensible default.
That answer decides what you get. A server gets aether and aether-server
in /usr/local/bin, with sudo. A client gets the aether CLI alone in
~/.local/bin, without sudo, so the desktop app can replace it when it
updates; if that directory is not on your PATH, the script prints the one
line that adds it, and the app finds it either way. macOS is a client
platform, so it only ever gets aether. Later, aether update upgrades
whatever is installed.
It then finishes that side's setup: step 2 on the
server, the desktop app (step 7) on a client. To
install the binaries and stop there, add --role none; see
install.md.
On Windows, download and review the PowerShell installer, then run it:
Invoke-WebRequest -UseBasicParsing -Uri https://raw.githubusercontent.com/3xDevOps/Aether/main/scripts/install.ps1 -OutFile "$env:TEMP\aether-install.ps1"
& "$env:TEMP\aether-install.ps1"
It installs the CLI and desktop app by default, without administrator access.
Use -Role none for the CLI alone. Desktop setup requires a release containing
the Windows installer fixes (v0.4.0-alpha.6 or newer); the installer refuses
older desktop builds. See install.md
for script-policy requirements, version selection, upgrades, and manual installation.
2. Start the server
Answering server in step 1 already ran the command below on the server box. It writes the config and the systemd unit but deliberately starts nothing, so the activation line it printed is still yours to run. Run setup by hand if you skipped the question:
sudo aether-server setup
It asks for the listen address, data directory, and tailnet policy - plus, on a host that already runs tailscaled, the dashboard's HTTPS port on the tailnet (Enter accepts each default) - then prints:
systemctl daemon-reload && systemctl enable --now aether-server
Run that and the server is live on :2222. The SSH host key is generated on
first start, and the SSH port is the only thing exposed unless you answered the
dashboard question. Change any option later with
aether-server config set <key> <value>, then restart.
To try it in the foreground first, sudo aether-server serve runs until
Ctrl-C. install.md covers unattended installs, running
unprivileged, and every serve option.
3. Link from your machine
aether link <server-host>:2222
Output:
linked to <server-host>:2222 as admin (admin)
The first identity to link a fresh server becomes the admin. That is the
whole account setup - there is no signup, no password, no config file to edit.
The link is saved to ~/.config/aether/config.json, or
%AppData%\aether\config.json on Windows. (Joining over a tailnet,
the display name comes from your tailnet login instead of the literal
admin; the role is the same. Change any display color with
aether member color <#rrggbb>.)
How you were identified depends on the network:
- On a tailnet: Tailscale already knows who you are and the server asks it. No SSH key, no invite code, nothing to copy. See networking.md.
- Anywhere else: your SSH public key (
~/.ssh/id_ed25519, or any key in your ssh-agent) is registered as the admin's key. Generate one first withssh-keygen -t ed25519if you do not have one. On Windows that is%USERPROFILE%\.ssh\id_ed25519and the OpenSSH agent service;ssh-keygenships with Windows OpenSSH.
On first contact aether records the server's host key in ~/.ssh/known_hosts
(%USERPROFILE%\.ssh\known_hosts on Windows) and prints its fingerprint.
Compare that against what the server printed if you care to.
Then tell Aether who to put on your commits:
aether member git --name "Ada Lovelace" --email ada@example.com
Every commit an agent makes in your runs, and every commit Aether makes for
them, is authored as that name and address, so branches you merge upstream
credit your account. Without it the fallback is your display name at
<member-id>@aether.local, which maps nowhere. The local dashboard's onboarding
wizard asks for the same two fields in its Git identity step, right after
Link, prefilled from this machine's git config user.name and user.email.
aether member git with no flags shows what is set.
4. Create a workspace and push your repo
A workspace is the repo plus a server-owned scope for runs and shells. Creating one is an admin operation. Every container for a member starts from that member's saved image, or the server's standard image when none is saved:
aether workspace init myproject
Use --base <branch> when runs should branch from something other than
main. The dashboard's create-workspace form uses the same command. Install
tools in the environment terminal, then press Save environment so runs get
them too; see environments.md.
Now point your local clone at it and seed the repo:
aether link <server-host>:2222 --repo ~/code/myproject
cd ~/code/myproject
git push -u aether main
link --repo adds an aether git remote - a normal git remote over the same
SSH port, no separate credentials. With multiple workspaces, add
--workspace <name-or-id> (aether workspace list shows them). The push
sends the workspace's base branch; replace main if you created the
workspace with --base.
The first link --repo also reads your clone's own origin URL and records
it as the workspace's checkout Origin, printing workspace origin -> <url>. Every run checkout created afterwards gets an origin remote pointing
there, so an agent in a run can git push origin <branch> and open a pull
request once you have connected GitHub (step 5). The
local dashboard's Repository step does the same. Recording happens only when
you may push - a viewer may not - and your clone's origin is one the server
accepts; otherwise the link still succeeds, prints no workspace origin ->
line, and records nothing. aether workspace origin shows what was recorded
and takes a URL or --clear to change it. A github.com origin you cloned
over SSH is recorded in its https://github.com/... form, because that is the
form a run can push to; teams.md has the rule for other
hosts.
This checkout Origin is a push destination, not the source used to refresh a workspace base. A workspace is local-only by default. To have Aether fetch a protected base from an upstream repository, configure the optional source mirror below.
In the local dashboard's onboarding wizard, the Repository step does both for
you. Point it at your clone: type the absolute path, or, in the desktop app,
pick it with Choose folder. The step adds the remote, then its
Push now button compares your clone
with the workspace and pushes when there is something to push, keeping git's
own output on the page. It runs the same push with --no-follow-tags, so the
command above also sends your tags if you have push.followTags set. The step
remembers the repository it connected, so walking back to it shows the
connected clone and its push result with Use a different repository to
re-point, not an empty form.
A fresh workspace has no branch there yet, so the button pushes. A member joining a workspace someone else already seeded gets a report instead of a failed push: the same commit on both sides; the workspace ahead of the clone, where a Fast-forward my clone button catches the clone up, fast-forward only; or the two diverged, where the wizard prints the git commands to resolve it by hand. A fast-forward writes no merge commit and never rewrites your own commits, and nothing here force-pushes. See teams.md for that case.
For optional source-mirror setup, see the detailed instructions.
Optional: configure source control
An administrator can make the workspace's base a read-only mirror of an upstream branch. Configure it from the CLI or local dashboard only after the initial workspace push or after reconciling the two repositories:
# Public GitHub or any public HTTPS repository:
aether workspace mirror configure --workspace myproject \
--source https://github.com/acme/myproject.git --branch main --auth public
# Private GitHub: Aether generates a read-only deploy key.
aether workspace mirror configure --workspace myproject \
--source https://github.com/acme/private.git --branch main --auth deploy-key
# Generic SSH: provide a known_hosts file; do not use unverified TOFU.
aether workspace mirror configure --workspace myproject \
--source ssh://git.example.com/acme/myproject.git \
--branch main --auth deploy-key --known-hosts-file ~/.ssh/known_hosts
--branch defaults to the workspace base branch. Public mode accepts
credential-free HTTPS. Deploy-key mode accepts a GitHub HTTPS URL or a generic
ssh:// URL. For GitHub, configure prints the public key and:
GitHub deploy-key settings: https://github.com/<owner>/<repo>/settings/keys/new
and Open Settings > Deploy keys > Add deploy key. Follow that URL, paste the
public key as a repository deploy key, leave Allow write access off, and
then verify it. The private key never appears in CLI output or the dashboard.
Configuration is initially pending and does not fetch. Verify it with:
aether workspace mirror refresh --workspace myproject
aether workspace mirror status --workspace myproject
Forward-only upstream changes become ready and advance the mirrored base. Rewrites or a base that diverged locally retain an observed candidate and leave the accepted base alone; review the candidate and explicitly adopt it:
aether workspace mirror adopt --workspace myproject --generation <n> --yes
aether workspace mirror disable --workspace myproject --yes
The local dashboard's onboarding wizard offers this same setup inline only to
administrators and only when capability workspace.mirror.status exists. The
entry is prefilled from checkout Origin when available and opens this
existing Source control flow for public HTTPS or deploy-key setup and
verification. Choosing
it configures the server-owned read-only upstream; new runs refresh it before
launch. Skipping it leaves the workspace's current source settings unchanged.
If no source mirror is configured, it remains local-only; configure it later
from Workspace > Source control. On the dashboard's Workspace page, the flow is
admin-only: its one-time Configure form is prefilled from Workspace.Origin;
choose public or deploy-key authentication, copy the public key and follow the
GitHub link, then use Verify or Refresh. Adopt candidate and
Disable both ask for explicit confirmation. Disable returns the workspace
to local-only mode; it does not revoke a deploy key at GitHub, so remove that
key there.
Every launch refreshes a configured mirror before creating the run. An authentication, network, missing-source, rewrite, divergence, or other refresh failure creates no run and never silently uses a stale base. If the failure reports an accepted commit, the CLI offers an explicit retry:
aether run "add a health check endpoint" --agent claude \
--cached-base <accepted-commit>
The --cached-base override is request-scoped: it applies only to the launch
request where it is supplied and is never inherited by later launches.
Repeating the same override is accepted while the SHA still matches the
accepted commit and the workspace base has not moved; a mismatched SHA or
moved base is rejected.
5. Set up your agent
Choose an agent once:
aether agent add claude
For a shipped agent, the local dashboard's Agents step opens the live
environment terminal dock and types its vendor install script. The dock says
Starting your environment container while Docker starts it, then the
shell appears. When you press I've installed and logged in, the wizard
checks the executable is on the server and runs env save for you, naming
the saved image. It cannot check the vendor login; the agent does that when
it starts. From the CLI, open the terminal, run the script, install into
~/.local/bin, and complete the vendor login there:
aether terminal
For a name Aether does not ship, the command first asks for interactive and
headless launch templates. Install that executable into ~/.local/bin using
the vendor's instructions, then complete its login in the environment terminal.
Return to the dashboard when finished.
The member home persists the executable and vendor login state across containers. Configuration can be imported once from the browser and then edited in Files. See the environment terminal guide for tab and stop behavior.
Your own configuration is separate from vendor login and image setup. In the
local dashboard's Agents step, choose one directory such as ~/.claude,
~/.codex, or ~/.pi with Choose directory. Review the preview, select a
destination when the basename is unknown or ambiguous, and click Import
configuration. This is explicit and one-time; there is no local directory
watcher or AI inventory. The server-hosted dashboard has no local directory
picker; use aether gui for this step.
Known credential names in any path component and runtime/history defaults are
skipped in the browser before upload. Remaining bytes are uploaded and
server-scanned, so never assume all secret content stays on your machine.
Empty files and arbitrary binary regular files are preserved. Imports are
limited to 1 MiB per file, 20 MiB decoded total, and 2,000 files. Browser
imports create files with mode 0644; executable mode and symlinks cannot be
preserved, so a script may need chmod in the remote terminal.
The imported files go into your authenticated member's persistent home, which is mounted read-write in the environment terminal and in runs using that account. Changes are immediately visible, including to active runs; the agent may need to reload. This is a shared home, not an isolated per-run profile. The snapshot pin records launch provenance, not an isolated writable copy or a promise that home edits wait for later runs. Importing or editing configuration does not rebuild the installed-agent image.
Open Files to browse your own configuration beside workspace base and
live-run files. The editor supports JSON, JavaScript, TypeScript, Markdown,
Python, and TOML syntax highlighting, plus find/replace. Save explicitly with
Save, Commit to
Configuration editing accepts complete UTF-8 text up to 64 MiB. Binary and
oversized files are read-only. New configuration files accept nested relative
paths and refuse overwrites.
config.* methods require Launch and target only your authenticated member
home; there is no admin/member selector. For
workspace files, Commit to
Connect GitHub
Optional, and worth doing before the first run: connect GitHub once and your
runs can push their branch to the origin recorded in step 4, open a pull
request, and sign their commits as you.
In the dashboard, the Agents step has a Connect GitHub button. It opens the same terminal dock, types the login command, and its I've logged in button runs the rest, reporting the account and the registered key.
From the CLI it is two commands. In the environment terminal:
gh auth login --hostname github.com --git-protocol https --web \
--scopes admin:ssh_signing_key
Then, back on your machine:
aether github connect
What each step writes, how to re-run it, and how to revoke are in environment-home.md and security.md.
6. Launch a run
aether run "add a health check endpoint" --agent claude
The run gets its own container and checkout while using your persistent home.
run 01m04mhf114eap4k85n2mgcped running
A run is one agent execution with its own container, git worktree, and
branch; aether runs lists them. Scoped commands default to the only
workspace when there is exactly one, which is why nothing above named it.
7. Watch it
aether gui
This serves the dashboard from your own machine and opens a browser tab
already carrying a per-process token. It rides your SSH key, so everything
the CLI can do works from the page, plus local verbs like pulling a run
branch into your clone. Leave it running; Ctrl-C stops the gateway and the
token dies with it. aether gui --url prints the URL instead of opening a
browser. See local-gateway.md.
On a tailnet, the server can host the dashboard instead, so a phone or any
other tailnet device opens https://<the server's MagicDNS name>/ with
nothing installed and no token. Set web-port and restart the server; see
networking.md. This server-hosted dashboard has
no machine-local verbs or onboarding wizard, but its authenticated Files
view can edit the shared member home.
Prefer a native window?
aether gui in a browser tab is the whole dashboard. If you would rather it
lived in its own window - with desktop notifications and a dock badge when a
run parks in needs-attention, plus aether://run/<id> deep links - build
the desktop app. Answering client in step 1 already did this. Nothing has
to be installed first - the CLI fetches its own Node.js copy when the machine
has none, which makes the first build longer:
aether gui build
That installs Aether into your application menu (Linux), your Applications
folder (macOS; ~/Applications without administrator rights, and the command
prints the path), or the Start Menu (Windows). Open it like any other app.
Two things to know:
- There is no download. No release publishes an installer.
aether gui buildpackages the Electron shell on your machine from sources carried in the CLI. Details in install.md. - It is not a standalone client. The app does not bundle
aether; it launchesaether guifrom yourPATH, and the dashboard lives inside that CLI binary. Install the CLI (step 1); on an unlinked machine, the app opens its local onboarding wizard and links from there. When you update the CLI, the window picks up the new dashboard without rebuilding the app.
In the dashboard: a workspace switcher over the runs in scope, a board bucketed by what needs attention, a live terminal mirror per run, the diff timeline, the workspace feed. Read-only by default; typing into a run needs the steer capability, which as the owner you have.
The terminal escape hatch is aether attach <run-id> - a raw byte-for-byte
passthrough where every native keybind and theme of the agent's own TUI works.
Detach without killing anything: the PTY lives on the server.
To nudge a running agent without attaching:
aether inject <run-id> "also update the README"
The message appears in the transcript as a banner in your member color, and everyone watching sees who said it.
8. Pull the result
When the TUI agent exits, the run stays alive in a login shell. Start another installed agent in the same checkout if needed; exiting that shell opens another login shell, so the run remains live until you explicitly close it. Close commits and publishes the latest work to the run's branch.
aether pull <run-id>
If your checkout is already on the run branch, Aether fast-forwards it. If not, Aether creates or updates the local run branch without switching your checkout:
Branch aether/run-add-a-health-check-endpoint-mgcped is ready. Switch with: git switch aether/run-add-a-health-check-endpoint-mgcped
The branch name is aether/run-<slug>-<short-id>: the task slugified, then the
last six characters of the run ID. Aether never switches branches or merges
the run into your base branch. Review it, diff it, then merge it by hand:
git log --oneline aether/run-add-a-health-check-endpoint-mgcped
git diff main...aether/run-add-a-health-check-endpoint-mgcped
If the local checkout has uncommitted changes, the pull still fetches the run branch and reports that the checkout is dirty. Commit or stash those changes before switching to the run branch or merging it.
When the review is complete, merge locally. In a local-only workspace, push the reviewed base back to Aether:
git switch main
git merge aether/run-add-a-health-check-endpoint-mgcped
git push aether main
In a mirrored workspace, the Aether base is protected. Push the reviewed result to the configured source branch using credentials for that upstream:
git push <source-remote> main:<configured-source-branch>
Here <source-remote> is a local remote (or URL) for the configured source;
the mirror's server-side credentials are read-only. The checkout Origin is
optional and independent of the source mirror. It may be a different
repository, and is a review destination for publishing the run branch (for
example, to open a pull request); it is not the destination for a mirrored
base update.
Then close the run out so it leaves the attention board:
aether close <run-id> --outcome merged # or --outcome abandoned
Closing retains the exact TUI container, checkout, run row, member account and
coordination surfaces for --run-container-ttl (default 1h). Before that
retention expires, reopen the same run with:
aether relaunch <run-id>
Relaunch does not create a new run or container and expired or unavailable runs cannot be relaunched. Kill and Delete remain immediate cleanup operations.
The local daemon is optional. It fetches server-owned run branches as agents
commit and can push your local base branch in local-only workspaces. It
does not watch agent configuration directories; configuration is imported
explicitly in the local dashboard (aether gui) and edited in Files. It
does not fetch a mirror source or forward a checkout's origin into the
workspace. In a mirrored workspace, a base push attempt is rejected because
the mirror owns that branch; use aether workspace mirror refresh and, when
needed, explicit aether workspace mirror adopt instead. aether pull remains
the review path in every mode.
aether daemon install --server <server-host>:2222 --repo ~/code/myproject
systemctl --user daemon-reload && systemctl --user enable --now aether-daemon
That second line is the Linux one. daemon install prints the activation
command for whatever platform you are on: launchctl load on macOS,
schtasks /Create on Windows. A unit installed by an older release that still
contains --sync-origin will fail after upgrade; reinstall it with the
command above and activate the newly written unit. There is no recurring
dashboard Sync from origin action.
Prove the plumbing without an agent subscription
No vendor login yet? Aether ships a deterministic agent named fake for
exactly this: it runs a script from your repo instead of an agent, so you can
drive the whole lifecycle end to end and see a real branch come back.
Start the server with the fake agent's command in its environment. If the
systemd unit from step 2 is already running, stop it first
(sudo systemctl stop aether-server) - two servers cannot share :2222:
AETHER_FAKE_AGENT="sh /workspace/agent.sh" \
aether-server serve --data-dir /var/lib/aether --addr :2222
/workspace is where the run's checkout is mounted, so agent.sh is just a
file in your repo. Create a throwaway one:
mkdir demo && cd demo
git init -b main
git config user.name "You" && git config user.email you@example.com # if git has no identity yet
cat > agent.sh <<'EOF'
echo "agent starting"
printf 'hello from the agent\n' > result.txt
echo "agent done"
EOF
echo "# demo" > README.md
git add -A && git commit -m seed
Then run steps 3, 4, 6 and 8 above with the default standard image and
--agent fake instead of --agent claude. Skip step 5: fake has no agent
login. Step 7 (aether gui) works too if you want to watch.
Launching it is the CLI's job, though. fake is a server-side registration
rather than an executable installed in your account, and the local dashboard's
two launch surfaces - the launch form and the wizard's First run step -
offer only agents installed in the account, plus the custom harness a
deployment pins, so fake never appears in either.
aether link <server-host>:2222
aether workspace init demo
aether link <server-host>:2222 --repo "$PWD"
git push -u aether main
aether run "write a result file" --agent fake
aether runs
aether pull <run-id>
aether runs shows the run reaching needs-attention within seconds, and the
pulled branch carries a commit adding result.txt. That is the full path -
container, worktree, PTY, commit, fetch - with nothing mocked but the agent.
When something does not work
| Symptom | Cause |
|---|---|
not linked; run aether link <addr> |
CLI commands that need a server have no saved link. The desktop app opens its onboarding wizard and can link from there. |
no Aether member for this key |
The server already has an admin, so you are not the first member. Get an invite: teams.md. |
unable to authenticate, attempted methods [none] |
The CLI had no key to offer: none at ~/.ssh/id_ed25519 and no ssh-agent. The same error names the key when one was found but could not be used - read the rest of the line. On Windows, check Get-Service ssh-agent and look for the key at %USERPROFILE%\.ssh\id_ed25519. |
<path> is passphrase-protected; add it to ssh-agent (ssh-add <path>) or pass --key <unencrypted key> |
The key exists but the CLI cannot decrypt it; it does not prompt for a passphrase. Run ssh-add <path>, or point at an unencrypted key with aether link <addr> --key <path>. |
parse ssh key <path> |
The file at that path is not an SSH private key (a public key, or a truncated file). Pass the private key with aether link <addr> --key <path>. |
link --key: stat <path> / ssh key <path>: open <path>: no such file |
The key path you chose is not there. A chosen key is never skipped in favor of the agent, so re-link with the right --key. Re-linking without --key keeps the saved one; to go back to ~/.ssh/id_ed25519, delete the key line from ~/.config/aether/config.json. |
host key mismatch / REMOTE HOST IDENTIFICATION HAS CHANGED on aether link |
The server was reinstalled and generated a new host key, but your known_hosts still trusts the old one. Clear it: ssh-keygen -R '[<server-host>]:2222'. |
tailnet identity unavailable; key authentication required |
Informational, not an error. The server has Tailscale but this connection did not arrive over the tailnet, so it fell back to your SSH key. |
membership pending admin approval |
You joined over a tailnet on a server that requires approval. An admin runs aether member approve <your-member-id>. |
no workspace yet; skip git remote |
Run aether workspace init first, then re-run aether link --repo. |
multiple workspaces available; specify --workspace |
Pass --workspace <name> to aether link or another command that accepts a workspace selector. Agent setup is member-scoped. |
Run reaches failed immediately |
The agent started and exited. aether timeline --run <run-id> shows the exit code; aether attach only works while a run is alive. |
self-update is not supported on Windows |
Expected. Re-download the release binary: install.md. |
Starting over
Testing the whole path from a clean slate, or handing the box to someone else?
install.md has the full removal order for the
server, the client, and your linked repos. Two things bite people: run
containers outlive the server unit and must be removed separately, and a
reinstalled server gets a new host key, so stale known_hosts entries have to
go or the next aether link fails.
Next
- install.md - systemd, upgrades, data layout
- environments.md - member images, saving, resetting, and persistence
- environment-home.md - member home, installed agents, and migration
- networking.md - Tailscale-first, plus LAN and VPN
- teams.md - joining, roles, workspaces
- harnesses.md - login, configuration import, and launch definitions
- security.md - what the container boundary does and does not do


