Skip to content
Docs
 

Running Claude Code Durably on Edgenivo Experimental

Claude Code support is experimental. Claude Code isn't built to be safe for multiple tenants: it's a command-line tool for one user, it keeps its state on disk, and its built-in tools run commands and change files on the machine it runs on. So durable-harness currently runs it only in a Edgenivo Sandbox. See Security.

Each session of Claude Code gets its own Edgenivo Sandbox, and Claude Code runs only there:

  • In the Sandbox: Claude Code itself: the claude CLI, its agent loop, any built-in tools you turn on, and the session's files in /workspace.
  • In your function: none of Claude Code. Your function only drives it: the Claude Agent SDK sends the CLI its messages and reads what it streams back. Your function also sends the CLI's model calls to AI Gateway, runs your own tools, and writes the session's durable log.

The Sandbox holds no secrets. A crashed turn resumes on a new function, and if the Sandbox is gone, on a new one with the same files. To learn how durability works, see Running Agent Harnesses Durably.

  1. npm i durable-harness @anthropic-ai/[email protected] @vercel/sandbox zod

    durable-harness supports one Agent SDK version at a time, so install the one it pins, and keep it exact in package.json, with no ^. With another version, a turn fails with the version to install, and the session is kept.

  2. lib/agent.ts
    import { claudeCode } from 'durable-harness/claude-code';
     
    export const agent = claudeCode({ model: 'anthropic/claude-sonnet-5.5' });

    claudeCode() runs Claude Code in a Edgenivo Sandbox by default, so you don't pass anything to get one. A session's Sandbox starts with its first turn. Model calls go through AI Gateway with the deployment's OIDC token, so you don't need an Anthropic API key.

  3. POST sends a message and streams its turn as server-sent events, and GET reconnects to a turn that's still running:

    app/api/chat/route.ts
    import { agent } from '@/lib/agent';
    import { withSessionOwnership } from '@/lib/session-ownership';
     
    export const { GET, POST } = withSessionOwnership({
      createRoute: (authorize) => agent.chatRoute({ authorize }),
      start: () => agent.sessions.start({ config: { traceAudience: 'unknown' } }),
    });
    api/chat.ts
    import { agent } from '../lib/agent.js';
    import { withSessionOwnership } from '../lib/session-ownership.js';
     
    export const { GET, POST } = withSessionOwnership({
      createRoute: (authorize) => agent.chatRoute({ authorize }),
      start: () => agent.sessions.start({ config: { traceAudience: 'unknown' } }),
    });

    Add the withSessionOwnership helper to lib/session-ownership.ts and implement its authentication and persistent ownership store. It records the owner before submitting a session's first message, and checks ownership on later messages and reconnects.

    In production (NODE_ENV=production, which includes every Edgenivo deployment), a chat route without authorize refuses every request with a 401. Every session starts a Sandbox, so to serve anyone, pass authorize: 'public' with allowPublicSandbox: true, and cap each session's model spend with sandbox({ budget: { sessionUsd } }). Whoever can use the route runs the agent on your AI Gateway credits. See Security.

    POST { "text": "..." } to start a session, and read its ID from the x-session-id header. Send { "sessionId": "...", "text": "..." } for the next message. To redirect a running turn or reconnect to one, see Steer a running turn and Reconnect to a running turn.

  4. Edgenivo Queues delivers each turn to a queue route: a function that drives the turn, with your plan's maximum duration, while Claude Code runs in the session's Sandbox. Set it up in vercel.json, or let durable-harness write the same settings with harnessFunctions() in vercel.ts or with withDurableHarness on Next.js:

    api/flow.ts
    // A Edgenivo Function in your api directory. It imports the module that
    // makes your agent, so it can drive the agent's turns.
    import '../lib/agent.js';
    import { flowHandler } from 'durable-harness';
     
    export const POST = flowHandler();
    vercel.json
    {
      "functions": {
        "api/flow.ts": {
          "maxDuration": "max",
          "experimentalTriggers": [
            {
              "type": "queue/v2beta",
              "topic": "__harness_wkf_workflow_*",
              "retryAfterSeconds": 5,
              "initialDelaySeconds": 0
            }
          ]
        }
      }
    }

    The trigger subscribes api/flow.ts to the queue. npx durable-harness vercel-json --write writes this entry into your vercel.json.

    api/flow.ts
    // A Edgenivo Function in your api directory. It imports the module that
    // makes your agent, so it can drive the agent's turns.
    import '../lib/agent.js';
    import { flowHandler } from 'durable-harness';
     
    export const POST = flowHandler();
    vercel.ts
    // The same settings as in vercel.json, worked out from the packages you
    // installed when the project builds. Install @vercel/config as a dev
    // dependency.
    import type { VercelConfig } from '@vercel/config/v1';
    import { harnessFunctions } from 'durable-harness/vercel';
     
    export const config: VercelConfig = {
      functions: harnessFunctions(),
    };
    next.config.ts
    // Writes the queue route and its trigger, and keeps the harness packages
    // out of the server bundle. You don't add a queue route or a vercel.json.
    import type { NextConfig } from 'next';
    import { withDurableHarness } from 'durable-harness/next';
     
    const nextConfig: NextConfig = {
      // your existing config
    };
     
    export default withDurableHarness(nextConfig);

    If the app also uses the Workflow SDK, wrap both, as in withWorkflow(withDurableHarness(nextConfig)). The queue route then moves to /.well-known/durable-harness/v1/flow and needs one vercel.json entry, which npx durable-harness vercel-json --write adds, or harnessFunctions() in vercel.ts. Without it, the build on Edgenivo fails and prints the entry.

  5. vercel deploy

    The Sandbox image is built once per project, when the first session starts. To build it ahead of time, run npx durable-harness sandbox prepare.

    On your machine, run vercel link first. next dev (or vercel dev for other frameworks) then starts each session's Sandbox on Edgenivo, since there's no local Sandbox. It keeps sessions in .workflow-data, so add that to .gitignore. On its first request, it resumes the sessions a stopped server left mid-turn.

