Skip to content
Sandbox SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Files and git

Read, write, and edit files in a sandbox, move folders in and out, search, watch for changes, see what changed since creation, and use git with tokens kept out of logs.

box.files works on the sandbox file system. box.git runs git with credentials that never touch the command line. Relative paths resolve against /workspace, the same default directory run uses.

Read and write

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

await box.files.write("app/index.ts", 'console.log("hi");\n');
await box.files.write({
  "app/package.json": '{ "type": "module" }\n',
  "app/src/util.ts": "export const one = 1;\n",
});

const text = await box.files.read("app/index.ts");
const bytes = await box.files.read("app/index.ts", { as: "bytes" });
console.log(text, bytes.length);

write creates missing directories. It takes a string or a Uint8Array, or an object of many paths at once. read returns a string unless you ask for bytes.

Edit in place

edit replaces one exact piece of text. It is the safest way to let a model change a file.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

await box.files.write("app/index.ts", "start();\n");
await box.files.edit("app/index.ts", { find: "start()", replace: "start({ port: 3000 })" });

find must match exactly once. If it matches nothing, edit throws SandboxError with code edit_not_found. If it matches more than once, the code is edit_not_unique and the message lists the line numbers of every match, so the caller can add context and retry.

List, inspect, remove

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

const entries = await box.files.list("app", { recursive: true, glob: "**/*.ts" });
for (const entry of entries) console.log(entry.path, entry.type, entry.size);

if (await box.files.exists("app/dist")) {
  await box.files.remove("app/dist", { recursive: true });
}

const info = await box.files.stat("app/index.ts");
console.log(info.modifiedAt);

Move folders in and out

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

await box.files.upload("./starter", "app", { ignore: [".git", "node_modules"] });
await box.run("npm install && npm run build", { cwd: "app" });
await box.files.download("app/dist", "./out");

upload copies a local folder into the sandbox as one tar archive, which is much faster than writing files one by one. download does the reverse.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

const matches = await box.files.search("TODO", { path: "app", glob: "*.ts", maxMatches: 200 });
for (const m of matches) console.log(`${m.path}:${m.line} ${m.text}`);

Watch for changes

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

const stop = box.files.watch(
  "app",
  (event) => {
    console.log(event.type, event.path);
  },
  { recursive: true },
);

await box.run("touch app/new.ts");
stop();

Each event has type ("added", "modified", or "removed") and path. The function watch returns stops watching.

See what changed

changes tells you what files differ from a checkpoint. Use it to show a user what their agent did.

To compare against the sandbox as it was created, pass trackChanges: true. The SDK then records a checkpoint named "start", which changes() uses when you leave out since:

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local(), { trackChanges: true });

await box.files.write("app/index.ts", "export {};\n");

const diff = await box.files.changes({ since: "start" });
console.log(diff.added, diff.modified, diff.removed);
console.log(diff.patch);

trackChanges is off by default, because recording the checkpoint runs a command in the sandbox before createSandbox returns. Without it, changes() with no since throws an invalid_argument error that tells you to pass trackChanges: true or call mark first.

To compare against a later point, set a checkpoint with mark and pass its name. This works with or without trackChanges:

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

await box.files.mark("before-agent");
await box.run("echo changed > app/notes.txt");
const diff = await box.files.changes({ since: "before-agent" });
console.log(diff.patch);

This works on every provider. The SDK uses git with a hidden index, so it doesn’t touch your project’s own git history. Files inside nested git repositories are compared one by one, like any other file, and .git directories are skipped.

Git

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

const token = process.env.GITHUB_TOKEN ?? "";

await using box = await createSandbox(local());

await box.git.clone("https://github.com/acme/web.git", {
  dir: "app",
  branch: "main",
  depth: 1,
  token,
});
await box.git.checkout("app", { branch: "agent/fix-login", create: true });

await box.files.edit("app/src/login.ts", { find: "timeout: 10", replace: "timeout: 30" });

const status = await box.git.status("app");
console.log(status.stdout);

await box.git.commit("app", { message: "Raise login timeout", all: true });
await box.git.push("app", { token, branch: "agent/fix-login" });

Each git method runs through run and resolves with a normal command result. A failed push is ok: false, not a throw.

token goes through a temporary credential helper. It never appears in the command line, the shell history, or any output the SDK returns.

Was this page helpful?