Skip to content

Security: 3xDevOps/aether

Security

docs/security.md

Security model

What Aether isolates, what it leaves exposed, and who can see or act on what. Read it before you run Aether for other people or let someone join your server.

The Aether server runs on your Linux host and starts every agent in a Docker container. A member is a person admitted to the server. Each member has a home directory on the server and an environment, a long-lived container where they install and sign in to agent CLIs. A session is one agent task in its own container, with the launching member's home mounted into it. A workspace is a repository and its sessions. Concepts has the full glossary.

Report a vulnerability

Do not open a public issue for a security problem. Follow Security in CONTRIBUTING.md, and leave live credentials out of the report.

At a glance

Surface What it protects What it does not protect
Session container Keeps agents off the host, out of the host Docker daemon and out of other members' homes. Sessions and environments cannot connect to each other. Agents run unprivileged. It is not a VM. A session can read and change everything mounted into it: your home, the checkout and the workspace secrets.
Permission tiers A Default session's sudo waits for its owner or an admin of its workspace. Isolated keeps every change outside the checkout inside the session. Root access runs sudo without asking. Docker inside a Default session gives root in the container without asking.
Egress control Off by default. When on, a workspace's sessions reach only its listed hosts, through the server's proxy, and never a private, loopback or metadata address. An allowed host receives whatever the agent sends: with github.com allowed, the launcher's gh token can push anywhere it can write. Sessions still reach the server host.
Member home and environment Your home is mounted only into your own environment and your own sessions. Every session you launch can read and replace your agent logins and gh token, and sign with your commit signing key.
Who can act on a session Viewers can only watch. Every action is attributed on the workspace timeline. By default every collaborator can type into, message and kill your sessions, which hold your home.
Account sharing A recipient's session gets your agent login, not your home. The recipient's session can copy, overwrite or log out your login. A copied refresh token outlives a revoke. An omp share is as broad as sharing your home.
GitHub credentials The gh token stays in your home and the private signing key on the server, out of the database and out of saved images. Sessions sign through an agent and cannot copy the key. Any of your sessions can act as you on GitHub within the token's scopes, including registering a signing key of its own.
Workspace secrets Hidden in settings, never returned by the API, masked in setup script output. Every process in every session of the workspace reads them, and anyone who can launch or message a session can make it print one.
Assistant Its own container, home and sign-in, which your sessions and account share recipients cannot see or change. Launching, messaging and stopping sessions and saving the environment wait for your click. A prompt that steers it can redirect the rest of its running session. Starting it needs the vendor's sign-in service. Auto-approve removes the click, and an approval rule removes it for one kind of action.
Remote development and browser companion Terminals, the app browser and captures need permission to steer the session. Chromium mounts none of your files. Pages and captures can contain secrets. Anyone who can view the session can read a retained capture.
Workspace mirrors Nobody can push to a mirrored base. A failed refresh blocks the launch instead of using stale code. The configuring admin's gh token or the deploy key sits on the server, readable by the server administrator.
Dashboard gateways aether gui listens on loopback with a per-process token. The server's dashboard is tailnet-only HTTPS with per-request identity. Any process on the server host that connects to the host's own tailnet address is served as the node's owner.
Dictation The speech-to-text engine runs in a container with no network, no capabilities and a memory cap, reachable only through a 0600 Unix socket. Only approved members call it, never connectors, one clip at a time. Audio stays in memory. The engine and model are third-party binaries, checked only against pinned SHA-256 sums.
Push notifications The server posts only to public HTTPS endpoints, and the payload is encrypted to your browser. A notice shows run text (title and reason) on your device.
Agent configuration and Files aether config push skips credential files and leaves out files with secrets. Files will not show a known credential file. Files acts on your whole home. A credential under an unrecognized name is not locked.
SSH into a session and port forwarding Same admission as a session shell. Forwards reach only the session's own loopback. Sessions are not recorded. Attaching an editor lets the session's agent reach your computer through the editor.
Edge SSH stays end to end. The server decides membership. By default a person approves every new device. Under edge-access account, a taken-over GitHub account or edge admits a device. The edge sees connection metadata.
Connectors A connector acts as one member through the tools of the scopes they granted, with that member's permissions. TLS ends at the edge, so a compromised edge sees connector traffic and tokens. Under edge-access account a GitHub sign-in at the edge authorizes one without a code.
Slack The bot connects out to Slack and acts as one linked member: it messages runs and answers requests once, with that member's permissions. Whoever controls the linked Slack account acts as you on your sessions. Slack keeps the messages it was sent.
Coordination Sessions message only allowed peers in their workspace. A message grants no control. Sessions in one workspace share a repository and can influence each other through files.
Team memory Only members pin notes into briefs. Notes with secrets are refused. Agents cannot delete. A note can be wrong or planted. It is data, never an instruction.
Sysbox Docker inside a session without the host socket. Container root maps to an unprivileged host user. Installing it from the dashboard runs a root script on the host.
Release signing Updaters install only binaries listed in a signed manifest for that exact tag. aether gui build --zip checks only the app layout of a local zip.
Self-update Only signed v* releases of 3xDevOps/Aether. A failed download replaces nothing. A super admin chooses which release the server runs.
Vulnerability scanning govulncheck runs on every pull request that changes code. It is advisory: a finding does not fail the build.

The agent container

The container is the only isolation boundary. It is not a VM: every session shares the host's kernel, and Aether builds no second sandbox inside it.

  • Agents run as uid 1000, or as an image's own non-root user. What they may do as root depends on the session's permission tier. Treat root in a session as able to do anything the container can reach.
  • A session is never --privileged and never gets the host Docker socket. With Sysbox installed, runs and environments get their own Docker daemon, and root in the container maps to an unprivileged host user. Without Sysbox a session has no Docker.
  • Sessions and environments start on aether-agents, a Docker bridge network the server creates with inter-container traffic off (com.docker.network.bridge.enable_icc=false). They reach the host and the internet but not each other. To set its subnet or MTU, create it yourself with that option before the first launch; a launch fails on a network of that name without it.
  • A session of a workspace with egress control on starts on aether-egress instead: the same, but --internal, so its one way out is the server's egress proxy on the network's gateway.
  • The packaged systemd unit runs aether-server as root. Docker socket access is root-equivalent anyway, and root lets the server hand homes and checkouts to the agent's user. An unprivileged server runs every container as its own uid and gid, the only owner it can hand files to.

What a session container is given decides what it can do:

