# How Do I Give Each Team's Agent Its Own Tools From One Agent?

> Keep one agent and give each team a sandbox template. Save a versioned named snapshot per team with its CLI tools, MCP servers and instructions, label it with the team name, and have the agent create with fromLabels. Pin a release label to test a new version before the agent switches to it.

Source: https://sandboxer.com/guides/give-each-team-its-own-agent-sandbox-template. Updated 2026-10-27.

**Short answer:** Keep one agent and give each team a sandbox template. Save a versioned named snapshot per team with its CLI tools, MCP servers and instructions, label it with the team name, and have the agent create with fromLabels. Pin a release label to test a new version before the agent switches to it.

The agent's code stays the same for every team. What changes is the machine it works in. A web team's sandbox has a linter and a filesystem MCP server; a data team's has csvkit and a SQLite MCP server. The agent only needs to know which team asked, and it turns that into labels.

This page covers the per-team part: labels, versions, switching and rollback. For how named snapshots work, see [Turn a sandbox into a reusable template](/guides/turn-a-sandbox-into-a-reusable-template). For caching slow dependency installs, see [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot).

## What goes where

Sandboxer has two things that could hold "a team's setup". They hold different things.

| Put it in | What it holds | Pick it at create with |
| - | - | - |
| A **named snapshot** per team | The whole disk: CLI tools, MCP servers, `.mcp.json`, `AGENTS.md`, editor and linter config | `fromLabels` (or `from`) |
| A named **environment** per team | Variables, secret files, GitHub repos and credential switches ([Environments](/docs/environments)) | `environment` (`--environment` on the CLI) |
| **Labels** on each sandbox | Who it belongs to, so you can list and count a team's sandboxes | `labels` |

An environment can't install a tool. It writes variables and files and clones repos. So the tools go in the snapshot, and a team's secrets go in an environment. Keep secrets out of templates. We didn't test per-team environments for this page; everything else below ran on prod.

## Results

We ran this on prod on 2026-10-27 between 15:47 and 15:52 UTC with `small` sandboxes (2 vCPU, 4 GB). Times are single runs, wall clock from the create call to `state: ready`, polled every 0.25 s.

| Step | Time |
| - | - |
| Fresh sandbox, no template | 1.95 s |
| From the web team template (`fromLabels` team=web, release=1) | 1.95 s |
| From the data team template (`fromLabels` team=data, newest) | 1.94 s |
| Install the web team's tools in a fresh sandbox | 5.4 s |
| Install the data team's tools with `uv tool install` | 0.8 s |
| Save a versioned team template | 1.3 to 1.4 s |

In this run, starting from a template took the same time as starting fresh. Since the warm pool shipped on 2026-10-27, Backend measured fresh `small` and `default` creates from the standard image at a median of 0.69 s (90% within 1.15 s, from US-East); creates from a snapshot boot as before, so a fresh start can now be faster than a template start. Our toolsets were small, so the install time a template saves here is a few seconds. The bigger win is that every sandbox for a team has the same tools, versions and config, and that you change them in one place.

## Step 1: build and save each team's template

Each team gets a short script. This is the web team's:

```bash
#!/bin/bash
# Team "web": its CLI tools, its MCP server and its agent instructions.
set -euo pipefail
cd /home/user
sudo npm install -g --silent @biomejs/biome@2 @modelcontextprotocol/server-filesystem
cat > .mcp.json <<'J'
{ "mcpServers": { "files": { "command": "mcp-server-filesystem", "args": ["/home/user"] } } }
J
printf '# Team: web\nFormat and lint with biome before every commit.\n' > AGENTS.md
printf 'team=web\nrelease=1\n' > .team
```

Global npm installs need `sudo` in a sandbox; without it the install fails with a permissions error. The data team's script runs `uv tool install -q csvkit` and `uv tool install -q mcp-server-sqlite`, and points its `.mcp.json` at `/home/user/data.db`. The `.mcp.json` format is the project-scoped MCP file that [Claude Code reads](https://docs.anthropic.com/en/docs/claude-code/mcp); use whatever file your agent reads.

