---
title: 자체 호스팅 워커 배포
url: https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers
description: "자체 호스팅 샌드박스 워커가 작업을 가져오는 방식과 세션이 실행되는 위치를 선택하세요: 상시 실행 또는 웹훅 트리거 방식, 단일 프로세스 또는 세션당 하나의 샌드박스."
featureMetadata:
  topic:
    title: Managed Agents
    url: https://platform.claude.com/docs/en/managed-agents/overview
  status: beta
  betaHeader: managed-agents-2026-04-01
---

[빠른 시작](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes#quickstart)에서는 지속적으로 폴링하고 모든 세션을 하나의 프로세스에서 실행하는 `ant` CLI 워커 하나를 실행합니다. 이 페이지에서는 워커를 실행하는 다른 방법과 그중에서 선택하는 방법을 다룹니다.

## 배포 패턴 선택

워커를 배포할 때는 두 가지를 선택해야 합니다. 워커가 작업을 가져오는 방식과 각 세션이 실행되는 위치입니다.

**워커가 작업을 가져오는 방식:**

* **상시 실행(Always-on):** 장기 실행 프로세스가 큐를 지속적으로 폴링하며, 아웃바운드 HTTPS만 필요합니다. 가장 간단한 설정입니다.
* **웹훅 트리거(Webhook-triggered):** 핸들러가 `session.status_run_started`에서 깨어나 폴링을 시작합니다. 유휴 폴러를 피할 수 있지만, Anthropic이 접근할 수 있는 [웹훅](https://platform.claude.com/docs/ko/managed-agents/webhooks) 엔드포인트가 필요합니다.

**각 세션이 실행되는 위치:**

* **프로세스 내(In process):** 세션을 가져온 워커가 하나의 공유 작업 디렉터리에서 해당 세션의 도구 호출도 실행합니다.
* **세션당 샌드박스(Sandbox per session):** 폴러가 가져온 각 세션마다 새 샌드박스를 시작합니다. 새로운 파일 시스템, 리소스 제한 또는 세션별 네트워크 제어 등 더 강력한 격리가 필요할 때 이 방식을 선택하세요.

CLI 워커와 SDK 워커는 서로 다른 조합을 지원합니다:

| 기능                                                                                                 | `ant` CLI       | SDK (Python, TypeScript, Go) |
| -------------------------------------------------------------------------------------------------- | --------------- | ---------------------------- |
| 상시 폴링                                                                                              | 예               | 예                            |
| 웹훅 트리거                                                                                             | 아니요             | 예                            |
| 세션당 샌드박스                                                                                           | 예               | 예                            |
| [메모리 스토어](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-memory)         | 예, 기본 동기화 설정 사용 | 예, 동기화 구성 가능                 |
| [사용자 정의 도구](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-custom-tools) | 아니요             | 예                            |

모든 CLI 플래그와 SDK 옵션은 [자체 호스팅 워커 참조](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-reference)를 참조하세요. 더 세밀하게 제어하려면 [Environments Work 엔드포인트](https://platform.claude.com/docs/ko/api/beta/environments/work)를 직접 호출하여 자체 워커를 구현하세요.

## 상시 실행 워커 실행

큐를 지속적으로 폴링하는 워커를 시작합니다. 워커는 빠른 시작에서 받은 [환경 키](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes#run-your-first-session)로 인증합니다:

<CodeGroup exclude="shell:cURL, csharp, java, php, ruby">
  ```bash CLI
  ant beta:worker poll --workdir /workspace
  ```

  ```python Python
  import asyncio
  import contextlib
  import os
  import signal
  from anthropic import AsyncAnthropic
  from anthropic.lib.environments import EnvironmentWorker


  async def main() -> None:
      environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
      environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
      async with AsyncAnthropic(auth_token=environment_key) as client:
          worker = EnvironmentWorker(
              client,
              environment_id=environment_id,
              environment_key=environment_key,
              workdir="/workspace",
          )
          task = asyncio.create_task(worker.run())
          # 프로세스를 강제 종료하는 대신 태스크를 취소하면 워커가 진행 중인
          # 작업 항목을 중단하고 종료 전에 변경된 메모리 파일을 업로드할 수 있습니다.
          loop = asyncio.get_running_loop()
          for signum in (signal.SIGINT, signal.SIGTERM):
              loop.add_signal_handler(signum, task.cancel)
          with contextlib.suppress(asyncio.CancelledError):
              await task


  asyncio.run(main())
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";
  import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";

  const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
  const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
  const client = new Anthropic({ authToken: environmentKey });
  const controller = new AbortController();
  // 두 신호 중 하나로 중단하면 프로세스가 종료되기 전에 워커가 변경된 메모리 파일을 업로드하고
  // 스토어 디렉터리를 제거할 수 있습니다.
  process.once("SIGINT", () => controller.abort());
  process.once("SIGTERM", () => controller.abort());

  await new EnvironmentWorker({
    client,
    environmentId,
    environmentKey,
    workdir: "/workspace",
    signal: controller.signal
  }).run();
  ```

  <CodeGroupItem language="C#">
    C# SDK에는 `EnvironmentWorker`가 없습니다. 대신 `ant` CLI 워커를 실행하세요: `ant beta:worker poll --workdir /workspace`.
  </CodeGroupItem>

  ```go Go
  package main

  import (
  	"context"
  	"log"
  	"os"
  	"os/signal"
  	"syscall"

  	"github.com/anthropics/anthropic-sdk-go"
  	"github.com/anthropics/anthropic-sdk-go/lib/environments"
  	"github.com/anthropics/anthropic-sdk-go/option"
  )

  func main() {
  	environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY")
  	environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID")

  	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  	defer stop()

  	client := anthropic.NewClient(option.WithAuthToken(environmentKey))

  	worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
  		EnvironmentID:  environmentID,
  		EnvironmentKey: environmentKey,
  		Workdir:        "/workspace",
  	})
  	if err := worker.Run(ctx); err != nil {
  		log.Fatalf("worker: %v", err)
  	}
  }
  ```

  <CodeGroupItem language="Java">
    Java SDK에는 `EnvironmentWorker`가 없습니다. 대신 `ant` CLI 워커를 실행하세요: `ant beta:worker poll --workdir /workspace`.
  </CodeGroupItem>

  <CodeGroupItem language="PHP">
    PHP SDK에는 `EnvironmentWorker`가 없습니다. 대신 `ant` CLI 워커를 실행하세요: `ant beta:worker poll --workdir /workspace`.
  </CodeGroupItem>

  <CodeGroupItem language="Ruby">
    Ruby SDK에는 `EnvironmentWorker`가 없습니다. 대신 `ant` CLI 워커를 실행하세요: `ant beta:worker poll --workdir /workspace`.
  </CodeGroupItem>
</CodeGroup>

## 웹훅으로 워커 트리거

<Steps>
  <Step title="세션 웹훅 구독">
    [Console](https://platform.claude.com/settings/workspaces/default/webhooks)에서 `session.status_run_started` 이벤트를 수신하는 웹훅 엔드포인트를 정의하세요. 자세한 내용은 [웹훅](https://platform.claude.com/docs/ko/managed-agents/webhooks)을 참조하세요.
  </Step>

  <Step title="웹훅 서명 키 내보내기">
    [빠른 시작](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes#run-your-first-session)의 환경 ID 및 키와 함께, 핸들러 호스트에서 웹훅 서명 키를 내보내세요. 핸들러는 이 키를 사용하여 수신 페이로드를 검증합니다.

    ```bash
    export ANTHROPIC_WEBHOOK_SIGNING_KEY="whsec_..."
    ```
  </Step>

  <Step title="웹훅 핸들러 구현">
    `session.status_run_started`가 발생하면 워커를 호출하세요. 핸들러는 큐를 비우면서 가져온 각 작업 항목을 `handle_item()` (typescript: `handleItem()`; go: `HandleItem()`)에 전달하며, 이 함수는 스킬을 다운로드하고, 도구 호출을 실행하고, 결과를 다시 게시한 후 반환합니다.

    <CodeGroup exclude="shell, csharp, java, php, ruby">
      <CodeGroupItem>
        웹훅 서명을 검증하려면 webhooks extra를 설치하세요: `pip install "anthropic[webhooks]"`.

        ```python Python
        import asyncio
        import os
        import anthropic
        import standardwebhooks  # installed by the anthropic[webhooks] extra

        environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
        environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
        client = anthropic.AsyncAnthropic(
            auth_token=environment_key,
        )
        # shutdown()에 의해 취소되어, 진행 중인 작업 항목이 프로세스 종료 전에 변경된 메모리 파일을 업로드하고
        # 스토어 디렉터리를 제거할 수 있습니다.
        inflight: set[asyncio.Task[None]] = set()


        # 호스트의 종료 훅에서 이를 await 하세요. 예: ASGI lifespan 종료(FastAPI lifespan의
        # `yield` 이후 코드)로, uvicorn이 SIGTERM 시 실행합니다. uvicorn은 해당 훅 실행 전에 열린 요청이
        # 완료되도록 하므로, 대기 시간을 제한하려면 --timeout-graceful-shutdown을 설정하세요.
        async def shutdown() -> None:
            for task in inflight:
                task.cancel()
            await asyncio.gather(*inflight, return_exceptions=True)


        async def handle(raw: bytes, headers: dict[str, str]) -> tuple[dict[str, str], int]:
            try:
                event = client.beta.webhooks.unwrap(raw.decode(), headers=headers)
            except standardwebhooks.WebhookVerificationError:
                return {"error": "signature verification failed"}, 401
            if event.data.type != "session.status_run_started":
                return {"status": "ignored"}, 200
            task = asyncio.create_task(run_queued_work())
            inflight.add(task)
            task.add_done_callback(inflight.discard)
            try:
                # Shielded: 누락되거나 시간 초과된 전달이 항목을 취소해서는 안 되며, shutdown()이 취소합니다.
                await asyncio.shield(task)
            except asyncio.CancelledError:
                return {"status": "shutting down"}, 503
            return {"status": "ok"}, 200


        async def run_queued_work() -> None:
            async for work in client.beta.environments.work.poller(
                environment_id=environment_id,
                environment_key=environment_key,
                block_ms=None,
                reclaim_older_than_ms=2000,
                drain=True,
                auto_stop=False,
            ):
                await client.beta.environments.work.worker(workdir="/workspace").handle_item(
                    work_id=work.id,
                    environment_id=environment_id,
                    session_id=work.data.id,
                    environment_key=environment_key,
                    # 세션별 시크릿은 워커가 세션의 메모리 스토어를 마운트할 수 있게 해 줍니다.
                    work_secret=work.secret,
                )
        ```
      </CodeGroupItem>

      ```typescript TypeScript
      import Anthropic from "@anthropic-ai/sdk";

      const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
      const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
      const client = new Anthropic({
        authToken: environmentKey
      });
      // 호스트의 SIGTERM/SIGINT 핸들러에서 서버를 닫는 것과 함께 shutdown.abort()를 호출하고,
      // 종료 전에 진행 중인 handle() 호출을 기다리세요: abort는 실행 중인 작업 항목이 먼저
      // 변경된 메모리 파일을 업로드하고 스토어 디렉터리를 제거할 수 있게 합니다.
      export const shutdown = new AbortController();

      export async function handle(req: Request): Promise<Response> {
        // 여기서 실행되지 않을 작업의 전달은 절대 확인 응답하지 마세요; 503은 발신자가 재시도하게 합니다.
        if (shutdown.signal.aborted) {
          return Response.json({ status: "shutting down" }, { status: 503 });
        }
        const body = await req.text();
        let event;
        try {
          event = client.beta.webhooks.unwrap(body, { headers: Object.fromEntries(req.headers) });
        } catch {
          return new Response("signature verification failed", { status: 401 });
        }
        if (event.data.type !== "session.status_run_started") {
          return Response.json({ status: "ignored" });
        }

        for await (const work of client.beta.environments.work.poller({
          environmentId,
          environmentKey,
          blockMs: null,
          reclaimOlderThanMs: 2000,
          drain: true,
          autoStop: false,
          signal: shutdown.signal
        })) {
          await client.beta.environments.work.worker({ workdir: "/workspace" }).handleItem({
            workId: work.id,
            environmentId,
            sessionId: work.data.id,
            environmentKey,
            // 세션별 시크릿은 워커가 세션의 메모리 스토어를 마운트할 수 있게 해 줍니다.
            workSecret: work.secret ?? undefined,
            signal: shutdown.signal
          });
        }
        // 폴러와 handleItem은 abort 시 조용히 반환하므로, 중단된 드레인은 여기에 도달합니다.
        if (shutdown.signal.aborted) {
          return Response.json({ status: "shutting down" }, { status: 503 });
        }
        return Response.json({ status: "ok" });
      }
      ```

      <CodeGroupItem language="C#">
        C# SDK에는 `EnvironmentWorker`가 없습니다. 자체 코드에서 작업 항목을 처리하려면 [Environments Work 엔드포인트](https://platform.claude.com/docs/ko/api/beta/environments/work)를 호출하세요.
      </CodeGroupItem>

      ```go Go
      package main

      import (
      	"context"
      	"encoding/json"
      	"errors"
      	"io"
      	"log/slog"
      	"net/http"
      	"os"
      	"os/signal"
      	"syscall"

      	"github.com/anthropics/anthropic-sdk-go"
      	"github.com/anthropics/anthropic-sdk-go/lib/environments"
      	"github.com/anthropics/anthropic-sdk-go/option"
      	"github.com/anthropics/anthropic-sdk-go/packages/param"
      )

      var (
      	environmentKey = os.Getenv("ANTHROPIC_ENVIRONMENT_KEY")
      	environmentID  = os.Getenv("ANTHROPIC_ENVIRONMENT_ID")
      	client         = anthropic.NewClient(
      		option.WithAuthToken(environmentKey),
      		option.WithWebhookKey(os.Getenv("ANTHROPIC_WEBHOOK_SIGNING_KEY")),
      	)
      	worker = environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
      		Workdir: "/workspace",
      	})
      	// SIGINT 또는 SIGTERM 시 취소되어(main에서 설정) 진행 중인 작업 항목이 종료 전에
      	// 변경된 메모리 파일을 업로드하고 스토어 디렉터리를 제거할 수 있습니다.
      	shutdown context.Context
      )

      func handle(w http.ResponseWriter, r *http.Request) {
      	body, err := io.ReadAll(r.Body)
      	if err != nil {
      		http.Error(w, "bad request", http.StatusBadRequest)
      		return
      	}
      	event, err := client.Beta.Webhooks.Unwrap(body, r.Header)
      	if err != nil {
      		http.Error(w, "signature verification failed", http.StatusUnauthorized)
      		return
      	}
      	if event.Data.Type != "session.status_run_started" {
      		json.NewEncoder(w).Encode(map[string]string{"status": "ignored"})
      		return
      	}

      	// Go SDK는 RunOne 편의 기능을 제공하지 않습니다: WorkPoller로 대기 중인 항목을
      	// 비우고 각 항목을 HandleItem으로 실행하세요.
      	// r.Context()에서 분리: 세션이 웹훅 전달 타임아웃보다 오래 지속될 수 있습니다.
      	// 프로세스 전역 종료 컨텍스트는 SIGTERM 시 여전히 항목을 깔끔하게 종료합니다.
      	ctx := shutdown
      	poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{
      		EnvironmentID:      environmentID,
      		EnvironmentKey:     environmentKey,
      		BlockMs:            param.Null[int64](),
      		ReclaimOlderThanMs: param.NewOpt[int64](2000),
      		Drain:              true,
      		AutoStop:           param.NewOpt(false),
      	})
      	defer poller.Close()
      	for poller.Next() {
      		item := poller.Current()
      		if err := worker.HandleItem(ctx, environments.HandleItemOptions{
      			WorkID:         item.ID,
      			EnvironmentID:  item.EnvironmentID,
      			SessionID:      item.Data.ID,
      			EnvironmentKey: environmentKey,
      			// 세션별 시크릿은 워커가 세션의 메모리 스토어를 마운트할 수 있게 해 줍니다.
      			WorkSecret: item.Secret,
      		}); err != nil {
      			slog.Error("handle work item", "work_id", item.ID, "err", err)
      			http.Error(w, "internal error", http.StatusInternalServerError)
      			return
      		}
      	}
      	if err := poller.Err(); err != nil {
      		slog.Error("poll work queue", "err", err)
      		http.Error(w, "internal error", http.StatusInternalServerError)
      		return
      	}
      	json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
      }

      func main() {
      	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
      	defer stop()
      	shutdown = ctx

      	server := &http.Server{Addr: ":8080"}
      	http.HandleFunc("POST /webhook", handle)
      	go func() {
      		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
      			slog.Error("http server", "err", err)
      			os.Exit(1)
      		}
      	}()
      	// 시그널 수신 시 전달 수락을 중단하고, 진행 중인 핸들러와 그에 따른
      	// 작업 항목의 메모리 정리가 완료된 후에만 반환합니다.
      	<-ctx.Done()
      	if err := server.Shutdown(context.Background()); err != nil {
      		slog.Error("http shutdown", "err", err)
      	}
      }
      ```

      <CodeGroupItem language="Java">
        Java SDK에는 `EnvironmentWorker`가 없습니다. 자체 코드에서 작업 항목을 처리하려면 [Environments Work 엔드포인트](https://platform.claude.com/docs/ko/api/beta/environments/work)를 호출하세요.
      </CodeGroupItem>

      <CodeGroupItem language="PHP">
        PHP SDK에는 `EnvironmentWorker`가 없습니다. 자체 코드에서 작업 항목을 처리하려면 [Environments Work 엔드포인트](https://platform.claude.com/docs/ko/api/beta/environments/work)를 호출하세요.
      </CodeGroupItem>

      <CodeGroupItem language="Ruby">
        Ruby SDK에는 `EnvironmentWorker`가 없습니다. 자체 코드에서 작업 항목을 처리하려면 [Environments Work 엔드포인트](https://platform.claude.com/docs/ko/api/beta/environments/work)를 호출하세요.
      </CodeGroupItem>
    </CodeGroup>

    핸들러가 직접 작업을 가져오므로, 여기서 `work_secret` (typescript: `workSecret`; go: `WorkSecret`) 인수가 하는 것처럼 [작업 항목의 시크릿을 전달](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers#forward-the-work-items-secret)해야 합니다.
  </Step>
</Steps>

이 핸들러는 가져온 모든 항목을 하나의 호스트에서 하나의 프로세스로 실행합니다. 세션들이 동일한 메모리 스토어를 연결하는 경우 [스토어를 공유하는 세션 격리하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-memory#isolate-sessions-that-share-a-store)를 참조하세요.

## 세션당 하나의 샌드박스 실행

호스트의 폴러가 작업을 가져오고 작업 항목마다 한 번씩 스크립트를 호출합니다. 스크립트는 해당 세션 하나를 위한 샌드박스를 시작합니다.

<Steps>
  <Step title="샌드박스 이미지 빌드">
    `ant`를 설치하고 `ant beta:worker run`을 엔트리포인트로 설정하세요. 샌드박스가 시작되면 환경 변수에서 세션 세부 정보를 읽고, 해당 세션을 처리한 후 종료됩니다. 베이스 이미지는 `/bin/bash`를 제공해야 하며, `curl`은 빌드 시에만 사용됩니다.

    ```dockerfile
    FROM your-base-image
    ARG ANT_VERSION=1.42.0
    ARG TARGETARCH
    RUN ARCH=$([ "$TARGETARCH" = "arm64" ] && echo arm64 || echo amd64) && \
        curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v${ANT_VERSION}/ant_${ANT_VERSION}_linux_${ARCH}.tar.gz" \
          | tar -xz -C /usr/local/bin ant
    WORKDIR /workspace
    VOLUME /workspace
    ENTRYPOINT ["ant", "beta:worker", "run"]
    ```
  </Step>

  <Step title="스폰 스크립트 작성">
    스크립트는 세션 세부 정보를 새 샌드박스로 전달합니다. 폴러 호스트에 `jq`가 필요합니다.

    ```bash
    #!/bin/bash
    # spawn.sh: 할당받은 작업 항목마다 한 번씩 호출됩니다
    # 할당받은 작업 항목은 stdin을 통해 JSON으로 전달됩니다.
    ANTHROPIC_WORK_SECRET="$(jq -r '.secret // empty')"
    export ANTHROPIC_WORK_SECRET
    mkdir -p "/host/outputs/$ANTHROPIC_SESSION_ID"
    exec docker run --rm \
      -e ANTHROPIC_SESSION_ID -e ANTHROPIC_ENVIRONMENT_KEY \
      -e ANTHROPIC_WORK_ID -e ANTHROPIC_ENVIRONMENT_ID -e ANTHROPIC_BASE_URL \
      -e ANTHROPIC_WORK_SECRET \
      -v "/host/outputs/$ANTHROPIC_SESSION_ID":/workspace \
      your-image
    ```

    폴러는 스크립트가 전달하는 `ANTHROPIC_*` 변수를 설정하지만, 시크릿은 예외입니다. [환경 변수](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-reference#environment-variables)를 참조하세요.

    `/host/outputs`는 사용자가 선택하는 호스트 디렉터리입니다. 이를 `/workspace`에 마운트하면 샌드박스가 종료된 후 세션의 결과물을 가져올 수 있습니다. 이 마운트에는 다운로드된 `skills/` 트리와 모든 중간 파일도 포함됩니다.
  </Step>

  <Step title="폴러 시작">
    ```bash
    ant beta:worker poll --on-work ./spawn.sh
    ```
  </Step>
</Steps>

### 작업 항목의 시크릿 전달

가져온 각 작업 항목에는 Anthropic이 발급하는 세션별 `secret` (csharp, go: `Secret`)이 포함될 수 있습니다. 세션을 실행하는 워커는 [메모리 스토어](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-memory)를 마운트하기 위해 이 시크릿이 필요합니다.

하나의 프로세스에서 세션을 가져오고 실행하는 워커(`--on-work` 없이 실행하는 `ant beta:worker poll`, 또는 `run()` (go: `Run()`)을 사용하는 `EnvironmentWorker`)는 시크릿을 자체적으로 전달합니다. 작업 가져오기와 워커 사이에 사용자 코드가 있는 경우에는 직접 전달해야 합니다:

| 작업을 가져오는 방법                                               | 시크릿이 도착하는 형태                               | 워커에 전달하는 방법                                                                                                                                                             |
| --------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ant beta:worker poll --on-work`                          | 스크립트의 표준 입력으로 들어오는 작업 항목 JSON의 `secret` 필드 | 샌드박스 환경의 `ANTHROPIC_WORK_SECRET`                                                                                                                                        |
| SDK의 `work.poller()` (go: `environments.NewWorkPoller()`) | 가져온 각 작업 항목의 `secret` (go: `Secret`) 필드    | 샌드박스 환경의 `ANTHROPIC_WORK_SECRET`, 또는 `handle_item()` (typescript: `handleItem()`; go: `HandleItem()`)에 대한 `work_secret` (typescript: `workSecret`; go: `WorkSecret`) 인수 |

시크릿은 해당 세션을 처리하는 샌드박스에만 전달하고, 절대 로그에 기록하지 마세요. 시크릿과 환경 키의 관계는 [보안 모델](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-security)을 참조하세요.

### SDK 폴러에서 샌드박스 시작

`ant beta:worker poll --on-work` 대신 자체 코드에서 작업을 가져오려면 `work.poller()` (go: `environments.NewWorkPoller()`)를 사용하세요. 이 함수는 큐를 폴링하여 가져온 각 세션을 제공하며, 샌드박스는 사용자가 시작합니다:

<CodeGroup exclude="shell, csharp, java, php, ruby">
  <CodeGroupItem language="cURL">
    루프에서 작업을 가져오고 각 세션마다 샌드박스를 시작하려면 단일 셸 명령보다 더 많은 로직이 필요합니다. 셸에서는 [세션당 하나의 샌드박스 실행](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers#run-one-sandbox-per-session)에서처럼 스폰 스크립트와 함께 `ant beta:worker poll --on-work`를 실행하세요.
  </CodeGroupItem>

  <CodeGroupItem language="CLI">
    셸에서 작업을 가져오고 각 세션마다 샌드박스를 시작하려면 [세션당 하나의 샌드박스 실행](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers#run-one-sandbox-per-session)에서처럼 스폰 스크립트와 함께 `ant beta:worker poll --on-work`를 실행하세요.
  </CodeGroupItem>

  ```python Python
  import asyncio
  import os

  from anthropic import AsyncAnthropic
  from anthropic.types.beta.environments import BetaSelfHostedWork

  SANDBOX_ENV = (
      "ANTHROPIC_ENVIRONMENT_ID",
      "ANTHROPIC_ENVIRONMENT_KEY",
      "ANTHROPIC_WORK_ID",
      "ANTHROPIC_SESSION_ID",
      "ANTHROPIC_WORK_SECRET",
      "ANTHROPIC_BASE_URL",  # forwarded only when set on this host
  )


  async def launch_container(work: BetaSelfHostedWork) -> None:
      print(f"claimed session {work.data.id}")
      # `docker run`을 자체 샌드박스 실행기로 바꾸세요. 환경
      # 키(API 키는 절대 아님)와 작업 항목의 세션별 시크릿을 전달하세요. 내부의 worker는
      # 세션의 메모리 저장소를 마운트하려면 이 시크릿이 필요합니다.
      env = os.environ | {
          "ANTHROPIC_WORK_ID": work.id,
          "ANTHROPIC_SESSION_ID": work.data.id,
          "ANTHROPIC_WORK_SECRET": work.secret or "",
      }
      forward = [arg for name in SANDBOX_ENV for arg in ("-e", name)]
      launcher = await asyncio.create_subprocess_exec(
          "docker", "run", "--rm", "--detach", *forward, "your-image", env=env
      )
      await launcher.wait()


  async def main() -> None:
      environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
      environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
      async with AsyncAnthropic(auth_token=environment_key) as client:
          async for work in client.beta.environments.work.poller(
              environment_id=environment_id,
              environment_key=environment_key,
              auto_stop=False,  # the launched sandbox owns the stop call
          ):
              await launch_container(work)


  asyncio.run(main())
  ```

  ```typescript TypeScript
  import { spawn } from "node:child_process";
  import { once } from "node:events";
  import Anthropic from "@anthropic-ai/sdk";
  import { WorkPoller } from "@anthropic-ai/sdk/helpers/beta/environments";
  import type { BetaSelfHostedWork } from "@anthropic-ai/sdk/resources/beta/environments";

  const SANDBOX_ENV = [
    "ANTHROPIC_ENVIRONMENT_ID",
    "ANTHROPIC_ENVIRONMENT_KEY",
    "ANTHROPIC_WORK_ID",
    "ANTHROPIC_SESSION_ID",
    "ANTHROPIC_WORK_SECRET",
    "ANTHROPIC_BASE_URL" // forwarded only when set on this host
  ];

  const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
  const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
  const client = new Anthropic({ authToken: environmentKey });

  async function launchContainer(work: BetaSelfHostedWork): Promise<void> {
    console.log(`claimed session ${work.data.id}`);
    // `docker run`을 자체 샌드박스 실행기로 바꾸세요. 환경
    // 키(API 키는 절대 안 됨)와 작업 항목의 세션별 시크릿을 전달하세요. 내부 워커는
    // 세션의 메모리 저장소를 마운트하려면 이 시크릿이 필요합니다.
    const env = {
      ...process.env,
      ANTHROPIC_WORK_ID: work.id,
      ANTHROPIC_SESSION_ID: work.data.id,
      ANTHROPIC_WORK_SECRET: work.secret ?? ""
    };
    const forward = SANDBOX_ENV.flatMap((name) => ["-e", name]);
    const launcher = spawn("docker", ["run", "--rm", "--detach", ...forward, "your-image"], {
      env,
      stdio: "inherit"
    });
    await once(launcher, "close");
  }

  const poller = new WorkPoller({
    client,
    environmentId,
    environmentKey,
    autoStop: false // the launched sandbox owns the stop call
  });

  for await (const work of poller) {
    await launchContainer(work);
  }
  ```

  <CodeGroupItem language="C#">
    C# SDK에는 작업을 폴링하는 헬퍼가 없습니다. 자체 코드에서 작업을 가져오려면 [Environments Work 엔드포인트](https://platform.claude.com/docs/ko/api/beta/environments/work)를 호출하세요.
  </CodeGroupItem>

  ```go Go
  package main

  import (
  	"context"
  	"fmt"
  	"log"
  	"os"
  	"os/exec"

  	"github.com/anthropics/anthropic-sdk-go"
  	"github.com/anthropics/anthropic-sdk-go/lib/environments"
  	"github.com/anthropics/anthropic-sdk-go/option"
  	"github.com/anthropics/anthropic-sdk-go/packages/param"
  )

  var sandboxEnv = []string{
  	"ANTHROPIC_ENVIRONMENT_ID",
  	"ANTHROPIC_ENVIRONMENT_KEY",
  	"ANTHROPIC_WORK_ID",
  	"ANTHROPIC_SESSION_ID",
  	"ANTHROPIC_WORK_SECRET",
  	"ANTHROPIC_BASE_URL", // forwarded only when set on this host
  }

  func launchContainer(ctx context.Context, work *anthropic.BetaSelfHostedWork) error {
  	fmt.Printf("claimed session %s\n", work.Data.ID)
  	// `docker run`을 자체 샌드박스 실행기로 바꾸세요. 환경
  	// 키(API 키는 절대 아님)와 작업 항목의 세션별 시크릿을 전달하세요. 내부의 워커는
  	// 세션의 메모리 저장소를 마운트하려면 이 시크릿이 필요합니다.
  	args := []string{"run", "--rm", "--detach"}
  	for _, name := range sandboxEnv {
  		args = append(args, "-e", name)
  	}
  	launcher := exec.CommandContext(ctx, "docker", append(args, "your-image")...)
  	launcher.Env = append(os.Environ(),
  		"ANTHROPIC_WORK_ID="+work.ID,
  		"ANTHROPIC_SESSION_ID="+work.Data.ID,
  		"ANTHROPIC_WORK_SECRET="+work.Secret,
  	)
  	launcher.Stdout, launcher.Stderr = os.Stdout, os.Stderr
  	return launcher.Run()
  }

  func main() {
  	environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID")
  	environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY")

  	client := anthropic.NewClient(option.WithAuthToken(environmentKey))

  	ctx := context.Background()

  	poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{
  		EnvironmentID:  environmentID,
  		EnvironmentKey: environmentKey,
  		AutoStop:       param.NewOpt(false), // the launched sandbox owns the stop call
  	})
  	defer poller.Close()

  	for work, err := range poller.All() {
  		if err != nil {
  			log.Fatal(err)
  		}
  		if err := launchContainer(ctx, work); err != nil {
  			log.Fatal(err)
  		}
  	}
  }
  ```

  <CodeGroupItem language="Java">
    Java SDK에는 작업을 폴링하는 헬퍼가 없습니다. 자체 코드에서 작업을 가져오려면 [Environments Work 엔드포인트](https://platform.claude.com/docs/ko/api/beta/environments/work)를 호출하세요.
  </CodeGroupItem>

  <CodeGroupItem language="PHP">
    PHP SDK에는 작업을 폴링하는 헬퍼가 없습니다. 자체 코드에서 작업을 가져오려면 [Environments Work 엔드포인트](https://platform.claude.com/docs/ko/api/beta/environments/work)를 호출하세요.
  </CodeGroupItem>

  <CodeGroupItem language="Ruby">
    Ruby SDK에는 작업을 폴링하는 헬퍼가 없습니다. 자체 코드에서 작업을 가져오려면 [Environments Work 엔드포인트](https://platform.claude.com/docs/ko/api/beta/environments/work)를 호출하세요.
  </CodeGroupItem>
</CodeGroup>

### 샌드박스 내부에서 SDK 워커 실행

샌드박스가 [사용자 정의 도구를 제공](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-custom-tools)하거나 [기본값이 아닌 메모리 동기화 설정](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-memory#configure-sync)을 사용해야 하는 경우, `ant beta:worker run` 엔트리포인트를 SDK 엔트리포인트로 교체하세요. 이 엔트리포인트는 `EnvironmentWorker`를 생성하고 `handle_item()` (typescript: `handleItem()`; go: `HandleItem()`)을 호출하며, 이 함수는 스폰 스크립트가 전달하는 것과 동일한 `ANTHROPIC_*` 변수를 읽습니다.

<CodeGroup exclude="shell, csharp, java, php, ruby">
  ```python Python
  import asyncio
  import contextlib
  import os
  import signal
  from anthropic import AsyncAnthropic
  from anthropic.lib.environments import EnvironmentWorker


  async def main() -> None:
      async with AsyncAnthropic(
          auth_token=os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
      ) as client:
          worker = EnvironmentWorker(client, workdir="/workspace")
          # 인수 없이 호출하면 handle_item()은 spawn 스크립트가 전달한 ANTHROPIC_* 변수를
          # 읽으며, 여기에는 ANTHROPIC_WORK_SECRET도 포함됩니다.
          task = asyncio.create_task(worker.handle_item())
          # 컨테이너가 중지될 때 태스크를 취소하면 워커가 종료 전에 변경된
          # 메모리 파일을 업로드하고 스토어 디렉터리를 제거할 수 있습니다.
          loop = asyncio.get_running_loop()
          for signum in (signal.SIGINT, signal.SIGTERM):
              loop.add_signal_handler(signum, task.cancel)
          with contextlib.suppress(asyncio.CancelledError):
              await task


  asyncio.run(main())
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";
  import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";

  const client = new Anthropic({ authToken: process.env.ANTHROPIC_ENVIRONMENT_KEY });
  const controller = new AbortController();
  // 컨테이너가 중지될 때 중단하면 워커가 종료 전에 변경된 메모리 파일을
  // 업로드하고 스토어 디렉터리를 제거할 수 있습니다.
  process.once("SIGTERM", () => controller.abort());
  process.once("SIGINT", () => controller.abort());

  // 인수가 없으면 handleItem()은 spawn 스크립트가 전달한 ANTHROPIC_* 변수를
  // 읽으며, 여기에는 ANTHROPIC_WORK_SECRET도 포함됩니다.
  await new EnvironmentWorker({
    client,
    workdir: "/workspace",
    signal: controller.signal
  }).handleItem();
  ```

  <CodeGroupItem language="C#">
    C# SDK에는 `EnvironmentWorker`가 없습니다.
  </CodeGroupItem>

  ```go Go
  package main

  import (
  	"context"
  	"log"
  	"os"
  	"os/signal"
  	"syscall"

  	"github.com/anthropics/anthropic-sdk-go"
  	"github.com/anthropics/anthropic-sdk-go/lib/environments"
  	"github.com/anthropics/anthropic-sdk-go/option"
  )

  func main() {
  	// 컨테이너가 중지될 때 컨텍스트를 취소하면 워커가 종료 전에
  	// 변경된 메모리 파일을 업로드하고 스토어 디렉터리를 제거할 수 있습니다.
  	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  	defer stop()

  	client := anthropic.NewClient(option.WithAuthToken(os.Getenv("ANTHROPIC_ENVIRONMENT_KEY")))
  	worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
  		Workdir: "/workspace",
  	})
  	// 옵션이 제로 값이면 HandleItem은 spawn 스크립트가 전달한 ANTHROPIC_* 변수를
  	// 읽으며, 여기에는 ANTHROPIC_WORK_SECRET도 포함됩니다.
  	if err := worker.HandleItem(ctx, environments.HandleItemOptions{}); err != nil {
  		log.Fatalf("worker: %v", err)
  	}
  }
  ```

  <CodeGroupItem language="Java">
    Java SDK에는 `EnvironmentWorker`가 없습니다.
  </CodeGroupItem>

  <CodeGroupItem language="PHP">
    PHP SDK에는 `EnvironmentWorker`가 없습니다.
  </CodeGroupItem>

  <CodeGroupItem language="Ruby">
    Ruby SDK에는 `EnvironmentWorker`가 없습니다.
  </CodeGroupItem>
</CodeGroup>

## 세션용 파일 준비

Anthropic은 자체 호스팅 샌드박스에 파일이나 GitHub 저장소를 마운트하지 않습니다. 세션별 파일을 사용할 수 있게 하려면:

1. S3 경로나 커밋 SHA 같은 파일 참조를 세션의 `metadata` 필드에 전달하세요.
2. 스폰 스크립트 또는 `--on-work` 핸들러에서 세션을 조회(`GET /v1/sessions/{session_id}`)하고 `metadata`를 읽으세요. 가져온 작업 항목에는 세션 ID는 포함되지만 메타데이터는 포함되지 않습니다.
3. 도구 실행이 시작되기 전에 파일을 작업 디렉터리에 준비하세요.

<CodeGroup>
  ```bash cURL
  curl -sS --fail-with-body https://api.anthropic.com/v1/sessions \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "anthropic-beta: managed-agents-2026-04-01" \
    -H "content-type: application/json" \
    -d '{
      "agent": "'"$AGENT_ID"'",
      "environment_id": "'"$ANTHROPIC_ENVIRONMENT_ID"'",
      "metadata": {"input_file": "s3://my-bucket/data.csv"}
    }'
  ```

  ```bash CLI
  ant beta:sessions create \
    --agent "$AGENT_ID" \
    --environment-id "$ANTHROPIC_ENVIRONMENT_ID" \
    --metadata '{"input_file": "s3://my-bucket/data.csv"}'
  ```

  ```python Python
  session = client.beta.sessions.create(
      agent=agent.id,
      environment_id=environment.id,
      metadata={"input_file": "s3://my-bucket/data.csv"},
  )
  ```

  ```typescript TypeScript
  const session = await client.beta.sessions.create({
    agent: agent.id,
    environment_id: environment.id,
    metadata: { input_file: "s3://my-bucket/data.csv" }
  });
  ```

  ```csharp C#
  var session = await client.Beta.Sessions.Create(new()
  {
      Agent = agent.ID,
      EnvironmentID = environment.ID,
      Metadata = new Dictionary<string, string> { ["input_file"] = "s3://my-bucket/data.csv" },
  });
  ```

  ```go Go
  session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{
  	Agent:         anthropic.BetaSessionNewParamsAgentUnion{OfString: anthropic.String(agent.ID)},
  	EnvironmentID: environment.ID,
  	Metadata: map[string]string{
  		"input_file": "s3://my-bucket/data.csv",
  	},
  })
  if err != nil {
  	panic(err)
  }
  ```

  ```java Java
  var session = client.beta().sessions().create(SessionCreateParams.builder()
      .agent(agent.id())
      .environmentId(environment.id())
      .metadata(SessionCreateParams.Metadata.builder()
          .putAdditionalProperty("input_file", JsonValue.from("s3://my-bucket/data.csv"))
          .build())
      .build());
  ```

  ```php PHP
  $session = $client->beta->sessions->create(
      agent: $agent->id,
      environmentID: $environment->id,
      metadata: ['input_file' => 's3://my-bucket/data.csv'],
  );
  ```

  ```ruby Ruby
  session = client.beta.sessions.create(
    agent: agent.id,
    environment_id: environment.id,
    metadata: {input_file: "s3://my-bucket/data.csv"}
  )
  ```
</CodeGroup>

## 다음 단계

<CardGroup cols={2}>
  <Card title="모니터링 및 문제 해결" icon="lightning" href="https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-operations">
    큐 깊이를 확인하고, 세션과 워커를 깔끔하게 중지하고, 일반적인 오류를 해결하세요.
  </Card>

  <Card title="보안 모델" icon="lock" href="https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-security">
    자체 호스팅 샌드박스 환경을 위한 공동 책임 모델입니다.
  </Card>
</CardGroup>