flowchart LR
  home["Launcher's home on the server"] -- "read-write as $HOME" --> run["Session container (agent is uid 1000)"]
  checkout["Session checkout"] -- "mounted at /workspace" --> run
  secrets["Workspace variables and secrets"] -- "environment variables" --> run
  coord["Session's coordination socket"] -- "mounted" --> run
  run -- "outbound network" --> net["Internet and your network"]
Loading

Your home is mounted only into your environment and the sessions you launch; an account share is the one exception. Your git name and email (set with aether member git) are the author and committer in those sessions, and /run/aether/co-authors holds a Co-authored-by: Name <email> line for each other member who messaged or typed into the session.

Warning

If you run agents you do not trust, put the server's --data-dir on a filesystem mounted nosuid,nodev. Docker has no per-mount switch for this, so otherwise root in a session or environment can plant a setuid binary through a writable mount and have it survive on the host.

Permission tiers

Every session has a permission tier. Pick it under Permission at the top of the launch form, which remembers your last choice, or with aether session --permission root|default|isolated. The assistant and connectors launch at the tier they pass as permission, or else at your saved tier, as the launch form preselects it. Everything else launches at Default: the CLI without the flag and Slack. A subsession gets its parent's tier. Any member who may launch a session may pick any tier.

Root access Default Isolated
sudo Sessions without asking Asks you first Refused
Your home and tool caches Read-write Read-write A throwaway copy
Network As the workspace allows As the workspace allows The agents' own hosts
Docker inside the session With Sysbox With Sysbox None
Messages to other sessions, team memory notes Yes Yes Refused
  • A tier governs sudo, the container and the network, never the agent's own permission prompts. Those follow the agent's launch flags or Enhanced mode (Modes), which no tier sets, and an agent can still ask for checks it never skips (Permissions and input).
  • In every tier the agent runs as uid 1000, or as the image's own user when that is not root. An image whose user is root, such as an environment you saved, still runs as 1000:1000, with your home at /root so paths saved in it keep working. The server hands your home and the checkout to that uid before each container starts, and gives the uid a passwd entry whose home is /root: git, ssh and ssh-keygen refuse to run without one, and ssh reads ~/.ssh from it rather than $HOME.
  • Every container of yours mounts the same home, so root in one session can take it from all of them. The start step links /etc/tmpfiles.d/provision.conf to /dev/null, replacing any the image ships: without that, installing a package that pulls in systemd (Playwright's --with-deps browser libraries do) runs systemd-tmpfiles --create, whose Ubuntu provision.conf hands /root to root. A container started before your server had this change gets the link at its next start. If root changes the home's owner or removes its owner permissions anyway, every agent on it loses its login and its executables until the server gives the home back. It restores the home directory's owner and the owner's rwx, not files inside it, when an Enhanced agent next starts or restarts, with the notice Home restored; each new container's start hands the whole home back.
  • Default and Isolated sessions get Docker's no-new-privileges, so no setuid binary raises the agent.
  • A Root access run and your environment terminal get a sudoers entry for their uid before anything else starts in them.
  • Docker inside a Default session puts the agent in the daemon's group, so it can reach root inside the container through Docker without asking. With Sysbox that root maps to an unprivileged host user.
  • An Enhanced session's adapter runs under a keeper that outlives server restarts. The keeper and the server's bridge to it run as root and cannot be traced, and their socket sits in a root-only directory under /run/aether-acp, so the agent's user cannot read the conversation's stream, inject into it or take it over. The adapter itself runs as the agent's user. The agent's group can read the status of the keeper's exec and stop it, which ends only its own agent, so that a server build without keepers can still stop a keeper after a rollback. Root in the container can reach the keeper: in a Root access session, or a Default session with Docker, that boundary is the tier's.
  • In every tier the session's checkout and session branch are its result. Sessions launched before tiers existed show as Root access.

An Isolated session mounts your home and /aether-cache as overlays: the agent reads them as they are, and its writes land in the session's scratch directory, which is deleted with the container. Changes made to your home elsewhere while it runs may not show inside it. Its network reaches only the agent hosts in the egress list, through the egress proxy, and everything else is refused with aether: egress to <host> denied; an isolated session reaches only the agents' own hosts. sudo fails with sudo: an isolated session cannot run commands as root, in the workspace setup script too, which then fails the launch. aether-internal send, ask, reply and memory add fail with <method>: an isolated session cannot message other sessions or write team memory.

Two things in an Isolated session still reach outside it:

  • Your agent's login file stays mounted read-write from your home. Claude Code and Codex replace the refresh token whenever they refresh, so a refresh kept only in the copy would leave your real login revoked. A login that is a whole directory, such as oh-my-pi's ~/.omp/agent, stays in the copy, so its refreshes are lost with the session. On another member's shared account such a login cannot stay in a copy, so the launch fails with scheduler: an isolated session cannot use another member's omp login: ~/.omp/agent is a whole directory the session could change for their later sessions; launch it at Default, or on your own account.
  • The session can read your gh token and sign with your commit signing key. With GitHub unreachable it cannot use the token, and a signature only covers commits in its own checkout.

Sudo in a Default session

A Default session's sudo is Aether's shim at /usr/local/bin/sudo, ahead of the image's on PATH. Docker's no-new-privileges stops the image's own: /usr/bin/sudo fails with sudo: The "no new privileges" flag is set, which prevents sudo from running as root.

The shim sends the command to the server and waits. The session shows Needs you with Run as root in <directory>: <command> wherever permission requests show: the session's Needs you panel, the Needs you list, push and Slack notifications, aether answer and the connectors' aether_decide_approval. The command text is the agent's: read it before you allow it.

  • Only the session's owner or an admin of its workspace may allow it, because root in the container acts on the owner's home and credentials. Anyone who can steer the session may deny it. Anyone else gets run.input.respond: permission denied: allowing a command to run as root requires the session's owner or an admin.
  • Allowed, the command runs once as root (with -u <user>, as that user) in the session's container, with the same arguments, the shim's working directory, the container's environment with sudo's fixed PATH (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, never the agent-writable ~/.local/bin) and up to 1 MiB of piped standard input. A terminal's input is not sent, so a command that prompts reads end of input: pass -y. The shim prints the output and exits with the command's status.
  • Denied, the shim prints sudo: <member> denied this command and exits 1. Unanswered after 10 minutes, it prints sudo: nobody allowed this command within 10 minutes and exits 1. Each decision is a timeline note naming who decided and the command.
  • When the shim goes away (the agent's tool timed out, Ctrl-C), a waiting request is withdrawn and a running command's process group is killed. Processes a command leaves behind when it ends by itself keep running.
  • The shim accepts sudo [-u user] [-E] [-H] [-n] [--] command [args...]. -E and -H change nothing. -n fails instead of asking, with sudo: a person must allow this command; -n cannot wait for one. Any other flag fails, for example sudo: -i is not supported in an Aether Default session.
  • While the workspace setup script runs, before the agent starts, sudo runs without asking: an admin wrote the script.

Who can see and act on a session

A member acts only in workspaces they own or were granted (workspace access). A workspace a member cannot reach does not exist for them: it is absent from lists, counts, events and search, and a request naming it gets the same not found as one that never existed. Enforcement is on the server; the dashboard only mirrors it.

In a workspace they reach, the default role, collaborator, can view, steer (type into the terminal, message, approve, pause) and kill every other member's session. A viewer can only watch (roles). A session's container mounts its launcher's home, so steering someone's session acts with their home, gh login and signing key. A handoff (aether handoff) changes the owner but not the home.

To narrow it, aether workspace settings --message-others admins-only limits messaging, steering and killing others' sessions to owners and admins. aether protect <session> does the same for one session; aether unprotect <session> lifts it.

Development terminals, the app browser, captures and SSH into a session need permission to steer it even to read, because the app browser can already be signed in. Role changes reach open connections within seconds: a write attach that loses that permission drops to read-only.

Account sharing

Sharing your agent account lets another member session tasks on your agent subscription. Turn on Share my account on their row in Members, or run aether account share <member-id>; turn it off again, or run aether account revoke <member-id>, to revoke. The other member picks your account in the launch dialog's Account picker, or with aether session --account <your-member-id>. They stay the session's owner; usage and cost count against your account. Their agents can use it too: a subsession running the same agent as a session they launched on your account starts on it, up to the workspace's subsession limit (8 busy at once by default), and their agents may pick it for any subsession with --account (teams.md).

Their session uses their own image and home plus your agent login file, mounted read-write because a token refresh rewrites it. The server checks the mounted file is yours before their agent starts. Their own login is mounted in place in all their containers, so a refresh in another of their sessions cannot replace the file yours is mounted over and detach it. For claude that is ~/.claude/.credentials.json; harnesses.md gives each agent's path. When they have no installation of the agent in their own ~/.local/bin, they also get yours, read-only, with your whole ~/.local/bin on their PATH after their own entries.

Claude Code caches the account it is signed in to as oauthAccount in ~/.claude.json, and claude auth status reports that cache, not the owner of the login. A claude session on your account therefore gets its own copy of their ~/.claude.json without oauthAccount, their primaryApiKey, the API keys they approved, or the account caches Claude Code clears when it signs in to another account. The copy is written to <data-dir>/scheduler/account-config/<session-id>/.claude.json, which no container can replace, and mounted at ~/.claude/.claude.json with CLAUDE_CONFIG_DIR=~/.claude, so Claude Code reads it instead. Claude Code then fetches your profile with your login. When they still have a legacy ~/.claude/.config.json, which Claude Code reads before either file, the copy is made from it instead, and the session sees an empty read-only file there, which Claude Code ignores.

Their own ~/.claude.json is never mounted there, so their other sessions rewriting it cannot replace the copy. Every member's containers other than Isolated sessions mount their own ~/.claude/.claude.json, and an existing ~/.claude/.config.json, in place like their login, so a CLI there that saves to those paths cannot detach the session's files either. Every member's home therefore has an empty ~/.claude/.claude.json, visible under Files. A ~/.claude.json or ~/.claude/.config.json of theirs that is a symlink, has another hard link, is not a JSON object, or is over 64 MiB refuses the launch with the read error.

What the session changes in its copy lasts until its container is recreated: a move to another account recreates it, while reopening a retained session keeps it. Claude Code writes backups of the copy, which name your account once it has fetched your profile, to their ~/.claude/backups/, and the cp -f restore command it prints for a damaged config can point at one of them.

A session on your account still takes from their home and environment everything but these files and the login file. A claude API key that reaches it overrides your login: apiKeyHelper, or ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN under env, in their ~/.claude/settings.json; the same variables set as workspace variables; and ANTHROPIC_API_KEY that the server passes through to every claude session. With ANTHROPIC_API_KEY set, claude auth status reports "authMethod": "api_key", and because the copy leaves out the keys they approved, the interactive session first asks Detected a custom API key in your environment; answering yes uses the key instead of your login.

  • Root in their session can copy your login file. The refresh token in it outlives a revoke and keeps working until you log out and in again.

  • Their session can end or replace your login. Measured with Claude Code 2.1.317 and Codex 0.166.0: /logout or claude auth logout, and codex logout, in their session POST your refresh token to the vendor's revoke endpoint, which ends your login everywhere. /login writes their login into your file. For every agent except omp, that costs the login and nothing else in your home.

  • Revoking blocks later launches and reopens, removes their control of sessions on your account, and takes your login out of their running sessions. Each of their live sessions on your account moves to another account they may use, picked by the usage fallback rules whether or not your usage ran out, and continues there with a room note; an Enhanced session gets a prompt to continue. A session that cannot move fails, its container removed, with one of:

    • account share revoked: <owner> revoked access to their account; no other account can run <agent>
    • account share revoked: <owner> revoked access to their account; fallback is off for this session (launched with --no-fallback)
    • account share revoked: <owner> revoked access to their account; cannot continue on another account: <reason>, for example the session is paused
    • account share revoked: <owner> revoked access to their account; continuing on <account>'s account failed: <error>

    aether account revoke returns once the moves start. The server checks again every 30 seconds (aether-server --poll-interval) and after a restart. A session still preparing when you revoke fails with check the account share: ... before its agent starts; one whose container had started, or that was moving onto your account, checks the share once it runs and then moves or stops, its agent having started with your login. A closed session's paused container cannot reopen on it. Only the launcher's share counts: a session handed to another member (aether handoff) stays on the launcher's account.

  • Usage fallback can move their session onto your account when theirs runs out, without them picking it: your share is the consent. It never picks an account not shared with the launcher.

  • A launch checks every account it names, fallback included, when it is created, so a member cannot queue a session on an account nobody shared with them: permission denied: member <member-id> has not shared their account with you. An id that names no member gets the same answer.

  • Sharing and revoking are recorded in the audit history, readable by the sharing member and admins.

Warning

An omp share mounts your whole ~/.omp/agent read-write, because omp keeps its login in a database there. The recipient's session can plant an extension or MCP server command that runs in your next omp session with your whole home, gh token and signing key. Share omp only with someone you would give your home to.

A member's own agent definition (aether agent add) is refused on a shared account:

scheduler: harness "<name>" is your own agent definition, which runs only on your own account; on a shared account, only a server-wide definition (aether-server --agent-definitions) can declare the login it shares

Sharing needs Docker Engine 26.0 or newer. An older engine refuses every launch that mounts a path inside a member home:

runtime: docker engine API "1.44" cannot mount a path beneath a member home; that needs API 1.45 (Docker Engine 26.0) or newer

After you first share a claude or gemini account, restart your environment (Stop environment in Profile, then Open on Environment, or aether terminal stop and aether terminal) so recipients get your refreshes.

GitHub credentials and signing keys

Connecting GitHub, from the dashboard or with gh auth login and aether github connect in your environment (environment-home.md), creates an ed25519 signing key and leaves three files in your home on the server. None is in the database, and aether env save never captures the home.

File Holds
~/.config/gh/hosts.yml The gh token, in plain text.
~/.ssh/aether_signing.pub The public half of the signing key, the one registered on GitHub.
~/.gitconfig gh's credential helper for https://github.com and the commit-signing settings, with user.signingkey = key::<public key>.

The private key is never in your home. The server keeps it in signing-keys/<member-id> in its data directory (mode 0600) and lends it to your sessions and environment through a read-only SSH agent mounted at /run/aether-signing/agent.sock, which SSH_AUTH_SOCK names. git signs through it with ssh-keygen. The agent lists and signs with your key and refuses to add, remove, lock or unlock keys. Aether signs its end-of-session commits with the same key, read from the data directory.

Root in any of your sessions can read and replace the token and ~/.gitconfig, and can sign anything with your key while the session lives. It cannot copy the key or make Aether sign with another one. The token carries gh's default scopes (repo, read:org, gist) plus the admin:ssh_signing_key scope Aether needs to register the key, so an agent can push to the workspace's origin, open pull requests, and add or remove signing keys on your account: a key it registers signs commits that GitHub shows as Verified. It can also plant ~/.local/bin/gh, which then runs on your next aether github connect.

SSH_AUTH_SOCK names the signing agent in every session and environment. A workspace variable of that name has no effect, and neither aether ssh nor an editor's Remote-SSH connection forwards your local agent. A shell that starts its own ssh-agent replaces it, and git commit there fails with error: Couldn't find key in agent?.

Releases before the server-held key kept it at ~/.ssh/aether_signing. On its first start after the upgrade the server moves each such key into signing-keys/, deletes it from the home and rewrites user.signingkey, so the key already registered on GitHub keeps signing. Sessions and the environment started before the upgrade have no agent, so git commit in them fails with error: Couldn't get agent socket?. Restart the environment (aether terminal stop, then aether terminal); such a session's end-of-session commit is still signed.

To revoke the connection:

  1. Revoke the GitHub CLI authorization in GitHub's application settings. gh auth logout alone does not revoke a copied token.

  2. Remove the signing key at github.com/settings/keys, and any signing key there you did not add yourself.

  3. Delete ~/.config/gh/hosts.yml and ~/.ssh/aether_signing.pub from your home. To retire the private key, a server admin deletes signing-keys/<member-id> from the data directory; the next aether github connect creates a new one.

  4. Clear the signing settings in your environment terminal. Without the private key, every git commit in a session fails with error: Couldn't find key in agent? until you do:

    git config --global --unset commit.gpgsign
    git config --global --unset user.signingkey
    git config --global --unset gpg.format

aether env reset does none of this; it leaves the home alone.

Workspace secrets

A workspace variable marked secret (environments.md) is hidden in the settings and is an ordinary environment variable inside a session. That protects the value from someone reading the settings, not from someone who can run code in the workspace.

  • Values are stored in <data-dir>/workspace-secrets/<workspace-id>.json, plain text, mode 0600. The server administrator can read the file, and a backup of it is a credential backup. Deleting the workspace deletes it.
  • Plain variables and the setup script are readable by every member, so keep credentials out of them. A variable marked secret later stays in old database backups: change its value too.
  • Every new session container gets the secrets as environment variables, readable by every process in it and by anyone with Docker access on the host (docker inspect). Anyone who can launch or message a session in the workspace can make it print a secret into the transcript.
  • A container keeps the values it was created with, so rotate a leaked credential at its issuer.
  • Setup script output has every exact occurrence of a secret replaced with ***. An encoded or split value is not recognized, and nothing masks what the agent prints later.

The assistant

The assistant is an agent that runs in a container of its own per member, driven from the dashboard. Its Aether tools act as you, with your permissions, and it uses the account you signed it in to.

  • Its home is <data>/assistant/<member>/home and its instructions are read-only. No session, environment or other member's container mounts either, and only the assistant's container mounts its tool socket. It mounts nothing from any member's home, so a session that changes your CLAUDE.md, ~/.claude settings and hooks, ~/.codex/config.toml, .bashrc, ~/.local/bin or agent login does not change the assistant.
  • Its sign-in is its own, made in its container and kept in its home. Your sessions, and the sessions of members you share your account with, cannot read or replace it. A login file carries more than tokens: Claude Code also reads a gateway URL that receives every prompt, MCP tokens and plugin secrets from .credentials.json.
  • The one remaining writer of that home is the assistant's own agent. Each start therefore runs a new container, deletes everything in the home but the sign-in and the session files, rewrites the sign-in file to the keys a sign-in writes, and refuses the start if it removed any. Then the server refreshes the sign-in with the vendor itself and asks the vendor which account it belongs to, and refuses the start unless it is the account you signed in with. The account is recorded at the first start of either agent after you sign in, before any agent runs, so neither agent's session can rewrite the other's sign-in first. A sign-in the agent swapped for another account's, in any field, is caught at the next start.
  • What a steered agent can still do, within the one session it runs in: change what that session reads again (Claude Code rereads .credentials.json and ~/.claude/settings.json) and so point the rest of the session at another endpoint or account, and write text into the session files, which the same conversation reads back when it resumes, until New conversation.
  • The assistant does not start when the server cannot reach the vendor's sign-in service.
  • The same-account check reads your environment's login read-only and sends only its Claude access token to the profile endpoint. It is information only and gates nothing.
  • Reading runs and pausing or resuming them happen at once. Launching, messaging or stopping a session and saving your environment wait for you to approve a card. An unanswered card expires after 10 minutes, or 24 hours when a connector turn raised it. In a connector turn, auto-approval never passes a call the connector's scopes do not cover. Only your own Aether devices decide a card (assistant.confirm), and a connector its own turns' cards (see ChatGPT and Claude connectors), never the assistant's tool socket. A card approved after its turn ended runs on the server as you, at most once, and only if it still reads as the card you approved.
  • A card's push holds only its id and what it does, never a token, and never the text of a saved preference: a card that saves, forgets or sends one reads only Save a preference, Forget a saved preference or An action that uses a saved preference. No notification approves a card, because a notification shows only a summary and its actions run even on a locked phone or Mac: Review… opens the full card, which shows every parameter the call sends, and you approve there. Decline decides at once through assistant.confirm from that device with its own sign-in (its tailnet identity or edge session, or the desktop app's gateway token). Someone who sees or copies a push cannot decide the card without that device's sign-in. A card is decided once; a replayed or foreign decision gets assistant: no such pending confirmation. Always options appear only on the full card.
  • Let the assistant act without asking (auto-approve, off by default) skips the card for your own sessions. Messaging or stopping another member's session always asks. The assistant cannot turn auto-approve on or approve a card itself.
  • An approval rule skips the card for one kind of action: launches of one agent, mode, model and permission tier in one workspace on one account, messages or stops of your own sessions in one workspace, or saving your environment. The server builds the rule from the card's typed parameters, never from text the assistant or a session wrote, and checks it on every call. Only your click on a card creates one; assistant.rules and assistant.rules.revoke refuse the assistant's own calls and connectors, and its tool socket serves none of them. A rule does not limit the task or message: a steered assistant can launch any task with the agent a rule covers, or send any message to your own sessions in its workspace. Rules never expire; revoke one you no longer use.
  • Its shell and file tools never ask, inside its own container.
  • Transcripts it reads can carry injected instructions. With auto-approve off and no rules, a steered assistant still needs your click. With it on, a steered assistant can launch, message and stop your sessions; with a rule, it can do what the rule covers.

Every write call the assistant makes, such as a launch, message, pause or stop, is recorded in the audit history as assistant.

Never sign the aether CLI in inside a session or your environment terminal. Its client key would sit in your home, where every session can read it and act as you, including approving the assistant's cards.

Remote development and browser isolation

A session can have development terminals, a shared app browser (a headless Chromium companion container) and captures of both. All need permission to steer the session, reads included. A terminal runs in the live session container with the session owner's home and credentials. Taking control of a terminal or the browser only decides whose input goes in; other processes keep running. A human can take control from an agent, never the reverse.

The browser companion runs as UID 1000 with a read-only root filesystem, every capability dropped, no-new-privileges and Chromium's sandbox on. It mounts none of the checkout, your home, keys, agent logins or the Docker socket. It shares the session's network, so it reaches the app's loopback ports and everything else the session can. Chromium is driven over a pipe, not a port.

Page content is not protected: cookies, tokens, customer data, console output, URLs and anything on screen can end up in a capture. Use test accounts, never production credentials, and review a capture before you keep it. Annotate leaves out form values and redacts attributes that look like credentials, but sends visible text as it is, and only after you press Send to agent.

Retaining a capture makes it workspace evidence that anyone who can view the session can read, without permission to steer it. Notes are at most 4096 bytes; a session keeps at most 64 captures and 128 MiB, and a capture is at most 8 MiB. Retention periods are in privacy.md.

Workspace source mirrors

A mirror is an admin-configured, read-only fetch of an upstream branch into a workspace's base branch. It is separate from the publish destination, where session branches and pull requests go. A source URL must not contain credentials.

Mode Credential Where it lives
github The configuring admin's own gh login, with all its scopes. The admin's home.
public None. None.
deploy-key An ed25519 key Aether generates for the mirror. <data-dir>/mirrors/<workspace>/private_key.<generation>, mode 0600.
  • Mirroring private source shares its code with every member and run in the workspace. It does not lend them the admin's GitHub login.
  • The server administrator can copy the deploy key, and a backup of mirrors/ is a credential backup. On GitHub, install only the printed public key and leave Allow write access off.
  • Reconfiguring replaces the local key but cannot remove the old one at GitHub; revoke it there. Disabling a mirror does not log out the admin's gh.
  • Pushes to a mirrored base are refused. Only a forward refresh, or an admin adopting a new revision by its full commit SHA, moves it.
  • A failed refresh blocks the launch instead of starting from stale code (Launch and mirror failures).

The dashboard gateways

The dashboard reaches the server through one of two gateways (local-gateway.md). An edge serves no dashboard.

aether gui runs on your own machine. It listens on 127.0.0.1 only, with no flag to expose it, and every API and WebSocket request must carry a token minted for that process, which dies with it. It acts with your SSH key's authority on the server and with your own file and git access locally.

The server's own listener (web-port, see networking.md) serves tailnet devices. It binds the host's tailnet addresses only, over HTTPS with tailscaled's certificate, and has no cleartext port. It issues no token: each request is identified by its tailnet address, a tagged node is refused with 403, and requests from another site's page are refused.

Warning

Any process on the server host that connects to the host's own tailnet address is served as the node's owner, usually the admin, with no credential. A device behind a Tailscale subnet router is served as the router's owner. SSH on port 2222 has the same boundary.

On both, every API call is checked, and open connections again every few seconds, so removing or demoting a member reaches sockets already open.

The Android app is a WebView locked to one HTTPS origin. It refuses http:// addresses and certificate errors, opens other sites in the phone's browser, stays out of cloud backup and holds no credential. Safe Browsing is off because it only loads your server and the phone's browser checks every other link (privacy.md). It holds the RECORD_AUDIO permission and grants the microphone only to the dashboard's origin, when you press the dashboard's mic button; every other origin and every other WebView permission request is denied. The audio goes to your Aether server for transcription and the app does not store it.

Push notifications

A push subscription comes from a member's browser and names a URL the server posts to (networking.md). The server keeps only https endpoints, at most 16 per member, and connects only to public addresses, never its own host, LAN or tailnet. The payload is encrypted to the browser and signed with the key in <data-dir>/push/vapid_key.pem, which grants nothing on the server. A notice carries the session's title and a reason from the session.

The Android app subscribes the same way through a UnifiedPush distributor and shows only messages encrypted to its own keys. Its Decline button declines an assistant card through assistant.confirm with the phone's tailnet identity, so the app offers it only on Android 12 and later, which require the phone to be unlocked before a tap runs it. An app with notification access can still press it; the card's notification is kept off watches. The notifications are marked private; whether a locked phone shows their text is Android's sensitive-notifications setting (networking.md). The connector library adds two exported components, as every UnifiedPush app has: a receiver for the distributor's broadcasts, which acts only on a registration it made and hands the app only what decrypts with its keys, and a service the distributor binds to keep the app running while it delivers. Any app can also open Aether at a dashboard route, as an aether:// link can, or take down one of its notifications; a route is only shown, never acted on.

Terminal image uploads

You upload only bytes you pick or paste; the server never reads a path on your computer. Images (PNG, JPEG, GIF or WebP, at most 8 MiB) go to ~/.aether/terminal-images/ and other attachments (at most 64 MiB) to ~/.aether/terminal-files/, mode 0600, in the home the target container mounts. Uploading to a session needs permission to steer it and writes into its launcher's home. The files stay until removed or the member is deleted, every container that mounts the home can read them, and saved images leave them out.

Session file uploads

Upload files… in a session's Files needs permission to steer the session and writes into its checkout. An uploaded file is part of the session's changes, and so of what it commits and publishes, unless the repository ignores it. Files lists ignored files too, and anyone who can view the session can read any file in its checkout, a .env included. Keep a value viewers must not see as a workspace secret and out of files.

Dictation

sudo aether-server dictation install downloads sherpa-onnx's prebuilt engine and an ONNX model from the sherpa-onnx GitHub releases and accepts only files matching the SHA-256 sums pinned in aether-server, and pulls the engine's image by digest (dictation.md). The engine has no authentication and listens on every interface, so its container has no network: nothing reaches it, sessions included, but the server through <data-dir>/dictation/sock/engine.sock, mode 0600 in a 0700 directory. The container runs as uid 65534 with a read-only root, every capability dropped, no-new-privileges, the engine and model mounted read-only, at most 4 CPUs, 2.5 GiB of memory and 256 processes, and the engine at nice 10 (dictation.md). dictation.transcribe takes any approved member's clip; pending members, connectors and the assistant are refused, and the server queues at most four clips behind the one it transcribes, one per member, so dictation takes no more than one engine's CPU from sessions.

Agent configuration and Files

aether config push uploads agent configuration from your computer (environment-home.md). It skips credential files, *.pem, .env and .env.*, symlinks and files over 64 MiB, and leaves out any file the secret scanner flags, with no override. The server applies the same rules again. Replacing a server file that differs needs --overwrite.

Files in the dashboard reaches your whole home. Credential files are listed but cannot be read, downloaded, edited, renamed or moved: each shipped agent's login files, .env and .env.*, anything ending in .pem, everything in ~/.ssh except *.pub, ~/.config/gh/hosts.yml, ~/.git-credentials and ~/.netrc. You can still delete one or upload over it. The lock keeps credential bytes out of the browser. It is not a boundary against you, since your terminal reads the same home, and a credential under another name is not recognized. Whatever you upload or edit is visible to your environment and every session you launch.

SSH into a session

ssh <session-id>.aether (terminal.md) gives you a shell, file transfer and port forwarding inside a session's container.

  • Admission is the session shell's: membership, permission to steer the session, and a running or needs-attention session that is not paused. It is checked again every three seconds, and the connection closes when you fail it.
  • The container has no SSH daemon, port or key; the server speaks SSH itself and picks the container. -L, -D and -W forwards reach only loopback inside the session. Remote, agent and X11 forwarding are refused.
  • Sessions are not recorded: teammates see that you connected, not what you did. A session takes 16 connections (8 per member), each with 10 sessions and 64 forwards.
  • Processes you start stop when the session ends. For a paused session the stop waits for resume, and a server restart in between forgets it, leaving them running. Only Free container… on a finished session is certain to end them.
  • Your ssh must be OpenSSH 9.6 or a build patched for CVE-2023-51385. Older clients can run a command on your computer when they open a crafted *.aether host name, for example from a git submodule URL.

Warning

An editor attached over SSH (VS Code, Cursor, Zed) trusts the machine it attaches to, and the session's agent controls that machine. A compromised remote can use the connection to run code on your computer. Attach only to sessions whose task and repository you would run on your own machine; use plain ssh for the rest.

The desktop app opens only exact editor links to a session, such as vscode://vscode-remote/ssh-remote+<run-id>.aether/workspace.

SSH port forwarding

aether forward and the dashboard's OAuth sign-in forwards reach only a session (run:<run-id>, which needs permission to steer it) or your own environment (terminal). Other hosts and reverse forwarding are refused. For an OAuth link that redirects to a loopback address, the local dashboard binds that port on your machine and forwards it only to the terminal the link appeared in.

Edge remote access

The edge relays SSH for members with no tailnet or direct route to the server. It vouches for who signed in with GitHub; the server decides who is a member.

  • A server uses an edge only when its config names one. aether-server setup turns on the project's edge on a host without tailscaled, and says so.
  • Until it has a member, a server with an edge takes its first super admin by SSH key only from a loopback address: the server itself or an SSH tunnel. Everyone else claims it with sudo aether-server edge claim-code, so a scan that finds port 2222 open cannot take super admin first. A forwarder on the host that connects outside clients to 127.0.0.1:2222, such as socat, defeats this. Without an edge, a server with tailnet sign-in takes that first key only over the tailnet or from loopback, and one with neither takes it from any address.
  • SSH is encrypted end to end. The edge cannot read it or pose as your server.
  • The server pins the edge's signing key. A different key is refused until you compare fingerprints and run sudo aether-server edge trust.
  • edge-access, set only on the server host, decides what a GitHub sign-in is worth. Under approved-devices (the default) a person must also approve each new device. Under account a member's GitHub sign-in is enough, so a taken-over GitHub account or a compromised edge gets in (what an attacker can do).
  • Approve only a code read from a device you are holding. aether device approve first shows the device, the GitHub account, and the member and role it would join as. Refuse a code that would admit someone as you, an admin or a super admin. An approved device key also works on the direct SSH port, without the edge.
  • Device tokens do not expire. After you recover a taken-over GitHub account, revoke the devices it signed in (edge.md).
  • Linking a second GitHub account to a member, or merging two members, needs proof from both sign-ins and a person's approval. A merged member keeps the lower role. Do not enter a merge code you did not ask for.
  • sudo aether-server edge claim-code --admin <member-id> recovers access for an existing admin or super admin and nothing else; it raises no role.
  • The first link pins the server's identity. A server id from aether servers comes from the edge: before you confirm, compare the id and fingerprint the CLI shows with sudo aether-server edge status on the server.

The edge operator sees server ids and host names, account identities, device labels, client addresses, timing and byte counts (privacy.md).

ChatGPT and Claude connectors

A connector lets ChatGPT or Claude act as one member through the tools of the scopes they granted. Calls get that member's role checks; there is no terminal, file, settings or git-writing tool.

  • Identity: a connector's calls carry the device its pairing code was minted on, so a method that needs an approved device refuses a connector minted on a registered or pending one, under either edge-access policy. Audit records name the connector, such as ChatGPT connector con-7k2m9q4xbd.
  • Scopes: the member picks them on the consent page, not the client. read is always granted, and every member can read every session on the server, so a connector can read all of them. write (launch sessions, message, pause and resume sessions, answer questions, mark sessions reviewed, ask the assistant) is checked by default; control (decide permission requests, stop and close sessions) is not, because text an agent wrote could talk the assistant into approving or ending work. A connector serves only its scopes' tools, and the server checks every call's method against its scopes, a permission decision against control, and refuses every method outside its table (connectors.md). It can make 30 write calls at once, then one every 2 seconds.
  • aether_ask_assistant runs a turn of your assistant, in your conversation with your preferences. Its origin and the grant it belongs to come from the connector's token, never from the call. The turn reads no dashboard view context. It returns only that turn's final reply, capped at 4 KiB, with saved preferences matched word for word withheld; a paraphrase still gets out, a risk the owner accepted. Your approval rules and auto-approval pass a connector turn's calls as they do a dashboard turn's, so content the external host reads, which Aether cannot see, can steer them; a method outside the connector's scopes, as they are when the call is made, never passes without a card, and a revoked connector's waiting prompts do not run. A connector decides only cards its own turns raised, and only by echoing the card's text, the summary and every parameter whole, byte for byte. The external host's confirmation of that tool call is the human gate, as for aether_decide_approval; the echo binds it to the exact card. Deciding is its own tool, aether_decide_assistant_card, so the host asks for each decision; a member who sets that tool to Always allow lets the external model, and content it read, approve cards without them. Approving needs the scope of the method's own tool. A card over 2,000 characters, one showing a saved preference, or one whose summary holds a line break or a character not printed as itself is decided only in Aether. The assistant's tool socket cannot call assistant.ask or assistant.confirm, and no connector can create a rule or change auto-approval.
  • Audit history: a connector's write calls, refused ones included, and its creation and revocation are recorded with the member, connector, method, target and outcome, never the parameters (connectors.md). A member reads their own entries; an admin reads all.
  • A connector minted over a member SSH key or a tailnet connection has no device: revoking a device does not cut it off. aether mcp revoke does.
  • TLS ends at the edge, because ChatGPT and Claude need a public certificate. A compromised edge sees every tool call, result and token it relays, and can act as the member until the token expires (an hour), a refresh rotates it, or you revoke the connector. It cannot mint codes or tokens: your server makes them and stores only their hashes.
  • Pairing needs a single-use 40-bit code, valid for 10 minutes, from a device you already use. Wrong codes are limited to 10 a minute per client address (IPv6 /64), so one address cannot lock other members out of pairing, and to 300 a minute server-wide, which someone guessing from many addresses can reach to pause pairing for everyone while they guess. Revoking that device, removing the member or tightening edge-access cuts off their connectors. Only clients on chatgpt.com and claude.ai can connect. Claude Code's client id alone may return to http://localhost or http://127.0.0.1 on any port; PKCE keeps a code another local program catches unusable.
  • The universal URL https://<relay host>/mcp is one OAuth issuer for every server on the edge, but each server still mints, stores and checks its own codes and tokens. The edge only routes by their <server id>. prefix, so a token from one server sent with another's prefix reaches that other server, which holds no hash for it and refuses it. The edge-signed assertion the choose page posts names the GitHub account, the server, the client, its PKCE challenge and redirect URI, and lasts 5 minutes. It travels in a form body, never a URL. The server binds it to the first browser that brings it, by a hash of an edge cookie on the relay host, and only that browser's consent form can take it, once. The server remembers assertions only in memory, so it refuses one issued before it last started, or within the 30 seconds of clock skew it allows the edge after, and with it any consent form carrying one. Behind an edge at protocol 3 the assertion still arrives in the URL and is bound to no browser (edge.md). On the universal paths the edge forwards a server's redirects and forms only to itself, chatgpt.com, claude.ai and Claude Code's loopback callback, /callback on http://localhost or http://127.0.0.1 at any port. Under edge-access account it replaces the pairing code: the trust is the same as a GitHub sign-in admitting a new device, so a taken-over GitHub account or a compromised edge (or its key) can authorize a connector as that member. Under approved-devices the server ignores it and requires the code. A connector authorized this way is cut off when edge-access becomes approved-devices, its GitHub account is unbound from the member (member.identity.remove, or deleting the account at the edge), or the member is removed. It has no device and is judged as an edge device no person approved: a method that needs an approved device refuses it, and it cannot mint a pairing code or a Slack link code that would outlive these cut-offs.
  • An edge given --reviewer-account-file offers one password sign-in, the reviewer account, for an app directory's review. Anyone holding its password authorizes a connector as the member it is bound to, so a server takes it only with edge-reviewer on, under edge-access account, and only for a member who is no admin; it gets no edge session. Ten wrong passwords in a row for its login lock it for 15 minutes, which also lets anyone who knows the login keep it locked, and the 429 confirms a guessed login, so keep the login as private as the password. A new password stops only new sign-ins; unlinking the login deletes its connectors, and binding it again does not revive them. Bind it to a member of a server kept for the review: every member reads every workspace.
  • Under edge-access account the consent page has no code, so an authorization link someone started in their own ChatGPT or Claude, opened by a member signed in at the edge, is one Allow away from connecting that person's assistant as the member. The page says to allow only a connection you started yourself just now.
  • Refresh tokens rotate on every use and lapse after 30 days unused. A rotated refresh token used again a minute or more later deletes the connector.
  • Results contain text agents and other people wrote: session titles, reasons, reports, history, questions, PR titles, comments and failing check names, memory notes. Tools put it under an untrusted field, and the server's instructions tell the assistant never to follow it. Outside that field are only fixed labels, ids, states, counts, times, links, workspace and agent names, branch names, file paths and the server's own errors. ChatGPT and Claude ask you before each write tool; Aether adds no confirmation of its own. A permission request is only ever allowed or denied once: the connector never picks an "always" option, refuses a decision whose action text differs from the pending request's, and refuses any request too long to show whole, so the confirmation shows everything that is decided.
  • No tool takes a member id. Every agent starts on that member's account, falling back to accounts the member may launch on exactly as the member's own launches do.
  • write cannot end a live session: aether_mark_reviewed completes only a session whose agent reported success and awaits review.

Slack

The Slack bot acts as the member who linked a Slack account, the way a connector does: with that member's role checks, judged by the device aether slack link ran on, and with audit records naming Slack connector <Slack user id>. It holds all three scopes, since deciding a permission request takes control, and calls only the methods its buttons and replies need. Its write calls enter the audit history as a connector's do.

  • It messages runs and answers requests once. It launches, stops and closes nothing, posts in no channel, and never picks an "always" option. A request longer than 600 characters gets no buttons, so a press never decides text Slack cut.
  • Whoever controls the linked Slack account acts as you on your sessions. aether slack unlink, revoking the device the code was minted on, or removing the member cuts the link off.
  • Linking takes a one-use 40-bit code, valid for 10 minutes, sent to the bot in a direct message.
  • Agent text in its messages is escaped, so it cannot mention anyone or disguise a link.
  • The tokens sit in 0600 files under <data-dir>/slack, readable by the server administrator. The app holds chat:write and im:history, and reads no channel.
  • A link makes 30 actions at once, then one every 2 seconds.

Coordination between sessions

Each session gets its own socket for messaging other sessions (coordination.md). The socket is the session's identity, so no token enters the container.

  • A session can message its parent and subsessions, sessions editing the same files, and live sessions in its workspace that its owner may message. Never across workspaces. A viewer's sessions message nobody.
  • A message is an attributed inbox item. It gives the sender no steer, kill, terminal, git, transcript or secret access to the recipient.
  • Messages are capped at 4 KiB, 100 unread per inbox and 8 new correspondents per session outside its parent and subsessions. Sessions in one workspace share a repository, so they can still steer each other through file contents.
  • aether-server config set conflict-coordination false turns off messages between sessions and outcome reports; overlap warnings stay.
  • Each container holds a read-only copy of the server binary for the optional MCP bridge. It carries no credentials.

Team memory

Team memory holds short notes that sessions and members share in a workspace. A note is an attributed claim that can be stale, wrong or planted, never an instruction.

  • Only members pin notes (aether memory pin, which needs permission to push). Pinned notes go into every session's launch brief; other notes reach an agent only when it searches.
  • Agents can add, search and read notes, and replace only another agent's unpinned note. They cannot pin, unpin or delete. A viewer's sessions only read. A note that gitleaks flags is refused, and a flagged report is withheld.
  • aether memory rm <id> deletes a note and aether memory rm --run <session> every note a session wrote. Every change shows in the activity feed.

Updates and releases

Release signing

Anyone who can write to the repository's GitHub releases can replace their files, so updaters trust a signature instead. Each release carries aether-release.json, listing every file's SHA-256 and size, and an ed25519 signature over it. aether update, the dashboard's Update now and aether server update check the signature against keys built into the binary, require the manifest's version to match the tag, and check every download against it. install.md shows the check by hand.

aether gui build on Windows checks the desktop app zip the same way, but aether gui build --zip <path> checks only a local zip's layout: compare it with checksums.txt first.

When the signing key changes, one bridge release trusts both keys. A client older than the bridge rejects every later release: install the bridge first with aether update --version <bridge-tag> (or aether server update --version <bridge-tag>), or reinstall with the install script. A client that has not installed a release past a leaked key still trusts that key.

Server and client updates

aether server update lets a super admin replace the server's binaries without a shell on the host. That adds no power, since a super admin can already run any code on the server's Docker daemon. The version must be a v semver tag from the 3xDevOps/Aether releases; a client never supplies a URL. Both binaries are verified before either is replaced.

Install in the Docker inside sessions row of Settings → Server (admins only) runs the Sysbox installer built into the server binary as root, the same one sudo aether-server sysbox install runs; the dashboard sends no script or URL. A server that is not root refuses with the server does not run as root; on the host run: sudo aether-server sysbox install.

Client self-update on macOS

When the aether CLI lives in a directory only root can write, such as /usr/local/bin, the dashboard's Update now asks for your password in a macOS dialog titled osascript. It authorizes one fixed command that copies the download, staged in ~/Library/Caches/aether/update (mode 0700), into place as root after checking its SHA-256. Your password goes to macOS, never through Aether. A same-user process can make the update fail or stall, not install other bytes: the digest is part of the command. A mismatch afterwards reports installed <path> does not match the release checksum; do not run it. Where Aether cannot offer the dialog, the banner shows sudo aether update (local-gateway.md).

Vulnerability scanning

CI runs govulncheck (make vulncheck) on every pull request that changes code. It is advisory: a finding does not fail the build. A reachable vulnerability it reports gets a fix, and go.mod pins a patched Go toolchain for release builds.

Limits

  • Containers share the host kernel. A kernel or Docker escape reaches the host.
  • Outbound traffic is limited only for sessions of a workspace with egress control on. Other sessions and environment terminals reach the internet, your network and ports the server host listens on.
  • An egress-limited session still reaches services on the server host, and every allowed host can receive data. A client that ignores the proxy variables cannot connect at all.
  • The egress proxy tells sessions apart by source address. Root in a session with Docker inside can claim another egress-limited session's address and get that workspace's host list.
  • On Docker older than 26.0 (25.0.4 and 23.0.11 on older lines), an internal network still resolves names through the host (CVE-2024-29018), so an egress-limited session can send data out in DNS queries.
  • Containers created before the aether-agents network existed keep Docker's default bridge, where they can reach each other's ports, until they are removed.
  • Every session trusts /workspace, and each extra repo's /repos/<name>, for git through GIT_CONFIG_COUNT. An image with git older than 2.31 ignores that and can refuse the checkout when its owner differs from the agent's user. An image's own ENV GIT_CONFIG_* entries are replaced, so set them as workspace variables.
  • Commit selected in a session's changes view does not run commit hooks. Use git commit in the session's terminal when hooks must run.
  • An account share's login mount can be redirected by the owner's own containers to another file in the owner's home, never outside it.

There aren't any published security advisories