Environments¶
An agent is only as good as the computer it gets. Say the dependencies aren't installed, the database isn't up or the dev server died an hour ago. Then the output just gets weaker, and people blame the model.
hangar's environment specs (envs/*.yaml, spec 0006) describe the
machine: the image, the harnesses, the credentials. This page answers how to write one. The repo can also say
things about itself, in a file committed next to its code:
| What | Page |
|---|---|
.hangar/environment.yaml: how to install the repo's dependencies, what to start, which long-running processes to keep up, and which check proves a change works |
The repo's own file |
Versioned builds: hangar runs install ahead of time, in the background, and keeps the result as an image. Sandboxes for the repo start from it, with everything installed |
Builds · Build secrets and limits |
The service supervisor (hangar-agent svc): one command for the agent to see, start, restart and read the logs of the environment's processes. A process that exits is restarted, so the agent doesn't have to look after it |
The service supervisor |
| Services (Postgres, Redis...) declared as images, run beside the sandbox and reachable at their name | Services |
| Preview ports: the sandbox's owner opens a declared port in their browser, through hangar, never publicly | Preview ports |
The heavy tier: Docker inside a VM of the sandbox's own, Compose stacks under the service supervisor, and bigger resource profiles. It is for repos that test with docker compose or Testcontainers |
The heavy tier · Docker in the sandbox · Isolation |
| Agent-led setup: agents write a repo's file, hangar validates it, and it arrives as a PR | Agent-led setup · Re-runs, budget and safety |
| Root in the sandbox, Claude Code plugins, and which MCP servers are trusted | Root, plugins and MCP trust |
Environment specs¶
Specs are YAML files in envs/. They can extend each other (how extends merges):
- maps merge by key (
nulldeletes one); - lists replace the parent's (
skills,harnesses,credentials, ...); - the additive build inputs
tools.aptanddependencies.pip/npmunion; skills_remove: [name]drops an inherited skill.
The full design is spec 0006. A repo can describe its own steps in a
committed .hangar/environment.yaml (install, start, terminals, verify). hangar builds it ahead of time, and
agents start with dependencies installed and dev servers supervised (the repo's own file).
name: my-service
extends: python # bare + Python 3.12/uv/ruff/pytest; bare = Ubuntu + git/gh/tmux/rg
harnesses: [claude-code, codex] # agents this env allows: claude-code, codex, pi, antigravity
default_harness: claude-code
model: { claude-code: frontier, codex: balanced } # per harness, or one: a policy or a pinned id (harnesses.md)
tools:
apt: [postgresql-client]
mise: { go: "1.23" }
dependencies:
pip: [ruff, pytest] # installed with `uv tool install`
npm: [pnpm]
script: | # runs as the sandbox user
curl -fsSL https://example.com/install.sh | bash
skills:
- path: skills/repo-conventions # copied into ~/.claude/skills and ~/.codex/skills
- git: https://github.com/org/skills#review@main
agent_config:
instructions: | # written to each allowed harness's global instructions file
Run `make test` before you finish.
files:
.config/foo/config.toml: "key = 1"
repo:
url: https://github.com/me/my-service.git
branch: main
setup: uv sync # after clone
bake: false # true = clone + setup at build time, for instant claims
credentials: [claude-api, codex-api, gh] # each sandbox gets its harness's ones, plus non-agent ones (git)
secrets: { SENTRY_TOKEN: sentry } # env var <- credential, injected at claim
mcp: # written in each harness's own config format at claim
sentry: { url: "https://mcp.sentry.dev/mcp", headers: { Authorization: "Bearer ${SENTRY_TOKEN}" } }
resources: { cpus: 4, memory_gb: 8 }
warm_pool: 2 # keep two pre-started
model_gateway: record # model calls through hangar: the API key stays out, calls become tapes
model_gateway: record sends Claude Code's and Codex's model calls through hangar's model gateway.
The sandbox gets the gateway's URL and a per-sandbox bearer instead of the API key, and each run's calls are kept as a
tape. It needs HANGAR_GATEWAY_URL and an API key (subscription logins are refused).
hangar env render my-service prints the generated Dockerfile. hangar pack render codex prints a harness pack's
build. When the server has no builder, hangar pack publish claude-code codex pi antigravity builds and pushes packs
from your machine.
How extends merges¶
An environment spec can extends another: the shipped envs/*.yaml, a saved one, or one hangar env /
create_environment cloned. The child is laid over the parent field by field:
| Field kind | Rule | Fields |
|---|---|---|
| Scalars | the child's value replaces | image, model, verify, install, resources.cpus, ... |
| Maps | merge by key; the child's key wins; KEY: null deletes the parent's key |
mcp, secrets, env, files, agent_config.files, services, tools.mise, build_secrets |
| Component lists | the child's list replaces the parent's ([] empties it) |
skills, harnesses, credentials, terminals, ports, and every list not named below |
| Additive build inputs | union with the parent's, in order, without duplicates | tools.apt, dependencies.pip, dependencies.npm; and egress.allow (hosts add up along a chain, as before) |
Installing one more package never changes what the agent is told, so build inputs add up. What shapes the agent's
behaviour (skills, harnesses, credentials) is the child's to state whole. To drop one inherited skill without
restating the rest, name it in skills_remove:
name: python-no-conventions
extends: claude-python
skills_remove: [repo-conventions] # by the skill's name (its `name`, else its directory's)
skills_remove applies once, to the merged list, and is not inherited. Naming a skill the environment doesn't have
is an error that lists the names it has. The table is envspec.LIST_MERGE.
A repo's .hangar/environment.yaml follows it too: each field it sets replaces the environment's. Every list it may
set replaces under extends as well. An experiment variant's skills and skills_remove have the same meaning
(experiments).
Before #132 every list except harnesses, credentials, terminals and ports was appended. hangar env
migrate-merge --dry-run lists which environments resolve differently
(the safe-release upgrade).
Settings¶
| Setting | Default | |
|---|---|---|
HANGAR_ENV_BUILDS |
true |
Off: repo files are ignored and sandboxes start from the environment's image, as before |
HANGAR_ENV_BUILDS_KEEP |
5 | Builds kept per repo, environment and provider (the active one is always kept) |
HANGAR_ENV_BUILD_TIMEOUT_MINUTES |
30 | Clone, install and capture, per build |
HANGAR_ENV_BUILDS_CONCURRENCY |
2 | Builds at once |
HANGAR_ENV_SERVICE_IMAGES |
*, docker.io/library/*, mirror.gcr.io/library/* |
Images services may use |
HANGAR_ENV_SERVICES_MAX_CPUS, HANGAR_ENV_SERVICES_MAX_MEMORY_GB |
2, 2 | What a sandbox's services may add up to |
HANGAR_PREVIEW_DOMAIN |
unset (off) | <port>-<sandbox>.<domain> serves preview ports (a registrable domain of its own) |
HANGAR_PREVIEW_SESSION_HOURS |
12 | How long an opened preview stays signed in |
HANGAR_ENV_SETUP_RETRIES |
5 | Setup turns after a failed validation, each with the failure's output |
HANGAR_ENV_SETUP_MAX_COST_USD, HANGAR_ENV_SETUP_MAX_TURNS |
30, 16 | An env setup's job budget when neither the job nor the repo's policy sets one |
HANGAR_ENV_SETUP_VALIDATE_MINUTES |
45 | One validation: the sandbox, install, the services and every command |
HANGAR_ENV_SETUP_MODELS |
unset | {"claude-code": "frontier", "codex": "gpt-6-astra"}: a model or a policy per harness instead of balanced |
HANGAR_ENV_SETUP_LABEL |
hangar-env-setup |
An issue (GitHub, Jira) with this label starts an env setup |
HANGAR_TIERS heavy |
unset (off) | The heavy tier's providers, in order (kubernetes, e2b, docker) |
HANGAR_HEAVY_RUNTIME_CLASS |
kata-qemu-runtime-rs |
Kubernetes: the VM runtime heavy pods run under |
HANGAR_HEAVY_NODE_SELECTOR |
true |
Kubernetes: pin heavy pods to the sandboxes-heavy pool (hangar.dev/runtime=heavy) |
HANGAR_HEAVY_PERSON_MAX |
{"large": 3, "xlarge": 1} |
Live sandboxes per person per profile (large counts xlarge too); 0: no cap |
HANGAR_HEAVY_WARM_POOLS |
false |
Keep heavy environments' warm pools |
HANGAR_HEAVY_DOCKER_IMAGE |
docker:29.8.2-dind |
Where images get dockerd, containerd, runc and the CLI (Compose, Buildx) |
HANGAR_HEAVY_REGISTRY_MIRRORS |
[] |
dockerd's registry-mirrors (Docker Hub only), e.g. ["https://mirror.gcr.io"] |
HANGAR_HEAVY_MIRROR |
false |
hangar's read-only Docker Hub pull-through on the gateway, first among the mirrors |
HANGAR_HEAVY_MIRROR_UPSTREAM |
https://registry-1.docker.io |
Its upstream: Docker Hub, or an Artifact Registry remote repository for it |
HANGAR_HEAVY_DOCKER_PRIVILEGED |
false |
Docker provider: heavy sandboxes as privileged runc containers (root on the host) |
REST¶
| Route | Who | |
|---|---|---|
GET /api/envbuilds |
member | Every repo with builds: its active and latest build |
GET /api/envbuilds?repo=&environment=&provider= |
member | One repo's history (newest first) |
GET /api/envbuilds/{id} |
member | One build, with its config and the end of the install's output |
POST /api/envbuilds {repo, environment?, provider?} |
member | Build now, in the background |
POST /api/envbuilds/{id}/rollback |
admin | Make it active, pinned |
POST /api/envbuilds/unpin {repo, environment?, provider?} |
admin | Let new builds replace the active one again |
POST /api/envbuilds/validate {content, repo?, environment?} |
member | Check a file; nothing is saved |
POST /api/env/setup {repo, environment?, harness?, model?, notes?, mode?, job_budget?} |
admin, jobs token |
Agent-led setup: an env-setup job (as POST /api/jobs returns it) |
GET /api/sandboxes/{ref}/previews |
owner, admin | Two-minute links to its preview ports |
The environment, when not named, is the one a job for the repo would get.
Moved from this page¶
| Section | Now on |
|---|---|
| The file | The repo's own file |
| What the repo can't set | The repo's own file |
| Resolution order | The repo's own file |
| Builds | Builds |
| What the agent and you see | Builds |
| Build secrets | Build secrets and limits |
| Limits of this version | Build secrets and limits |
| Build caches | Build secrets and limits |
| Services | Services |
| Preview ports | Preview ports |
| The service supervisor | The service supervisor |
| The heavy tier | The heavy tier |
| Turning it on | The heavy tier |
| What decides | The heavy tier |
| Docker in the sandbox | Docker in the sandbox |
| Docker images | Docker in the sandbox |
| Compose stacks under the service supervisor | The service supervisor |
| Isolation | Heavy-tier isolation |
| Why Kata and not gVisor | Heavy-tier isolation |
| What the agent sees | Heavy-tier isolation |
| Agent-led setup | Agent-led setup |
| Root in the sandbox | Root, plugins and MCP trust |
| Claude Code plugins | Root, plugins and MCP trust |
| MCP servers and trust | Root, plugins and MCP trust |