Sandbox hooks

A sandbox hook runs your code at two points of a sandbox's life. on_create runs once when the sandbox is ready, before the agent starts. on_destroy runs once when the session closes. Use them to seed a workspace, or to upload what the agent produced.

The examples use MistralSandbox, the sandbox Mistral hosts. Besides the SDK, they need the cloud_user role (Requirements) and the sandbox extra: uv add "mistralai-agents[koyeb]".

Seed the workspace

Seed the workspace

Subclass agents.SandboxHook, and put an instance in the sandbox's hooks=, not the agent's. Both points get ctx, an agents.SandboxCallbackContext. Its ctx.sandbox runs commands with execute() and reads files with read_file(). Commands run in the workspace, so the files you create there are visible to the agent's tools:

from pathlib import Path

from mistralai import agents

class SeedWorkspace(agents.SandboxHook):
    async def on_create(self, ctx: agents.SandboxCallbackContext) -> None:
        result = await ctx.sandbox.execute("echo 'Run the tests with pytest.' > NOTES.md")
        if result.exit_code != 0:
            raise RuntimeError(f"seeding failed: {result.stderr}")

    async def on_destroy(self, ctx: agents.SandboxCallbackContext) -> None:
        report = await ctx.sandbox.read_file("NOTES.md")
        Path("saved-notes.md").write_bytes(report)  # in your process; deployed, upload it from the sandbox

agent = agents.Agent(
    model="mistral-large-4",
    environment=agents.MistralSandbox(hooks=[SeedWorkspace()]),
)

Each session of this agent starts with a NOTES.md file in its workspace, and copies it out of the sandbox when the session closes. A session closes at await session.cancel(), at a session limit, or, in your process, once an agent.run() without idempotency_key= ends its turn. Inside an async with agents.local_runtime() block, it closes when the block exits. See how a session ends.

To try a hook on your machine without the cloud_user role, pass the same hooks= to a local sandbox, such as agents.LocalDockerSandbox(hooks=[SeedWorkspace()]), which needs Docker.

When you deploy with Studio (self-hosted or Mistral-managed), the hook runs in code that replays: do its work through ctx.sandbox, use agents.now() and agents.sleep() for time, and keep any upload idempotent.

A failing command does not raise, so check exit_code. A slow command, such as a git clone on a sandbox with network access, can outlast the sandbox's default time limit (default_exec_timeout_seconds on MistralSandbox and LocalDockerSandbox). Give it its own:

await ctx.sandbox.execute("git clone https://github.com/acme/api.git", timeout_seconds=240)

A sandbox hook keeps no state between its two points: derive what on_destroy needs from the hook's constructor arguments, or read it back from the workspace.

To set the git identity that commits in the sandbox carry, use the built-in agents.pattern.sandbox_hook.GitIdentity hook: see Git identity. NoSandbox, the default, never creates a sandbox, so neither point runs.

Seed it from a session input

Seed it from a session input

Both points declare session inputs as a tool does, with Annotated[T, agents.Depends()]. Use an input to seed the workspace with what this session works on:

import shlex
from typing import Annotated

from mistralai import agents

class RepositoryContext(agents.SessionInput):
    input_id = "acme.repository"

    provider: str
    repository: str
    revision: str

class RecordRepository(agents.SandboxHook):
    async def on_create(
        self,
        ctx: agents.SandboxCallbackContext,
        *,
        repo: Annotated[RepositoryContext, agents.Depends()],
    ) -> None:
        line = shlex.quote(f"{repo.repository}@{repo.revision}")
        result = await ctx.sandbox.execute(f"echo {line} > REPOSITORY")
        if result.exit_code != 0:
            raise RuntimeError(f"seeding failed: {result.stderr}")

agent = agents.Agent(
    model="mistral-large-4",
    environment=agents.MistralSandbox(hooks=[RecordRepository()]),
)

Pass the input when you start the session, in an async function, continuing the example above:

repo = RepositoryContext(provider="github", repository="acme/api", revision="main")
session = await agent.start_session(session_inputs={RepositoryContext.input_id: repo})

Quote input values with shlex.quote() before you put them in a command: the shell runs whatever the string holds. The input is checked before the sandbox is created.

When a sandbox hook raises

When a sandbox hook raises

If on_create raises, the sandbox is deleted and the session fails to start. In your process you get the hook's exception. On a deployed agent, the session ends as failed, and its message names only the hook and the exception type. The deployment's logs hold the full error, so keep credentials out of exception messages. agents serve shows that log only with AGENTS_SDK_DEBUG=1. If on_destroy raises, the error is logged and the other hooks still run.

Network access: choose what the sandbox can reach.

See also: