---
title: "Creating Environments"
description: "Scaffold a HUD environment, implement env.py with tasks and capabilities, test it locally with the CLI, and deploy it to the HUD platform."
icon: "hammer"
mode: "wide"
---


This guide outlines how to create an environment in the HUD format - defining the `env.py` file, registering tasks
and capabilities, doing quick local tests, and deploying the environment to our platform.

On this page: [Start](#start) · [The `env.py` file](#the-envpy-file) · [Capabilities](#capabilities) · [Lifecycle hooks](#lifecycle-hooks) · [Tasks](#tasks) · [Testing locally](#testing-the-environment-locally) · [Deploying](#deploying-to-the-platform)

## Start

Run `hud init` in your terminal. This lets you select a template to start with, the default is a blank template.

```bash
hud init my-env
cd my-env
```

Inside the blank template `my-env` you now have four files, each with one job:

<div className="filetree">
  <div className="ft-root">my-env/</div>
  <div className="ft-row"><span><span className="ft-branch">├── </span><span className="ft-name">env.py</span></span><span className="ft-note">the environment: object, capabilities, and task definitions</span></div>
  <div className="ft-row"><span><span className="ft-branch">├── </span><span className="ft-name">tasks.py</span></span><span className="ft-note">the concrete tasks to run, filled in with real arguments</span></div>
  <div className="ft-row"><span><span className="ft-branch">├── </span><span className="ft-name">Dockerfile.hud</span></span><span className="ft-note">how the environment is packaged into an image on deploy</span></div>
  <div className="ft-row"><span><span className="ft-branch">└── </span><span className="ft-name">pyproject.toml</span></span><span className="ft-note">the Python dependencies</span></div>
</div>

## The `env.py` file

`env.py` is the only file that defines behavior. In this section we'll go through each part of a full example (see it in full below).
It's an environment that gives the agent a sandboxed shell and file system, asks it to count a letter in a sentence, and scores the answer.

<Accordion title="Full env.py example">

We'll use the following `env.py` example in this section 

```python env.py
from pathlib import Path
from hud.environment import Environment

ROOT = Path("workspace")
env = Environment(name="counter")
env.workspace(ROOT)

@env.initialize
async def _setup():
    (ROOT / "instructions.txt").write_text("Count letters case-insensitively.\n")

@env.shutdown
async def _teardown():
    (ROOT / "instructions.txt").unlink(missing_ok=True)

@env.template(id="count")
async def count(sentence: str, letter: str):
    answer = yield f"How many times does '{letter}' appear in '{sentence}'?"
    yield 1.0 if str(sentence.lower().count(letter.lower())) in (answer or "") else 0.0
```

</Accordion>

<div className="guide-row">
<div className="guide-main">

<div className="part-label">1 · Imports and the environment object</div>

`env = Environment(name="counter")` creates the environment - the central object everything else
attaches to. Capabilities, lifecycle hooks, and tasks all register against `env`; it holds nothing on
its own. The `name` identifies it - how a task finds its way back to this definition, and what it's
published under when you deploy.

</div>
<div className="guide-aside">

<p className="aside-label">env.py</p>

```python
from pathlib import Path
from hud.environment import Environment

ROOT = Path("workspace")
env = Environment(name="counter")
```

</div>
</div>

<div className="guide-row">
<div className="guide-main">

<div className="part-label">2 · Capabilities</div>

`env.workspace(ROOT)` attaches a **capability**: a sandboxed shell and file system the agent reaches
over `ssh`, rooted at the `workspace/` directory next to `env.py`. 

</div>
<div className="guide-aside">

<p className="aside-label">env.py</p>

```python
env.workspace(ROOT)
```

</div>
</div>

## Capabilities

A **capability** is a connection the agent drives, and it's what keeps an environment portable: you
declare the connection, and any agent that speaks the protocol drives it with its own tools.

Above, `env.workspace(ROOT)` attached a single `ssh` capability - the common case. An environment isn't
limited to one, though: publish as many as the tasks need, and the agent sees every one. You attach them
three ways:

- in the `Environment(...)` constructor, for a service that already exists,
- with `env.workspace(root)`, the one-line shortcut for the typical shell case above,
- with `env.add_capability(...)` from a hook, for a service the environment runs itself.

That last form is how you expose a custom tool. A [FastMCP](https://gofastmcp.com) server is a common
choice: an `@env.initialize` hook starts it, waits for it to bind, then publishes its address.

```python
@env.initialize
async def _start():
    asyncio.create_task(server.run_http_async(host="127.0.0.1", port=8040))   # 1. start the server
    await asyncio.sleep(0.2)                                                   # 2. let it bind
    env.add_capability(Capability.mcp(name="tools",                           # 3. publish its address
                                      url="http://127.0.0.1:8040/mcp"))
```

Other protocols - a browser over `cdp`, a full desktop over `rfb`, a robot loop - attach the same way.
The [capabilities](/v6/reference/capabilities) page has the spin-up for each, and the
[protocol](/v6/advanced/protocol) covers how an agent discovers them over the wire.

<div className="guide-row">
<div className="guide-main">

<div className="part-label">3 · Lifecycle hooks</div>

`@env.initialize` runs once before the env serves - the place to seed files or start services - and
`@env.shutdown` runs on teardown, after the run finishes, to release them. Both are optional; here
they're dummy hooks for demonstration, writing and removing a file.

</div>
<div className="guide-aside">

<p className="aside-label">env.py</p>

```python
@env.initialize
async def _setup():
    (ROOT / "instructions.txt").write_text("Count letters case-insensitively.\n")

@env.shutdown
async def _teardown():
    (ROOT / "instructions.txt").unlink(missing_ok=True)
```

</div>
</div>

## Lifecycle hooks

When a task needs state - seeded files, a running service, a browser - you bring it up in
`@env.initialize` and release it in `@env.shutdown`. `@env.initialize` runs once before the env accepts
any connection, so by the time an agent connects, everything it needs is already in place. Because the
`env.workspace(...)` hook is registered first, the workspace directory already exists when your own
hook runs, so you can write straight into `ROOT`.

Hooks run per environment, not per task, so use them for state every task shares. The [environment
reference](/v6/reference/environment#methods) covers the full lifecycle.

<div className="guide-row">
<div className="guide-main">

<div className="part-label">4 · Tasks</div>

`@env.template(id="count")` defines the task. The first `yield` sends the prompt and pauses; when the
agent's `answer` arrives the function resumes, and the second `yield` returns the reward - `1.0` if the
count matches, else `0.0`.

</div>
<div className="guide-aside">

<p className="aside-label">env.py</p>

```python
@env.template(id="count")
async def count(sentence: str, letter: str):
    answer = yield f"How many times does '{letter}' appear in '{sentence}'?"
    yield 1.0 if str(sentence.lower().count(letter.lower())) in (answer or "") else 0.0
```

</div>
</div>

## Tasks

A **task** is HUD's unit of evaluation: a prompt the agent attempts and a reward that scores how it did.
You write one as an async generator with exactly two `yield`s:

1. **The first `yield` returns the prompt** and pauses the function while the agent works in the
   environment, driving whatever capabilities it was given. Its answer comes back into `answer`.
2. **The second `yield` returns the score** - a reward from `0.0` to `1.0`. This is where grading goes:
   compare `answer` inline, or call a [grader](#grading) to judge what the agent left behind.

`@env.template()` registers it as a **template**, not a single task: its parameters fill in per run, so
one definition describes a whole space of tasks. Calling it - `count(sentence="strawberry",
letter="r")` - mints one concrete, runnable [task](/v6/reference/tasks).

## Grading

The second yield is the reward, a number from `0.0` to `1.0`. Here it's plain Python: compare the
agent's answer to the count Python computes. You have three options as tasks grow:

| Approach | What it does |
| --- | --- |
| Plain Python | Compute the answer inline, like above. Comparison helpers (`exact_match`, `numeric_match`, ...) live in `hud.graders`. |
| Async graders | `BashGrader` scores a shell command by exit code; `LLMJudgeGrader` scores against rubric criteria. This is how you grade the state the agent left behind - a test passing, a file written. |
| Composed graders | `combine` runs several at once and weights them into one reward, each subscore visible in the trace. |

An async grader is an `await`ed call whose result you yield - `BashGrader`, for instance, runs a command
in the workspace and scores by exit code:

```python
result = await BashGrader.grade(weight=1.0, command="pytest -q", cwd=str(ROOT))
yield result.value
```

The full set lives in the [graders](/v6/reference/graders) reference; see [designing
tasks](/v6/reference/advice) for what separates good rewards from hackable ones.

## Listing tasks in `tasks.py`

`env.py` defines the shape of a task. `tasks.py` lists the concrete ones to run by calling the template
with real arguments:

```python tasks.py
from env import count

tasks = [
    count(sentence="strawberry", letter="r"),
    count(sentence="banana", letter="a"),
]
```

Each call binds one runnable task. Build them in a loop to turn one definition into a whole dataset.

## Testing the environment locally

Run an agent against the tasks. `hud eval` spawns the environment locally, runs the agent, grades each
task, and prints the reward:

```bash
hud eval tasks.py claude
```

No Docker or separate server needed - the only thing you need is a provider key for the model
(`ANTHROPIC_API_KEY`, ...), or a `HUD_API_KEY` to route through the HUD gateway instead.

<Note>
Runs on macOS, Windows, and Linux, but `ssh` sandbox isolation is **Linux-only** and `BashGrader` needs
bash (scores `0.0` on native Windows). Off Linux, get full behavior via `--runtime hud`
(see next guide for running), WSL2, or the built image (`Dockerfile.hud`).
</Note>

`hud eval` spawns a fresh env per run; to keep one serving locally for repeated runs or manual
inspection, use `hud serve`.

## Deploying to the platform

When it works locally, ship it. Deploying talks to the platform, so set your API key first:

```bash
hud set HUD_API_KEY=your-key   # get one at hud.ai
```

`hud deploy` builds the image from `Dockerfile.hud` and publishes the environment under its
`Environment(name=...)`:

```bash
hud deploy
```

To run a taskset from the platform, publish it too. `hud sync` uploads a taskset and only what changed:

```bash
hud sync tasks my-tasks   # publish tasks.py as a named taskset
hud sync env              # sync environment metadata
```

Your environment now lives on the platform, ready to run at scale and compare across models. Running
those evaluations - locally, on a cloud sandbox, or on the platform - is covered in
[evaluating agents](/v6/guides/running-an-eval).

## Export to other formats

A HUD environment isn't locked to HUD: its tasks export to other benchmark formats, like self-contained
[Harbor](/v6/advanced/harbor-convert) task folders.

## See environment examples

For more to build on, pick a different starting point with `hud init`, or read the
[cookbooks](/v6/cookbooks/index) for worked environments like a coding agent and an ops diagnostics task.
