> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runaether.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Environment Configuration

> Configure workspace setup with .aether/environment.json

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:

```json theme={"dark"}
{
  "setup": "npm install",
  "terminals": [
    { "name": "Dev Server", "command": "npm run dev" }
  ]
}
```

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.

| Field | Type | Description |
| - | - | - |
| `setup` | `string` | Shell command run once after clone, before the agent starts. Cached by seed volumes. |
| `setup_on_boot` | `string` | Shell command run on every task boot, after the seed volume is attached. For secret-dependent steps. |
| `docker_prewarm` | `string` | Shell command run only during seed builds to pull Docker images onto the seed volume. |
| `terminals` | `array` | Background processes started at boot and kept running. |
| `previews` | `object` | Per-port commands that mint authenticated development previews on demand. |

### `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](#seed-volumes-and-caching)). Put secret-dependent steps in `setup_on_boot`.

```json theme={"dark"}
{
  "setup": "npm install"
}
```

Chain commands with `&&` so later steps only run if earlier ones succeed:

```json theme={"dark"}
{
  "setup": "npm install && npm run build"
}
```

`setup` is also where a project installs any CLI the agent should have — into `~/.local/bin`, which is on the agent's `PATH` and kept by the seed. See [CLI tools in tasks](/guides/cli-tools).

### `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`.

```json theme={"dark"}
{
  "setup": "npm install && npm run build",
  "setup_on_boot": "npm run db:migrate"
}
```

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.

```json theme={"dark"}
{
  "setup": "npm install",
  "docker_prewarm": "docker compose pull"
}
```

### `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.

```json theme={"dark"}
{
  "terminals": [
    { "name": "Dev Server", "command": "npm run dev", "port": 3000 },
    { "name": "Type Check", "command": "npm run typecheck -- --watch" }
  ]
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `name` | `string` | Yes | A label for the process, 1–200 characters and not blank (shown in the UI and on PR preview comments). Names must be unique within `terminals`. |
| `command` | `string` | Yes | The shell command to run. Exact commands must be unique within `terminals`. |
| `port` | `number` | No | The port this service listens on (1–65535). Ports must be unique within `terminals`. Declaring it publishes the service's [preview URL on the task's pull request](/guides/preview-urls#preview-links-on-pull-requests). |

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.

<Note>
  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.
</Note>

### `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:

```json theme={"dark"}
{
  "previews": {
    "5173": {
      "auth_command": "bun scripts/preview-auth.ts",
      "auto": true
    }
  }
}
```

Port keys must be decimal integers from `1` to `65535`. Each entry accepts these fields:

| Field | Type | Required | Description |
| - | - | - | - |
| `auth_command` | `string` | Yes | Non-blank command that mints the development session. |
| `auto` | `boolean` | No | When `true`, the plain preview link automatically attempts to mint and apply a session on its first visit in each tab session. Defaults to `false`. |

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:

```json theme={"dark"}
{
  "port": 5173,
  "preview_origin": "https://5173-abc12345-<token>.preview.runaether.dev",
  "sibling_origins": {
    "8080": "https://8080-abc12345-<token>.preview.runaether.dev",
    "54321": "https://54321-abc12345-<token>.preview.runaether.dev"
  }
}
```

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:

```json theme={"dark"}
{ "url": "https://5173-abc12345-<token>.preview.runaether.dev/__dev/login?token=..." }
```

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`.

```json theme={"dark"}
{
  "local_storage": {
    "sb-localhost-auth-token": "<serialized session>"
  },
  "reload": true
}
```

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](/guides/preview-urls#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.

<Warning>
  **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.
</Warning>

## 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

```json theme={"dark"}
{
  "setup": "npm install",
  "terminals": [
    { "name": "Dev Server", "command": "npm run dev", "port": 3000 }
  ]
}
```

### Python / Django

```json theme={"dark"}
{
  "setup": "python -m venv .venv && .venv/bin/pip install -r requirements.txt",
  "setup_on_boot": ".venv/bin/python manage.py migrate",
  "terminals": [
    { "name": "Dev Server", "command": ".venv/bin/python manage.py runserver 0.0.0.0:8000" }
  ]
}
```

### Monorepo (Bun Workspaces)

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

```json theme={"dark"}
{
  "setup": "bun install",
  "terminals": [
    { "name": "API", "command": "cd apps/api && bun run dev", "port": 8080 },
    { "name": "Web", "command": "cd apps/web && bun run dev", "port": 3000 }
  ]
}
```

### Project with Docker Services

```json theme={"dark"}
{
  "setup": "npm install",
  "docker_prewarm": "docker compose pull",
  "setup_on_boot": "docker compose up -d postgres redis",
  "terminals": [
    { "name": "Dev Server", "command": "npm run dev" }
  ]
}
```

## 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:

```yaml theme={"dark"}
extra_hosts:
  - "host.docker.internal:host-gateway"
```

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:

```dockerfile theme={"dark"}
RUN git config --system --add safe.directory /app
```

If the build does not need VCS metadata, disable stamping instead:

```dockerfile theme={"dark"}
ENV GOFLAGS=-buildvcs=false
```

### 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:

```dockerfile theme={"dark"}
WORKDIR /app
COPY . .
# Keep /data empty; Compose mounts the shared named volume there.
```

If the image must contain files under the mount path, disable copy-up on every service that mounts the volume:

```yaml theme={"dark"}
services:
  api:
    volumes:
      - type: volume
        source: shared-data
        target: /data
        volume:
          nocopy: true
  worker:
    volumes:
      - type: volume
        source: shared-data
        target: /data
        volume:
          nocopy: true

volumes:
  shared-data:
```

## Execution Order

On a normal task boot, Aether runs the configuration in this order:

1. Clone the repository
2. Write [secret files](/guides/secret-files) to disk and inject [environment variables](/guides/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](/guides/environment-variables) and [secret files](/guides/secret-files) for sensitive values.
* **Chain setup commands with `&&`** so a failed step stops the rest.

<Warning>
  `.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.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.