Skip to content

Workspace sync

An agent can back up its workspace — the files it works with and its saved sessions — to Alfe and restore them later, for example onto a fresh machine. These endpoints are what the agent’s built-in sync uses; you can call them directly with the @alfe.ai/agent-api-client.

File bytes move to and from storage over short-lived presigned URLs (valid for 300 seconds). The agent API hands you the URL; you PUT or GET the bytes directly.

Register the agent for workspace sync (safe to call repeatedly).

sync.ts
const { agent } = await client.syncRegister({ displayName: "My Agent" });
// POST /agent/sync/register
{
"data": {
"agent": {
"agentId": "agt_…",
"displayName": "My Agent",
"status": "synced",
"fileCount": 42,
"totalSize": 918240,
"lastSync": "2026-07-01T12:00:00.000Z"
}
}
}

The manifest lists every backed-up file with its content hash and size — compare it against local files to decide what changed.

const manifest = await client.syncGetManifest();
// GET /agent/sync/manifest
// { version, agentId, lastSync, files: { "<path>": { hash, size, modified } } }

Uploading is two steps. First ask for a presigned put URL for each file. Pass the file’s size in bytes and its SHA-256 as 64 lowercase hex characters: the service signs both, so storage rejects any body of another length or digest. The response’s headers must be sent with the PUT exactly as given; a PUT without them is rejected.

import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
const bytes = await readFile("notes/todo.md");
const sha256 = createHash("sha256").update(bytes).digest("hex");
const { urls } = await client.syncPresign({
files: [
{
path: "notes/todo.md",
operation: "put",
contentType: "text/markdown",
size: bytes.byteLength,
sha256,
},
],
});
// POST /agent/sync/presign

PUT the bytes to the returned url within 5 minutes (the URL expires after 300 seconds). The upload lands in a staging area, not in your workspace. Then confirm it: the service checks the staged object’s size and checksum against what you declare, and only then publishes it to the workspace and the manifest.

await fetch(urls[0].url, {
method: "PUT",
body: bytes,
headers: {
...urls[0].headers, // the signed checksum header: required
"Content-Type": "text/markdown", // sets the stored content type
},
});
await client.syncConfirmUpload({
filePath: "notes/todo.md",
hash: `sha256:${sha256}`, // note the `sha256:` prefix
size: bytes.byteLength,
sha256, // the same bare hex you presigned with
});
// POST /agent/sync/confirm

hash takes the sha256:<hex> form. Sending sha256 as well makes the confirm fail closed: if storage recorded no matching checksum, the upload is discarded with 409. Uploads that declared no checksum are hashed by the service at confirm instead. A confirm whose size or checksum doesn’t match also returns 409, and the staged bytes are discarded; presign again and retry. Files are capped at 25 MiB.

Secret-bearing files are refused with 409 and code: "PATH_FORBIDDEN", for uploads and for reads (file content, presigned get URLs, restore bundles). The match ignores letter case. The list covers .env files (except .env.example, .env.sample and .env.template), private keys (*.pem, *.key, *.p8, *.p12, *.pfx, id_rsa and similar, but not *.pub), .ssh/, .aws/, .git/config, .git-credentials, .npmrc, .netrc, cloud credential files, Terraform state, MCP server configs and openclaw.json. In the agent’s runtime home it also covers credential, identity and device directories and the per-agent auth store. The *.key rule also blocks Keynote decks. Refused files still show up in listings and can be deleted.

Ask for a bundle of presigned get URLs to reconstruct the workspace. Choose how much to restore with mode:

const bundle = await client.syncReconstruct({ mode: "full" });
// POST /agent/sync/reconstruct
// mode: "full" | "active" | "memory"

Each entry in bundle.files has a path and a presigned url; GET each one to pull the bytes down. The URLs expire after 300 seconds. Secret-bearing paths are left out of the bundle.

const { files } = await client.syncListFiles({ prefix: "notes/" });
// GET /agent/sync/files?prefix=notes/
const stats = await client.syncGetStats();
// GET /agent/sync/stats → { fileCount, lastSyncAt, ... }

Saved sessions are backed up too and can be listed and read back individually.

const { sessions } = await client.syncListSessions();
// GET /agent/sync/sessions
const { content } = await client.syncGetSession(sessions[0].sessionId);
// GET /agent/sync/sessions/{sessionId}
const { removed } = await client.syncDeleteFile("notes/todo.md");
// DELETE /agent/sync/files/{filePath}

The endpoints above cover the agent’s own workspace. Files shared with the agent at the org, team, or project level are a separate, read-oriented surface — see Shared knowledge.