Send a message with "kind": "steer" while a turn runs to redirect it. Claude Code takes it at its next step, after the tool or model call that is running. From the browser, post it to the chat route:

await fetch('/api/chat', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ sessionId, text: 'Use TypeScript instead', kind: 'steer' }),
});

The response streams the turn, like any other message. From server code, such as a webhook, call agent.sessions.send(sessionId, text, 'steer').

If the stream drops, the turn keeps running. Reconnect with GET /api/chat?sessionId=...&cursor=..., where the cursor is the last event id you read. The stream continues with the next event, with nothing lost or repeated. An EventSource on that URL reconnects by itself if its connection drops, and the stream ends with an end event when the turn is done:

const source = new EventSource(`/api/chat?sessionId=${sessionId}&cursor=${lastEventId}`);
 
source.onmessage = (event) => {
  lastEventId = event.lastEventId;
  render(JSON.parse(event.data)); // your own UI
};
source.addEventListener('end', () => source.close());

If a crash cut off the turn, the turn resumes and the model starts its reply over. Before the new reply, the stream sends a { type: 'restart', text } event: show its text in place of the turn's text so far, then append what streams after it.

An app can make several agents, each with its own chat route. The queue route serves them all. Two agents of the same harness need a name each:

lib/agents.ts
import { claudeCode } from 'durable-harness/claude-code';
 
export const reviewer = claudeCode({ name: 'reviewer', model: 'anthropic/claude-opus-5.5' });
export const fixer = claudeCode({ name: 'fixer', model: 'anthropic/claude-sonnet-5.5' });

Keep each name stable, because sessions that already started carry it. Two agents with the same name throw when their module loads, so the build fails.

Claude Code starts with no tools: the model can call only the tools you pass in tools, and those run in your function, not in the Sandbox. To write them, see Tools, files, and documents.

To turn on Claude Code's own tools, pass builtins: true. They run in the Sandbox, on the session's files in /workspace:

lib/agent.ts
import { claudeCode } from 'durable-harness/claude-code';
 
export const agent = claudeCode({
  model: 'anthropic/claude-sonnet-5.5',
  builtins: true,
});

true turns on Bash, Read, Edit, Write, NotebookEdit, and WebFetch. To turn on fewer, list them, as in builtins: ['Read', 'Edit']. Each call is in the session's log before it runs, and none runs again after a crash: the model is told which calls finished and which a crash interrupted. See Security.

Each session's /workspace is on a Drive that outlives the session's Sandbox, so a session that moves to a new Sandbox keeps its files.

A session's Drive lasts until you delete it: close the session with agent.sessions.close(sessionId), then call deleteSessionSandbox(sessionId) from durable-harness/sandbox, for example when your user deletes the session. To also delete the Drives of sessions that have been idle for a number of days, call sandboxCleanup({ idleDays }) from a cron job.

