Sandboxes

Observability

The events stream is what a client reads. This page is the server-side half: hooks that fire on every file the agent touches, sandbox debug logging, and the low-level watcher you can drive outside a chat() run.

File-event hooks

Listen to files being created, changed, or deleted inside a sandbox — for example to watch what the agent edits as it works. The watcher is provider-agnostic: it uses native OS watching where the provider supports it (local-process) and falls back to a portable find poll everywhere else (Docker and other exec-only providers), with no extra dependencies or image changes.

There are two places to declare these hooks, with different scopes.

Sandbox-scoped hooks

Declared directly on defineSandbox({ hooks }). They fire once per file event, regardless of how many runs share the sandbox, alongside the sandbox's own lifecycle callbacks:

ts
import { defineSandbox } from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'

const repoSandbox = defineSandbox({
  id: 'repo-agent',
  provider: dockerSandbox({ image: 'node:22' }),
  hooks: {
    // catch-all: fires for every event
    onFile: (e) => console.log(`[${e.type}] ${e.path}`),
    // type-specific variants
    onFileCreate: (e) => console.log('created', e.path),
    onFileChange: (e) => console.log('changed', e.path),
    onFileDelete: (e) => console.log('deleted', e.path),
    // lifecycle
    onReady: (handle) => console.log('sandbox ready', handle.id),
    onError: (err) => console.error('sandbox error', err),
    onDestroy: () => console.log('sandbox destroyed'),
  },
})

Run-scoped hooks

To handle file events inside a middleware (for example per-request audit logging), use the sandbox hook group on defineChatMiddleware. These fire per-run, and each handler receives the current run's ChatMiddlewareContext:

ts
import { defineChatMiddleware } from '@tanstack/ai'
import { db } from './db'

const auditMiddleware = defineChatMiddleware({
  name: 'audit',
  // ctx is the ChatMiddlewareContext for the current run
  sandbox: {
    onFile: (ctx, e) => console.log(ctx.runId, e.type, e.path),
    onFileCreate: (ctx, e) => db.log({ run: ctx.runId, event: e }),
  },
})

Both hook groups fire server-side and are independent of the stream: the engine automatically emits one CUSTOM sandbox.file event per change regardless of whether you register any hooks — so the client can react to the same edits without extra middleware.

Disabling file watching

To stop the watcher and suppress sandbox.file events for a sandbox entirely, set fileEvents: false:

ts
import { defineSandbox } from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'

const sandbox = defineSandbox({
  id: 'quiet-agent',
  provider: dockerSandbox({ image: 'node:22' }),
  fileEvents: false, // watcher not started; no sandbox.file events emitted
})

Debugging

To log sandbox internals — watcher start/stop, event dispatch, lifecycle transitions — pass the sandbox debug category to chat():

ts
import { chat } from '@tanstack/ai'
import { grokBuildText } from '@tanstack/ai-grok-build'
import { withSandbox } from '@tanstack/ai-sandbox'
import { repoSandbox } from './sandbox'
import { messages } from './chat-context'

chat({
  threadId: 'thread-1',
  adapter: grokBuildText('grok-build'),
  messages,
  middleware: [withSandbox(repoSandbox)],
  debug: { sandbox: true }, // or `debug: true` for all categories
})

Low-level: watchWorkspace()

watchWorkspace() is the building block the hooks are built on. Reach for it when you want the watcher outside a chat() run:

ts
import { watchWorkspace } from '@tanstack/ai-sandbox'
import { repoSandbox } from './sandbox'

const handle = await repoSandbox.ensure({ threadId: 'thread-1', runId: 'run-1' })
const watcher = await watchWorkspace(handle, {
  onEvent: (event) => {
    // event.type is 'create' | 'change' | 'delete'
    console.log(`${event.type} ${event.path}`)
  },
  ignore: ['.git', 'node_modules'], // default
})
// …do work outside a chat run…
await watcher.stop()
  • Events — the CUSTOM event stream the client reads.
  • Lifecycle & Snapshots — when sandboxes are created and torn down.
  • Tools — bridged host tools that surface as tool-call chunks.