# Claude Managed Agents (/docs/agents/claude-managed-agents)

<!-- agent-signals: reading_time_min: 8 · est_tokens: 4441 · updated: 2026-07-30 -->
Related: [Amp](/docs/agents/amp.md), [Claude Code](/docs/agents/claude-code.md), [Codex](/docs/agents/codex.md), [Crabbox with E2B](/docs/agents/crabbox.md), [Devin](/docs/agents/devin.md), [Devin Outposts](/docs/agents/devin-outposts.md)

[Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) can use a self-hosted environment when you want tool calls to run in your own infrastructure. Claude runs the agentic loop and reasoning process; E2B provides isolated sandbox infrastructure for the environment where tool calls execute. The reusable `E2B/claude-managed-agents-webhooks` template receives webhooks and routes each Claude Managed Agents session to a persistent E2B worker sandbox.

<Info>
  This separation is intentional for security: Claude decides what work to do, while the sandbox contains the execution environment, filesystem, tools, network access, and runtime logs.
</Info>

<Info>
  For the full source, local setup scripts, and app-owned routing examples, see the [Claude Managed Agents cookbook](https://github.com/e2b-dev/e2b-cookbook/tree/main/examples/anthropic-managed-agents).
</Info>

## Install dependencies [#install-dependencies]

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```bash  
      npm install e2b @anthropic-ai/sdk
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```bash  
      pip install e2b anthropic
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

Export the values you will pass to the webhook sandbox:

```bash
# E2B dashboard API keys: https://e2b.dev/dashboard?tab=keys
export E2B_API_KEY="..."

# Claude Console API keys: https://console.anthropic.com/settings/keys
export ANTHROPIC_API_KEY="..."

# Claude Console environments: https://platform.claude.com/workspaces/default/environments
export ANTHROPIC_ENVIRONMENT_ID="..."

# Open the self-hosted environment in the Claude Console and click Generate environment key.
export ANTHROPIC_ENVIRONMENT_KEY="..."
```

## Start the webhook sandbox [#start-the-webhook-sandbox]

Start the public template with auto-resume enabled. The template starts a webhook server on port `8000`.
Because the signing key only appears after you register a webhook endpoint, write the router config now and add the signing key to the same sandbox after registration.

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```typescript  
      import { Sandbox } from 'e2b'

      const sandbox = await Sandbox.create('E2B/claude-managed-agents-webhooks', {
        lifecycle: { onTimeout: 'pause', autoResume: true },
      })

      await sandbox.files.write([
        {
          path: '/opt/anthropic-managed-agents-js/config/e2b-api-key',
          data: `${process.env.E2B_API_KEY}\n`,
        },
        {
          path: '/opt/anthropic-managed-agents-js/config/anthropic-api-key',
          data: `${process.env.ANTHROPIC_API_KEY}\n`,
        },
        {
          path: '/opt/anthropic-managed-agents-js/config/anthropic-environment-id',
          data: `${process.env.ANTHROPIC_ENVIRONMENT_ID}\n`,
        },
        {
          path: '/opt/anthropic-managed-agents-js/config/anthropic-environment-key',
          data: `${process.env.ANTHROPIC_ENVIRONMENT_KEY}\n`,
        },
      ])

      console.log(`Webhook URL: https://${sandbox.getHost(8000)}/webhook`)
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      import os
      from e2b import Sandbox

      sandbox = Sandbox.create(
          "E2B/claude-managed-agents-webhooks",
          lifecycle={"on_timeout": "pause", "auto_resume": True},
      )

      sandbox.files.write(
          "/opt/anthropic-managed-agents-js/config/e2b-api-key",
          f"{os.environ['E2B_API_KEY']}\n",
      )
      sandbox.files.write(
          "/opt/anthropic-managed-agents-js/config/anthropic-api-key",
          f"{os.environ['ANTHROPIC_API_KEY']}\n",
      )
      sandbox.files.write(
          "/opt/anthropic-managed-agents-js/config/anthropic-environment-id",
          f"{os.environ['ANTHROPIC_ENVIRONMENT_ID']}\n",
      )
      sandbox.files.write(
          "/opt/anthropic-managed-agents-js/config/anthropic-environment-key",
          f"{os.environ['ANTHROPIC_ENVIRONMENT_KEY']}\n",
      )

      print(f"Webhook URL: https://{sandbox.get_host(8000)}/webhook")
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

<Info>
  At this point `/health` returns `200`, but `/webhook` returns `503` until the signing key is configured. Keep this sandbox running or paused; registering a different sandbox URL means writing the signing key into that sandbox instead.
</Info>

## Register the webhook [#register-the-webhook]

In the [Claude Console webhooks settings](https://platform.claude.com/settings/workspaces/default/webhooks), create a webhook endpoint using the printed URL:

```text
https://<sandbox-host>/webhook
```

Subscribe it to:

```text
session.status_run_started
```

Save the generated signing key, export it locally, then write it into the same webhook sandbox.

```bash
export ANTHROPIC_WEBHOOK_SIGNING_KEY="whsec_..."
```

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```typescript  
      await sandbox.files.write(
        '/opt/anthropic-managed-agents-js/config/anthropic-webhook-signing-key',
        `${process.env.ANTHROPIC_WEBHOOK_SIGNING_KEY}\n`,
      )
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      sandbox.files.write(
          "/opt/anthropic-managed-agents-js/config/anthropic-webhook-signing-key",
          f"{os.environ['ANTHROPIC_WEBHOOK_SIGNING_KEY']}\n",
      )
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Run an end-to-end smoke test [#run-an-end-to-end-smoke-test]

Create or select a Claude Managed Agents agent in the [Claude Console](https://platform.claude.com/workspaces/default/agents), then create a session with that agent and the same `ANTHROPIC_ENVIRONMENT_ID` you wrote into the webhook sandbox. Creating the session does not start work; the `user.message` event does.

```bash
export ANTHROPIC_AGENT_ID="agent_..."
```

Use a small shell task for the first smoke test:

```text
Use bash to echo webhook-smoke-ok. Answer only with that text.
```

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```typescript  
      import Claude from '@anthropic-ai/sdk'

      const client = new Claude({
        apiKey: process.env.ANTHROPIC_API_KEY,
      })

      const session = await client.beta.sessions.create({
        agent: process.env.ANTHROPIC_AGENT_ID!,
        environment_id: process.env.ANTHROPIC_ENVIRONMENT_ID!,
        title: 'E2B webhook smoke',
      })

      await client.beta.sessions.events.send(session.id, {
        events: [
          {
            type: 'user.message',
            content: [
              {
                type: 'text',
                text: 'Use bash to echo webhook-smoke-ok. Answer only with that text.',
              },
            ],
          },
        ],
      })

      console.log(`Session ID: ${session.id}`)
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      import os
      from anthropic import Anthropic as Claude

      client = Claude(api_key=os.environ["ANTHROPIC_API_KEY"])

      session = client.beta.sessions.create(
          agent=os.environ["ANTHROPIC_AGENT_ID"],
          environment_id=os.environ["ANTHROPIC_ENVIRONMENT_ID"],
          title="E2B webhook smoke",
      )

      client.beta.sessions.events.send(
          session.id,
          events=[
              {
                  "type": "user.message",
                  "content": [
                      {
                          "type": "text",
                          "text": "Use bash to echo webhook-smoke-ok. Answer only with that text.",
                      },
                  ],
              },
          ],
      )

      print(f"Session ID: {session.id}")
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

Claude should answer:

```text
webhook-smoke-ok
```

Then check the webhook sandbox:

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```typescript  
      const health = await fetch(`https://${sandbox.getHost(8000)}/health`)
      console.log(await health.json())

      const logs = await sandbox.commands.run(
        'tail -200 /opt/anthropic-managed-agents-js/webhook.log || true',
      )
      console.log(logs.stdout)

      const assignments = await sandbox.commands.run(
        'cat /opt/anthropic-managed-agents-js/.managed-agent-sandbox-store.json || true',
      )
      console.log(assignments.stdout)
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      import json
      import urllib.request

      with urllib.request.urlopen(f"https://{sandbox.get_host(8000)}/health") as response:
          print(json.loads(response.read()))

      logs = sandbox.commands.run(
          "tail -200 /opt/anthropic-managed-agents-js/webhook.log || true",
      )
      print(logs.stdout)

      assignments = sandbox.commands.run(
          "cat /opt/anthropic-managed-agents-js/.managed-agent-sandbox-store.json || true",
      )
      print(assignments.stdout)
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

A healthy run has:

* `/health` returning `ok: true`.
* Router logs showing `routing work` and `assigned session`.
* A session-to-sandbox assignment in the router's assignment store.
* The assigned worker sandbox's `worker.log` showing the shell tool execution and successful results posted back to Claude.
* The Claude Managed Agents session returning to `session.status_idle`.

The assignment store records the worker sandbox ID for each routed session. Connect to that sandbox and check `/opt/anthropic-managed-agents-js/worker.log` when you need the tool execution logs.

If the session stays at `requires_action`, check `/opt/anthropic-managed-agents-js/webhook.log` in the router sandbox first, then check `/opt/anthropic-managed-agents-js/worker.log` in the assigned worker sandbox. Stale environment keys, missing signing keys, archived sessions, and failed tool-result posts show up there.

## Runtime behavior [#runtime-behavior]

The worker sandbox runs tool calls with `/mnt/session` as its workdir. File tools are constrained to that workdir, skills are downloaded under `/mnt/session/skills/<name>/`, and generated artifacts should be written under `/mnt/session/outputs`.

The webhook sandbox is the router. It keeps a session-to-sandbox assignment store and starts worker sandboxes with E2B auto-resume and pause-on-timeout settings.

## Session-scoped sandboxes [#session-scoped-sandboxes]

By default, the public webhook template gives each Claude Managed Agents session its own E2B worker sandbox. Follow-up turns for the same session reconnect to the same worker and reuse its `/mnt/session` filesystem. The sandbox is the isolated execution environment, not the place where Claude's reasoning loop runs.

Use [cloud buckets](/docs/storage/cloud-buckets), [Archil](/docs/storage/archil), or [volumes](/docs/volumes) when files need to outlive a sandbox or be shared across many sandboxes.

If you want to run that router in your own service instead of in E2B, use the cookbook's [`app-webhooks/` example](https://github.com/e2b-dev/e2b-cookbook/tree/main/examples/anthropic-managed-agents/javascript/app-webhooks). It receives webhooks in your app, claims work there, and routes each session to its own E2B sandbox by default.

## Clean up [#clean-up]

Remove the webhook endpoint in the Claude Console before deleting the router sandbox. Then kill the router sandbox and any assigned worker sandboxes you no longer need.

## Related guides [#related-guides]

<CardGroup cols="3">
  <Card title="Cookbook example" icon="<svg xmlns=&#x22;http://www.w3.org/2000/svg&#x22; viewBox=&#x22;0 0 24 24&#x22; fill=&#x22;none&#x22;><path d=&#x22;M5.33333 3.00001C7.79379 2.99657 10.1685 3.88709 12 5.5V21C10.1685 19.3871 7.79379 18.4966 5.33333 18.5C3.77132 18.5 2.99032 18.5 2.64526 18.2792C2.4381 18.1466 2.35346 18.0619 2.22086 17.8547C2 17.5097 2 16.8941 2 15.6629V6.40322C2 4.97543 2 4.26154 2.54874 3.68286C3.09748 3.10418 3.65923 3.07432 4.78272 3.0146C4.965 3.00491 5.14858 3.00001 5.33333 3.00001Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M18.6667 3.00001C16.2062 2.99657 13.8315 3.88709 12 5.5V21C13.8315 19.3871 16.2062 18.4966 18.6667 18.5C20.2287 18.5 21.0097 18.5 21.3547 18.2792C21.5619 18.1466 21.6465 18.0619 21.7791 17.8547C22 17.5097 22 16.8941 22 15.6629V6.40322C22 4.97543 22 4.26154 21.4513 3.68286C20.9025 3.10418 20.3408 3.07432 19.2173 3.0146C19.035 3.00491 18.8514 3.00001 18.6667 3.00001Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/></svg>" href="https://github.com/e2b-dev/e2b-cookbook/tree/main/examples/anthropic-managed-agents">
    Full JavaScript and Python examples for polling workers, sandbox-hosted webhooks, and app-owned routing.
  </Card>

  <Card title="Sandbox persistence" icon="<svg xmlns=&#x22;http://www.w3.org/2000/svg&#x22; viewBox=&#x22;0 0 24 24&#x22; fill=&#x22;none&#x22;><path d=&#x22;M15.8787 3H11C7.22876 3 5.34315 3 4.17157 4.17157C3 5.34315 3 7.22876 3 11V13C3 16.7712 3 18.6569 4.17157 19.8284C5.34315 21 7.22876 21 11 21H13C16.7712 21 18.6569 21 19.8284 19.8284C21 18.6569 21 16.7712 21 13V8.12132C21 7.66475 21 7.43646 20.9758 7.2174C20.8924 6.4633 20.5963 5.74846 20.122 5.15629C19.9843 4.98427 19.8228 4.82285 19.5 4.5C19.1772 4.17715 19.0157 4.01573 18.8437 3.87795C18.2515 3.40366 17.5367 3.10757 16.7826 3.02421C16.5635 3 16.3353 3 15.8787 3Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M17 3.5V4C17 5.88562 17 6.82843 16.4142 7.41421C15.8284 8 14.8856 8 13 8H11C9.11438 8 8.17157 8 7.58579 7.41421C7 6.82843 7 5.88562 7 4V3.5&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M17 20.5V17C17 15.1144 17 14.1716 16.4142 13.5858C15.8284 13 14.8856 13 13 13H11C9.11438 13 8.17157 13 7.58579 13.5858C7 14.1716 7 15.1144 7 17V20.5&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/></svg>" href="/docs/sandbox/persistence">
    Pause and resume sandboxes to preserve filesystem state between runs.
  </Card>

  <Card title="Cloud buckets" icon="<svg xmlns=&#x22;http://www.w3.org/2000/svg&#x22; viewBox=&#x22;0 0 24 24&#x22; fill=&#x22;none&#x22;><path d=&#x22;M15 5H9C6.09433 5 4.64149 5 3.62036 5.73563C3.27976 5.981 2.981 6.27976 2.73563 6.62036C2 7.64149 2 9.09433 2 12C2 14.9057 2 16.3585 2.73563 17.3796C2.981 17.7202 3.27976 18.019 3.62036 18.2644C4.64149 19 6.09433 19 9 19H15C17.9057 19 19.3585 19 20.3796 18.2644C20.7202 18.019 21.019 17.7202 21.2644 17.3796C22 16.3585 22 14.9057 22 12C22 9.09433 22 7.64149 21.2644 6.62036C21.019 6.27976 20.7202 5.981 20.3796 5.73563C19.3585 5 17.9057 5 15 5Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M18 9V15&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M14 9V15&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M10 9V15&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M6 9V15&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/></svg>" href="/docs/storage/cloud-buckets">
    Store generated files outside the sandbox filesystem.
  </Card>
</CardGroup>
