How Paperclip Isolates Agent Execution with Workspaces and Runtime Sandboxes

Paperclip enforces strict workspace and runtime isolation for agents through short-lived Git worktrees, sandboxed runtime slots, and a heartbeat controller that provisions and tears down resources for every run.

Every agent execution in the paperclipai/paperclip repository operates inside an isolated execution workspace—a dedicated Git worktree combined with a confined runtime environment. This architecture prevents code modifications, secrets, and side effects from leaking across runs while allowing controlled reuse of stateful workspaces when needed.

Execution Workspace Architecture

The execution workspace is the foundational isolation primitive. Each workspace receives a unique UUID (executionWorkspaceId) and maps to a Git worktree stored at data/workspaces/<id>. Agents interact with these workspaces exclusively through scoped API endpoints rather than direct filesystem access.

Workspace Realization

In server/src/services/workspace-realization.ts, the system either creates a fresh worktree or reattaches to an existing one:

  • Fresh workspace: Created when currentExecutionWorkspace is absent from the issue's heartbeat context
  • Reusable workspace: Reused when an issue is explicitly linked to a durable workspace for stateful operations
// Conceptual flow from workspace-realization.ts
const workspace = await realizeExecutionWorkspace({
  issueId,
  reusable: issue.linkedWorkspaceId != null,
  baseBranch: 'main'
});
// Returns: { id: UUID, cwd: '/data/workspaces/abc123', gitWorktreePath: '...' }

Worktrees are automatically cleaned up after the heartbeat lease expires unless marked reusable.

Heartbeat Controller and Temporal Isolation

The HeartbeatController in server/src/services/heartbeat.ts enforces temporal isolation—agents only run while an active lease exists.

Six-Stage Execution Flow

  1. Wakeup — Trigger sources: timer, assignment, on_demand, or automation
  2. Workspace resolution — Controller reads heartbeat-context to locate or create currentExecutionWorkspace
  3. Runtime slot acquisition — MCP runtime manager allocates a matching transport slot
  4. Adapter launch — Selected slot executes the configured adapter with workspace-scoped environment
  5. Result collection — Logs, tokens, and status persist to the run row; UI receives SSE/WebSocket updates
  6. Cleanup — Slot termination, worktree disposal (or retention), and lease release

# Trigger on-demand heartbeat manually

curl -sS -X POST \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
  "$PAPERCLIP_API_URL/api/agents/$AGENT_ID/wake" \
  -d '{"wakeReason":"on_demand"}'

The response includes a runId for status polling or cancellation.

Runtime Slot Sandboxing

The MCP runtime slot abstraction provides process-level isolation through two transport modes:

Slot Type Use Case Isolation Mechanism
remote_http Gateway adapters (e.g., hermes_gateway) Proxy to remote service with request-level boundaries
local_stdio Local adapters (e.g., claude_local, codex_local) Containerized process with CPU/memory caps and timeout guards

Slot health is monitored via /api/companies/:companyId/tools/runtime-health with automated alerts and recovery procedures.

Low-Trust Runtime Containment

server/src/services/low-trust-runtime-containment.ts enforces mandatory sandboxing for untrusted adapters. The default adapter trust level is untrusted, requiring an isolated workspace and runtime slot. Only explicitly privileged adapters bypass these constraints.

// From low-trust-runtime-containment.ts
if (adapter.requiresSandbox && !executionWorkspace.isSandboxed) {
  throw new LowTrustViolationError(
    `Adapter ${adapter.id} requires sandboxed execution workspace`
  );
}

Workspace Runtime Services

Execution workspaces can expose runtime services such as preview web servers. These are managed through server/src/services/workspace-runtime.ts and exposed via the workspace-scoped API:


# Discover current workspace and services

curl -sS -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
  "$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID/heartbeat-context"

Response includes currentExecutionWorkspace.id, cwd, and runtimeServices[] array.

Service Control Operations


# Start all configured services

curl -sS -X POST \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
  -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
  "$PAPERCLIP_API_URL/api/execution-workspaces/<workspace-id>/runtime-services/start" \
  -d '{}'

# Restart specific service (e.g., web preview)

curl -sS -X POST \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
  -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
  "$PAPERCLIP_API_URL/api/execution-workspaces/<workspace-id>/runtime-services/restart" \
  -d '{"workspaceCommandId":"web"}'

# Stop all services before teardown

curl -sS -X POST \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
  -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
  "$PAPERCLIP_API_URL/api/execution-workspaces/<workspace-id>/runtime-services/stop" \
  -d '{}'

Service URLs and state persist in the database with company-scoped access control enforced on every request.

Isolation Guarantees

Guarantee Implementation Verification
Temporal Heartbeat lease with automatic teardown server/src/services/heartbeat.ts
Filesystem Git worktree per run, no host path exposure server/src/services/workspace-realization.ts
Runtime Slot-based sandboxing with trust levels server/src/services/low-trust-runtime-containment.ts
Network Workspace-scoped API routes, no direct service access server/src/routes/execution-workspaces.ts

Key Source Files

Summary

  • Execution workspaces are isolated Git worktrees provisioned per run via workspace-realization.ts
  • Heartbeat controller enforces temporal boundaries with automatic resource cleanup
  • MCP runtime slots provide sandboxed process or HTTP transport isolation
  • Low-trust containment requires sandboxing for all adapters by default
  • Workspace runtime services expose controlled network endpoints without breaking isolation boundaries
  • All operations route through workspace-scoped APIs in execution-workspaces.ts rather than direct filesystem access

Frequently Asked Questions

What happens if an agent exceeds its heartbeat timeout?

The HeartbeatController terminates the runtime slot, releases the lease, and records the timeout status in the run row. Any incomplete file operations within the worktree are abandoned; the worktree is deleted unless explicitly marked reusable. According to heartbeat.ts, cleanup runs regardless of success, failure, or cancellation.

Can multiple agents share the same execution workspace?

Only sequentially. A workspace can be reused across heartbeats for the same issue if configured as durable, but concurrent access is prevented by lease-based locking. The currentExecutionWorkspace field in the heartbeat context ensures single-tenancy per run.

How do I verify that an adapter is running in a sandbox?

Query the runtime health endpoint and inspect the adapter's trust classification. Adapters with requiresSandbox: true that bypass containment trigger an error from low-trust-runtime-containment.ts. Privileged adapters like hermes_gateway explicitly opt out of sandbox requirements through allowlist configuration.

What is the difference between remote_http and local_stdio runtime slots?

remote_http slots proxy adapter invocations to external services without local process creation, suitable for gateway adapters that manage their own isolation. local_stdio slots spawn sandboxed subprocesses with resource caps, used for local LLM adapters like claude_local. The MCP runtime manager selects slots based on adapter transport requirements and availability.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →