# Give a CrewAI Agent a Code Interpreter That Runs in a MicroVM

> Write a CrewAI BaseTool whose _run sends the agent's Python to a Sandboxer microVM through sandboxer-sdk, then pass it in the agent's tools list. The sandbox starts on the first call and keeps files and packages between calls. CrewAI's own CodeInterpreterTool has been removed, and CrewAI now points you to a sandbox service.

Source: https://sandboxer.com/guides/crewai-code-interpreter-in-a-microvm. Updated 2026-10-27.

**Short answer:** Write a CrewAI BaseTool whose _run sends the agent's Python to a Sandboxer microVM through sandboxer-sdk, then pass it in the agent's tools list. The sandbox starts on the first call and keeps files and packages between calls. CrewAI's own CodeInterpreterTool has been removed, and CrewAI now points you to a sandbox service.

## What changed in CrewAI

CrewAI used to ship a built-in `CodeInterpreterTool`. Per [CrewAI's documentation](https://docs.crewai.com/en/tools/ai-ml/codeinterpretertool), it ran code in a Docker container (image `code-interpreter:latest`), fell back to a restricted Python environment when Docker wasn't available, and had an `unsafe_mode` that ran code directly on the host. The same page notes that the container can see the current working directory. That page now says the tool has been removed from `crewai-tools`, that the agent's `allow_code_execution` and `code_execution_mode` settings are deprecated, and that you should use a dedicated sandbox service instead.

We checked in `crewai` 1.15.30: the `Agent` fields `allow_code_execution` and `code_execution_mode` describe themselves as "Deprecated. CodeInterpreterTool is no longer available."

| | Old `CodeInterpreterTool` | The tool on this page |
| - | - | - |
| Where code runs | Docker on your machine, or a restricted Python, or the host in `unsafe_mode` | A Sandboxer microVM with its own kernel |
| What you install locally | Docker | Nothing beyond `crewai` and `sandboxer-sdk` |
| Can it see your files? | The container can see the working directory | No; only what you write into the sandbox |
| Packages | `libraries_used`; not allowed in the restricted fallback | `libraries_used`, installed with `pip` in the VM |
| State between calls | Not covered in its docs | Files and packages stay until you delete the sandbox |
| Status in current CrewAI | Removed | A custom `BaseTool`, the documented extension point |

## Install

We used Python 3.12, `crewai` 1.15.30 and `sandboxer-sdk` 0.1.1.

```bash
python3.12 -m venv venv && . venv/bin/activate
pip install crewai sandboxer-sdk
export SANDBOXER_API_KEY=sbx_live_…
```

Install `sandboxer-sdk`, not `sandboxer`: that PyPI name belongs to an unrelated project. The package imports as `sandboxer`.

## The tool

This follows CrewAI's [custom tool pattern](https://docs.crewai.com/en/learn/create-custom-tools): a `BaseTool` subclass with a pydantic `args_schema` and a `_run` method. The argument names match the old tool's (`code`, `libraries_used`), so prompts written for it still fit.

```python title="sandbox_tool.py"
import shlex
import uuid
from typing import List, Optional, Type

from crewai.tools import BaseTool
from pydantic import BaseModel, Field, PrivateAttr
from sandboxer import Sandbox, Sandboxer


class RunPythonInput(BaseModel):
    code: str = Field(..., description="Python 3 code to run. Print what you want to see.")
    libraries_used: List[str] = Field(
        default_factory=list,
        description="PyPI packages the code needs, installed with pip before it runs.",
    )


class SandboxerCodeInterpreterTool(BaseTool):
    name: str = "sandboxer_code_interpreter"
    description: str = (
        "Runs Python 3 code in an isolated cloud microVM (Ubuntu). "
        "Files and installed packages persist between calls. Returns exit code, stdout and stderr."
    )
    args_schema: Type[BaseModel] = RunPythonInput

    sandbox_name: str = "crewai-code"
    sandbox_type: str = "small"
    ttl_seconds: int = 1800
    timeout_seconds: int = 300

    _client: Optional[Sandboxer] = PrivateAttr(default=None)
    _sandbox: Optional[Sandbox] = PrivateAttr(default=None)

    def _get_sandbox(self) -> Sandbox:
        if self._sandbox is None:
            self._client = Sandboxer()  # reads SANDBOXER_API_KEY
            self._sandbox = self._client.create(
                type=self.sandbox_type, ttl_seconds=self.ttl_seconds, name=self.sandbox_name
            )  # waits until ready
        return self._sandbox

    def _run(self, code: str, libraries_used: Optional[List[str]] = None) -> str:
        sandbox = self._get_sandbox()
        if libraries_used:
            pkgs = " ".join(shlex.quote(p) for p in libraries_used)
            install = sandbox.exec(
                f"pip install -q --break-system-packages {pkgs}", timeout_seconds=self.timeout_seconds
            )
            if install.exit_code != 0:
                return f"pip install failed (exit {install.exit_code})\n{install.stderr[-2000:]}"
        path = f"crewai/{uuid.uuid4().hex[:8]}.py"
        sandbox.files.write(path, code)
        result = sandbox.exec(["python3", path], timeout_seconds=self.timeout_seconds)
        out = f"exit code {result.exit_code}\n{result.stdout}"
        if result.stderr:
            out += f"\nstderr:\n{result.stderr}"
        return out[-8000:]

    def close(self, delete: bool = False) -> None:
        if self._sandbox is not None:
            self._sandbox.delete() if delete else self._sandbox.stop()
            self._sandbox = None
```

A few choices worth knowing:

- The code goes in as a file (`files.write`) and runs as `python3 <file>`, so it is never pasted into a shell command.
- A non-zero exit is returned to the agent as text, not raised. The model sees the traceback and can fix its code.
- `ttl_seconds=1800` makes the sandbox stop itself after 30 minutes if your program never calls `close()`.
- The output is cut to the last 8,000 characters so a noisy script doesn't flood the model's context.

## Call it directly

This is the part we ran on a real `small` sandbox, without a model: three calls to the tool, the way CrewAI would make them.

```python
tool = SandboxerCodeInterpreterTool()
try:
    print(tool._run(code="import platform, os\nprint(platform.platform())\nprint('user', os.getuid())\nopen('notes.txt','w').write('42')"))
    print(tool.run(code="import os, humanize\nprint(open('notes.txt').read())\nprint(humanize.naturalsize(os.path.getsize('/usr/bin/python3')))", libraries_used=["humanize"]))
    print(tool.run(code="1/0"))
finally:
    tool.close(delete=True)
```

```text
exit code 0
Linux-6.18.56-sandboxer-x86_64-with-glibc2.39
user 1000

exit code 0
42
8.0 MB

exit code 1

stderr:
Traceback (most recent call last):
  File "/home/user/crewai/e1650c80.py", line 1, in <module>
    1/0
    ~^~
ZeroDivisionError: division by zero
```

In our single run on October 27, 2026, the first call took 4.7 seconds, including creating the sandbox. The second, which installed `humanize` and read the file the first call wrote, took 1.9 seconds. The kernel line shows the code ran in the VM, not on the machine running CrewAI.

## Attach it to an agent

Pass the tool in `tools=[...]` as in [CrewAI's tools docs](https://docs.crewai.com/en/concepts/tools):

```python
from crewai import Agent, Crew, Task
from sandbox_tool import SandboxerCodeInterpreterTool

code_tool = SandboxerCodeInterpreterTool(sandbox_name="crewai-analyst")
analyst = Agent(
    role="Data analyst",
    goal="Answer questions by writing and running Python",
    backstory="You check every number by running code.",
    tools=[code_tool],
)
task = Task(
    description="Compute the 30th Fibonacci number and the sum of primes below 10,000.",
    expected_output="Both numbers, with the code you ran.",
    agent=analyst,
)
crew = Crew(agents=[analyst], tasks=[task])

try:
    print(crew.kickoff())
finally:
    code_tool.close()   # stop: snapshot the disk and stop billing
```

We built this agent, task and crew with `crewai` 1.15.30 and confirmed the tool is registered, and that no sandbox is created until the tool is first called. We did not run `crew.kickoff()`, because it needs a model API key. So there's no model transcript here. The tool calls the model would make are the ones shown above.

## Share one sandbox or give each agent its own

One tool instance means one sandbox. Give two agents the same instance and they share files: one can write `data.csv` and the other can read it. Give each agent its own instance, with its own `sandbox_name`, when their work must stay apart.

For a crew that runs again later, keep `tool._sandbox.id`, and next time reopen it with `Sandboxer().get(sandbox_id)`, calling `resume()` first if it has stopped. The files and installed packages are still there.

## FAQ

### Do I need Docker for this?

No. The code runs in a Sandboxer microVM in the cloud, so the machine running CrewAI only needs Python and the two packages.

### What does a sandbox cost while the crew runs?

A `small` sandbox is $0.018/hour, billed per second while it runs. A stopped sandbox costs nothing, so call `close()` when the crew finishes or let the TTL stop it.

### Can the agent install packages?

Yes. List them in `libraries_used` and the tool installs them with `pip` in the VM before it runs the code. They stay installed for later calls to the same sandbox.

### Can the agent run shell commands, not just Python?

Not with this tool as written. Add a second `BaseTool` that calls `sandbox.exec(command)`, or connect the [Sandboxer MCP server](/docs/mcp) for the full tool set.

### Does it work with the async API?

Yes. `sandboxer-sdk` has `AsyncSandboxer` with the same methods, and `BaseTool` has an `_arun` method you can implement with it.

## Related

- [Python SDK](/docs/sdk-python)
- [LangChain & LangGraph](/docs/langchain)
- [MCP server](/docs/mcp)
- [Container vs microVM for untrusted code](/guides/container-vs-microvm-for-untrusted-code)