Run the script in a sandbox, then save it as a **versioned** named snapshot with labels. Labels are set through the API; the CLI has no labels flag yet ([API labels](/docs/api#labels)).

```bash
curl -sS -X POST https://sandboxer.com/api/v1/named-snapshots \
  -H "Authorization: Bearer $SANDBOXER_API_KEY" -H "Content-Type: application/json" \
  -d '{"sandboxId":"sb_xx6707mzdx","name":"teamtpl-web","versioned":true,"ifLatest":0,"keep":2,
       "labels":{"tpl":"teamtpl","team":"web","release":"1"}}'
```

```text
teamtpl-web-v1 ready in 1.4 s: {"series":"teamtpl-web","version":1,"labels":{"release":"1","team":"web","tpl":"teamtpl"}}
```

What the fields do:

- `versioned: true` saves `teamtpl-web-v1`, then `-v2` and so on, instead of replacing the template in place.
- `ifLatest` saves only if the newest version is still that number. Two people updating the same team's template at once can't overwrite each other. A second save with `ifLatest: 0` got `409 version_conflict` with `"details":{"latest":1}`.
- `keep: 2` deletes the oldest versions once the new one is ready, so you always have the current one and one to roll back to.
- `release` repeats the version number as a label so the agent can pin it. The `tpl` label keeps your templates apart from any other snapshots on the account.

To save test sandboxes, we built both teams' templates in one builder: we saved the web template, removed its tools, then installed the data team's. In practice, use a fresh builder per team.

## Step 2: the agent picks a template by label

The agent keeps one small table: team name to labels. When a request comes in, it creates the sandbox with `fromLabels` and tags it with the team:

```bash
curl -sS -X POST https://sandboxer.com/api/v1/sandboxes \
  -H "Authorization: Bearer $SANDBOXER_API_KEY" -H "Content-Type: application/json" \
  -d '{"type":"small","name":"teamtpl-2","ttlSeconds":1800,
       "fromLabels":{"tpl":"teamtpl","team":"web","release":"1"},
       "labels":{"tpl":"teamtpl","team":"web"}}'
```

`fromLabels` starts from the newest ready named snapshot that carries all of the labels. Two ways to use it:

| Agent sends | Gets | Use it when |
| - | - | - |
| `{"team":"data"}` (plus your `tpl` label) | The newest ready version, as soon as it's saved | The team wants updates right away |
| `{"team":"data","release":"2"}` | Exactly that version | You want to test a new version before the agent switches |

If nothing matches, no sandbox is created. Asking for a release that doesn't exist returned:

```json
{"ok":false,"error":{"code":"snapshot_not_found","status":404,"message":"No ready named snapshot has all of those labels."}}
```

The `labels` on the sandbox itself are separate from `fromLabels`. They let you list one team's sandboxes with `GET /sandboxes?labels=tpl=teamtpl,team=web`, which returned just `sb_o10ziqe58r ready`.

## Step 3: check what each team's agent sees

We ran the same check in a sandbox from each template. It prints the team file, which tools are on the `PATH`, and the MCP servers in `.mcp.json`:

```text
== team=web release=1
biome: yes
mcp-server-filesystem: yes
csvsql: no
mcp-server-sqlite: no
sqlite-utils: no
mcp: ["files"]
```

```text
== team=data release=2
biome: no
mcp-server-filesystem: no
csvsql: yes
mcp-server-sqlite: yes
sqlite-utils: yes
mcp: ["sqlite"]
```

Each team gets its own tools and none of the other team's. To check that an MCP server really starts, send it `initialize` and `tools/list` over stdio. The web template's filesystem server answered with its 14 tools: `read_file, read_text_file, … list_allowed_directories`.

## Step 4: update a template safely

Never edit what the agent is using. Make a new version, test it, then switch.

1. Start a sandbox from the current version and make the change. We added `uv tool install -q sqlite-utils` and set `release=2`.
2. Save with `"versioned":true,"ifLatest":1,"keep":2` and the labels `team=data, release=2`. It saved as `teamtpl-data-v2` in 1.3 s.
3. Start a sandbox with `fromLabels` `release=2` and run your smoke test. Start each MCP server; don't just check that the binary exists.
4. If it passes, change the team's row in the agent's table to `release=2`. If something goes wrong later, set it back to the previous release: `keep: 2` still has it.

Our smoke test caught a real break. The SQLite MCP server installed fine, and `command -v` found it, but it crashed on start:

```text
  File ".../mcp_server_sqlite/server.py", line 173, in main
    @server.list_resources()
     ^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Server' object has no attribute 'list_resources'
```

`uv tool install` had pulled the newest `mcp` library (2.3.0), which that server version doesn't support. Pinning the library fixed it:

```bash
uv tool install -q --force mcp-server-sqlite --with "mcp<2"
```

```text
read_query, write_query, create_table, list_tables, describe_table, append_insight
```

We saved that as `teamtpl-data-v3` with `ifLatest: 2`. Because of `keep: 2`, v1 was deleted, and v2 and v3 remained. The lesson for team templates: pin tool and library versions in the build script, and smoke-test each MCP server before you switch. Unpinned installs can change between two builds of the same script.

Saving a new version doesn't touch running sandboxes. A template is copied at create, so only sandboxes created after the switch get it.

## What it costs

Templates have no separate charge. What they store counts toward your plan's included storage, along with stopped sandboxes: 10 GB on the free trial ([Billing](/docs/billing)). With `keep: 2`, each team holds at most two versions. `GET /named-snapshots` shows each one's `storedBytes` once it has been measured; ours hadn't been measured before we deleted them.

The sandboxes cost what any sandbox costs: a `small` sandbox is $0.018/hour, billed per second, so a 10-minute agent task on one costs $0.003. Building a template means one builder sandbox for a few minutes. Delete the builder after the save; the template doesn't depend on it.

## FAQ

### Do I need a separate agent or API key per team?

No. One agent with one key can create from any team's template; the team only decides which labels it sends. Give each team its own named environment if their secrets differ.

### Should I use `from` with a name or `fromLabels`?

Either works. `from` takes an exact name such as `teamtpl-data-v2`, or a series name for its newest ready version. `fromLabels` lets the agent describe what it wants (`team`, `release`) without knowing names, and you can add more labels later, such as `os` or `size`.

### What happens to a team's running sandboxes when I save a new version?

Nothing. They keep the disk they started with. Only new sandboxes get the new version, and only once the agent asks for it.

### Can I put a team's API keys in its template?

Don't. Anyone who starts a sandbox from the template gets the file. Put secrets in a named environment per team, which keeps them versioned and separate from the tools.

### What if two teams need almost the same tools?

Build both from one base script and add each team's extras on top, then save each as its own series. Each series keeps its own versions and labels, so updating one team never changes the other.

## Related

- [Turn a sandbox into a reusable template](/guides/turn-a-sandbox-into-a-reusable-template)
- [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot)
- [What do AI app builders need a sandbox for?](/guides/sandboxes-for-ai-app-builders)
- [Environments & secrets](/docs/environments)
