Skip to main content
Documentation

Anthropic sandbox

Let Claude read, edit, and create documents in Anthropic's hosted sandbox.

On this page

Claude can run Python and shell commands in a sandbox hosted by Anthropic. Use it to edit Word documents, analyze spreadsheets, create PDFs, or generate charts. DeepSpace handles the provider connection and keeps sandbox files and containers scoped to your app.

The sandbox has no network access. Upload input files with sandboxFiles, attach them with sandboxUpload, and download the files Claude creates when the run finishes.

Before you start#

Use an SDK release with sandbox support and AI SDK 7. Redeploy the app after upgrading so the Worker has its app identity token. The helpers read DEEPSPACE_APP_ID and APP_IDENTITY_TOKEN from env; you don't need an Anthropic API key.

Run these helpers in your Worker. For work that should continue after the user closes the page, use a background job.

Upload and edit a document#

This function uploads a file, asks Claude to work on it, and returns the generated file IDs:

import { isStepCount, streamText } from 'ai'
import {
  codeExecutionTool,
  createDeepSpaceAI,
  forwardSandboxContainer,
  sandboxFiles,
  sandboxOutputs,
  sandboxUpload,
  type DeepSpaceAIEnv,
} from 'deepspace/worker'

async function editDocument(
  env: DeepSpaceAIEnv,
  file: File,
  task: string,
  authToken: string,
  signal: AbortSignal,
) {
  const files = sandboxFiles(env, { authToken })
  const uploaded = await files.upload(file)
  const ai = createDeepSpaceAI(env, 'anthropic', { authToken })

  const result = streamText({
    model: ai('claude-sonnet-5'),
    tools: { code_execution: codeExecutionTool() },
    prepareStep: forwardSandboxContainer,
    stopWhen: isStepCount(20),
    abortSignal: signal,
    messages: [{
      role: 'user',
      content: [
        { type: 'text', text: `${task}\nCopy the final files into $OUTPUT_DIR and list it in the same bash command.` },
        sandboxUpload(uploaded),
      ],
    }],
  })

  for await (const part of result.stream) {
    if (part.type === 'error') throw part.error
  }
  signal.throwIfAborted()
  const outputs = sandboxOutputs(await result.steps)
  if (outputs.fileIds.length === 0) throw new Error('No output files were created')
  return outputs
}
ts

Pass a verified caller's JWT as authToken for user-billed work. For owner-billed work, omit authToken from both sandboxFiles and createDeepSpaceAI; they use env.APP_OWNER_JWT. See file scope before moving a user's upload into an owner-billed job.

Use streamText for sandbox calls, even when you don't display the stream. A non-streamed call that takes longer than about two minutes can fail with a Cloudflare 524. Reading result.stream lets the handler throw on stream errors before returning files.

Anthropic exports files from the top level of $OUTPUT_DIR after each bash command. Files saved elsewhere remain in the container and have no downloadable file IDs.

Download the output#

Use the returned IDs with the same file client and caller identity:

const files = sandboxFiles(env, { authToken })
const outputs = await editDocument(env, file, 'Add an executive summary and export a PDF.', authToken, signal)

for (const fileId of outputs.fileIds) {
  const metadata = await files.get(fileId)
  const response = await files.download(fileId)
  // Save response.body in your app's file storage using metadata.filename.
}
ts

download returns a streaming Response with the file's content type and filename headers. Only sandbox-created files can be downloaded; uploaded input files cannot.

For a background job, save the downloaded files in your app's file storage and return their app file IDs or URLs in job.result. The result must be JSON-serializable. A sandbox file ID is used by the Worker to retrieve a file; it is not a browser download URL.

Keep the same sandbox#

prepareStep: forwardSandboxContainer reuses the container across steps in one turn. Save the containerId returned by sandboxOutputs if a later turn should continue with the same files and working directory:

import { reuseSandbox } from 'deepspace/worker'

const result = streamText({
  model: ai('claude-sonnet-5'),
  tools: { code_execution: codeExecutionTool() },
  prepareStep: forwardSandboxContainer,
  providerOptions: reuseSandbox(savedContainerId),
  stopWhen: isStepCount(20),
  messages,
})
ts

Containers expire. Keep important outputs in your app's storage rather than relying on a saved container to remain available.

Let sandbox code call an app tool#

Wrap an existing AI tool with callableFromSandbox to let sandbox code call it. The tool still executes in your app's Worker, so keep its normal permission checks and pass the caller's identity to your data-access code.

import { tool } from 'ai'
import { z } from 'zod'
import { callableFromSandbox } from 'deepspace/worker'

const lookupDeals = callableFromSandbox(tool({
  description: 'Look up deals visible to the current user',
  inputSchema: z.object({ quarter: z.string() }),
  execute: async ({ quarter }, { abortSignal }) => {
    return queryDealsForUser(userId, quarter, abortSignal)
  },
}))

const tools = {
  code_execution: codeExecutionTool(),
  lookup_deals: lookupDeals,
}
ts

The sandbox receives the tool result, not your credentials. Return promptly: Anthropic waits about four minutes for each app tool result.

File scope#

Files and containers default to user scope: only the JWT subject that created them can use them, inside the same app. For an app-shared workflow, select app scope on both clients:

const files = sandboxFiles(env, { authToken, scope: 'app' })
const ai = createDeepSpaceAI(env, 'anthropic', { authToken, sandboxScope: 'app' })
ts

App scope lets other callers in the same app use those resources. This includes an owner-billed job continuing work started by a user. Check who may start the job and receive its outputs in your app. Neither scope allows another app to use the resources.

Scope controls access; authToken controls who pays for the model call. Changing to app scope does not transfer billing to the owner.

File limits and cleanup#

  • Uploads are limited to 32 MiB per file.
  • Uploaded files expire after 30 days by default. Set expiresInSeconds when uploading to choose between one hour and 90 days.
  • Sandbox-created files do not automatically expire. Download the outputs you need, then delete files you no longer use with files.delete(fileId).
  • Custom Anthropic Skills are not supported through the DeepSpace proxy.

For large documents, set a deadline on the job handler and choose an appropriate maxOutputTokens. The job's runtime limit and the model's output limit are separate settings.

Next steps#