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.
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.
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:
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'),
},
})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:
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.
To stop the watcher and suppress sandbox.file events for a sandbox entirely, set fileEvents: false:
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
})To log sandbox internals — watcher start/stop, event dispatch, lifecycle transitions — pass the sandbox debug category to chat():
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
})watchWorkspace() is the building block the hooks are built on. Reach for it when you want the watcher outside a chat() run:
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()