Skip to main content
The .aether/environment.json file defines how your workspace is set up. It tells Aether what to run after cloning your repository — installing dependencies, running migrations, pulling Docker images, and starting background services like dev servers. This file lives in your repository and should be committed so every teammate and the agent get the same environment.

Quick Start

Create a .aether/environment.json file at the root of your repository:
The next time a workspace starts, Aether installs dependencies and starts your dev server in the background before the agent begins working.

Schema Reference

The file is parsed against the schema below. Only the five fields below are read; all are optional. Invalid JSON or a field of the wrong type causes boot to fail loudly with an error — it is never silently ignored.

setup

A shell command that prepares your project — installing dependencies, building assets. It runs after the repository is cloned, before the agent starts. setup runs through bash with a 10-minute timeout. It must be idempotent (safe to re-run) and must not depend on secrets, because seed builds run it without secrets present (see Seed volumes). Put secret-dependent steps in setup_on_boot.
Chain commands with && so later steps only run if earlier ones succeed:

setup_on_boot

A shell command that runs on every task boot, after the seed volume is attached. Use it for steps that depend on secrets or must run fresh each time — database migrations, private-registry authentication, environment-specific configuration. Same bash runner and 10-minute timeout as setup.
On a boot with no valid seed volume, both setup and setup_on_boot run. On a boot with a valid seed volume, setup is skipped and only setup_on_boot (and terminals) run.

docker_prewarm

A shell command that runs only during seed builds, after setup, to pull Docker images onto the seed volume so they don’t download at boot time. It is never run on a normal task boot. Docker pulls can be slow, so it gets a 15-minute timeout and is retried up to five times.

terminals

An array of background processes started automatically when the workspace boots and kept running while the agent works — dev servers, file watchers, queue workers.
Terminals are started sequentially and restarted when the workspace reboots. A terminal that fails to start is logged but does not block the others. Named terminal ports in the repository’s default-branch configuration define standard preview pins and the sticky “Preview” PR comment; catalog changes refresh both. Review links target the publishing task’s workspace. Branch-only listening ports remain available in the detected-port UI. Startup and preview-auth commands still execute from the workspace’s local checkout. Keep each declared port aligned with the port its command actually serves on.
Aether starts terminals before the agent starts; the agent does not issue those startup calls. The agent can start additional, non-persistent services with start_service. list_services identifies each service as declared or adhoc and includes its declared port. A second service cannot use a command that is already running; Aether reports the existing service so the agent can use it or stop it explicitly. Changes to a running terminal take effect after it is stopped or the workspace restarts.

previews

An object keyed by preview port. Each entry declares the repository-owned command that knows how to mint a development session for that application:
Port keys must be decimal integers from 1 to 65535. Each entry accepts these fields: The command runs from the repository root through bash -c and has a 30-second timeout. A non-zero exit, timeout, oversized output, or invalid JSON fails the authenticated-preview request loudly. The command receives exactly one JSON object on stdin. preview_origin is the target port, while sibling_origins contains the workspace’s other discovered and declared ports:
The platform passes no application credentials. Your hook uses credentials already available to code in the development VM, validates its inputs, and writes one JSON object to stdout in exactly one of these shapes:
Use url for an application-owned bootstrap route or URL fragment that establishes the browser session. The URL must use http or https, contain no URL credentials, and belong to preview_origin or one of sibling_origins.
Use local_storage for stacks whose client accepts a serialized browser session. reload is optional. This result is returned to the caller as data and is clearly marked as requiring client-side application; the platform does not silently discard it or construct a redirect wrapper. The two shapes are exclusive. Both keys, neither key, unknown fields, a URL outside the supplied preview origins, or stdout larger than 64 KiB are rejected. See Authenticated previews for CLI and web usage. With auto: true, the injected preview runtime starts the hook request while the page loads. The request is asynchronous, so application initialization can continue before the result is applied. A url result navigates to the returned URL. A local_storage result is written when the response arrives; clients that read authentication only at startup must return reload: true so they initialize again with the stored session. The runtime records the attempt in sessionStorage before making the request. Success or failure is therefore attempted only once per tab session at that preview origin; later navigations and reloads in the same tab do not mint another session.
A decorated link or auto-auth preview is a bearer capability. With automatic auth, possession of the preview URL becomes sufficient to mint an app session. This extends the existing preview model (URLs already embed a 32-character capability token) rather than weakening it, but use it only with seeded/dev data and never wire a hook to production credentials.

Seed Volumes and Caching

To avoid re-running setup on every task, Aether can pre-build a seed volume: a builder machine runs setup and docker_prewarm once, then writes a .seed-prepared marker that records a hash of the checkout’s dependency files and .aether configuration. On a later boot, if the marker matches the checkout, setup is skipped and only setup_on_boot and terminals run. If the marker is missing or the hash does not match, setup runs first. The marker records completed installation before boot commands run, so retrying a failed boot step does not repeat an already recorded installation. The environment is ready only after boot commands and terminal restoration succeed. This is why the two commands have different rules:
  • setup is the cacheable, secret-free prep that the seed captures once.
  • setup_on_boot is the per-boot work that can use secrets and is never cached.

Examples

Node.js / Next.js

Python / Django

Monorepo (Bun Workspaces)

bun install at the monorepo root resolves all workspace dependencies. Use terminals to start each app independently.

Project with Docker Services

Docker on Task VMs

Task VMs run Linux dockerd, which differs from Docker Desktop in a few important ways.

host.docker.internal does not resolve

Symptom: A container cannot reach a service on the task VM through host.docker.internal. Docker Desktop creates this hostname automatically. Linux dockerd only resolves it when the container has an explicit host-gateway mapping. Add the mapping to each Compose service that needs host access:
For docker run, pass --add-host host.docker.internal:host-gateway instead.

Go builds fail with error obtaining VCS status

Symptom: A Go dev container building from a bind-mounted checkout exits with error obtaining VCS status. The container user, often root, differs from the task VM user that owns the checkout. Git treats the mount as having dubious ownership, and Go’s build-VCS stamping fails with it. Trust only the mounted checkout path for every container user:
If the build does not need VCS metadata, disable stamping instead:

Shared named volumes fail on the first cold boot

Symptom: Several Compose services mounting the same new named volume can fail on the first cold boot with an error such as mkdir ...: file exists. When an image contains files under the volume’s mount path, dockerd copies them into a new volume on first use. Multiple services starting together can race while creating and populating the same paths. Prefer a dedicated mount path with no content baked into the image:
If the image must contain files under the mount path, disable copy-up on every service that mounts the volume:

Execution Order

On a normal task boot, Aether runs the configuration in this order:
  1. Clone the repository
  2. Write secret files to disk and inject environment variables into the agent’s process environment
  3. Run setup — skipped if a valid seed volume is attached
  4. Run setup_on_boot
  5. Start terminals in the background
  6. Start the agent
If setup exits non-zero, boot fails and the error is reported — check the workspace logs (and /tmp/setup.log inside the VM) for the full command output.

Best Practices

  • Keep setup idempotent and secret-free. It is cached and run without secrets. Put migrations, registry auth, and other secret-dependent steps in setup_on_boot.
  • Use setup for one-time prep, terminals for long-running processes. Installing dependencies is setup. Running a dev server is a terminal.
  • Don’t put secrets in this file. It is committed to your repository. Use environment variables and secret files for sensitive values.
  • Chain setup commands with && so a failed step stops the rest.
.aether/environment.json is read at workspace boot. If you change it while a workspace is running, the changes take effect on the next boot.