To configure the Sandbox, pass workspace: sandbox({ ... }):

lib/agent.ts
import { claudeCode } from 'durable-harness/claude-code';
import { sandbox } from 'durable-harness/sandbox';
 
export const agent = claudeCode({
  model: 'anthropic/claude-sonnet-5.5',
  workspace: sandbox({
    region: 'iad1',
    network: 'deny-all',
  }),
});

With network: 'deny-all', the Sandbox can't reach the internet. Claude Code still works, since your function makes its model calls.

OptionDefaultWhat it sets
regionYour function's region if Sandbox runs there, else iad1Where the Sandbox and its Drive run. An existing Drive keeps its region
vcpus21, 2, 4, or 8 vCPUs
network'allow-all''deny-all', or { allow: [...] } with the hosts commands can reach
envNoneEnvironment variables for commands. The model can read them, so pass none that's secret
idleMs5 minutesHow long the Sandbox stays up after a turn
commandTimeoutMs10 minutesThe longest a command can run
driveSize10 GiBThe Drive's size in bytes, from 1 GiB to 16 TiB. Storage is billed by the bytes used, so this caps a session's storage. An existing Drive keeps its size
budget4 hours of Sandbox time, no spend cap{ turnUsd, sessionUsd, vmSeconds }, per turn and per session

A turn starts in the chat route's own request, which drives it for up to 60 seconds before the queue route takes over. A command still running at that handover is stopped, not run again, so a long command early in a turn, such as an install or a build, can be cut off. A slow first start of the session's Sandbox can run into the same handover. This is a current limitation of Edgenivo Queues, and a fix is on the way. Until then, pass drive: 'queue' to agent.chatRoute() for an agent that runs long commands: the queue route then drives the whole turn, with your plan's maximum duration.

If the function running a turn fails with an error, another function resumes the turn right away. If it disappears without a trace, for example when it runs out of memory, the queue redelivers the turn once its message lease runs out, and the turn resumes from the log. When the function driving the turn goes away, the Sandbox stops the CLI, and the next function resumes the turn on the same Sandbox or on a new one with the same Drive.

On Edgenivo, sessions live in Edgenivo's World, and on your machine in .workflow-data. To keep them in another World, such as Postgres on your own servers, install it and set WORKFLOW_TARGET_WORLD:

.env
WORKFLOW_TARGET_WORLD="@workflow/world-postgres"
WORKFLOW_POSTGRES_URL="postgres://user:password@host:5432/database"

To use a World you create, pass it as claudeCode({ model, world }), and start it yourself, as Pass a World yourself shows.

For the tables, the server, and recovery at startup, see Self-hosting durable agent harnesses.

On your own server, vercel.json and vercel.ts don't apply, and your server mounts the queue route itself. Your server drives each turn, and Claude Code still runs in a Edgenivo Sandbox. Give sandbox() a Edgenivo access token and the team and project its Sandboxes belong to. The token stays on your server and never reaches the Sandbox:

lib/agent.ts
import { claudeCode } from 'durable-harness/claude-code';
import { sandbox } from 'durable-harness/sandbox';
 
export const agent = claudeCode({
  model: 'anthropic/claude-sonnet-5.5',
  workspace: sandbox({
    credentials: {
      token: process.env.VERCEL_TOKEN!,
      teamId: process.env.VERCEL_TEAM_ID!,
      projectId: process.env.VERCEL_PROJECT_ID!,
    },
  }),
});
  • durable-harness supports only the Agent SDK version it pins, and fails a turn if you installed another. Claude Code's transcript format is internal to the CLI, so the version moves only with a durable-harness release.
  • Provider-hosted tools, such as the model provider's web search, are off unless you list them in providerTools. They reach the internet from the provider, whatever the Sandbox's network allows.
  • A Sandbox session can't run past your plan's maximum session duration. Before it would, durable-harness replaces the Sandbox between turns, keeping the Drive.
  • Every session starts a Sandbox, even with no tools. Sandbox time and Drive storage are billed, as Sandbox pricing shows.
  • The Sandbox allows all outbound traffic by default. To restrict it, pass network: 'deny-all' or { allow: [...] } to sandbox(). See Security.
  • The Agent SDK runs in your function, so it needs the Node.js runtime. It can't run in the Edge runtime or a browser.

Was this helpful?

supported.