@cloudflare/shell
v0.3.1
Published
Experimental sandboxed JS execution and filesystem runtime for Cloudflare Workers agents
Maintainers
Readme
@cloudflare/shell
Experimental. API surface is still settling — expect breaking changes.
Sandboxed JavaScript execution and filesystem runtime for Cloudflare Workers agents.
Instead of parsing shell syntax, @cloudflare/shell runs JavaScript inside an isolated Worker and exposes a typed state object for operating on a filesystem backend. It is designed for agent workflows that need structured state operations, predictable semantics, and coarse host-side filesystem primitives.
What it is
- A runtime-neutral
StateBackendinterface for filesystem/state operations - A
FileSysteminterface with two implementations:InMemoryFs(ephemeral) andWorkspaceFileSystem(durable) FileSystemStateBackend— a single adapter wrapping anyFileSysteminto aStateBackendWorkspace— durable file storage backed by SQLite + optional R2stateTools(workspace)— aToolProviderfor@cloudflare/codemodethat exposesstate.*in sandboxed executionscreateGit(filesystem)— pure-JS git operations via isomorphic-git, backed by the virtual filesystemgitTools(workspace)— aToolProviderfor@cloudflare/codemodethat exposesgit.*in sandboxed executions with auto-injected auth- A prebuilt
statestdlib with type declarations for LLM prompts
What it is not
This is not a bash interpreter. It does not parse shell syntax, expose pipes, or emulate POSIX shell behavior. It executes JavaScript.
Example — in-memory state
import { createMemoryStateBackend } from "@cloudflare/shell";
import { stateToolsFromBackend } from "@cloudflare/shell/workers";
import { DynamicWorkerExecutor, resolveProvider } from "@cloudflare/codemode";
const backend = createMemoryStateBackend({
files: {
"/src/app.ts": 'export const answer = "foo";\n'
}
});
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });
const result = await executor.execute(
`async () => {
const text = await state.readFile("/src/app.ts");
await state.writeFile("/src/app.ts", text.replace("foo", "bar"));
return await state.readFile("/src/app.ts");
}`,
[resolveProvider(stateToolsFromBackend(backend))]
);Example — durable Workspace
import { Agent } from "agents";
import { Workspace } from "@cloudflare/shell";
import { stateTools } from "@cloudflare/shell/workers";
import { DynamicWorkerExecutor, resolveProvider } from "@cloudflare/codemode";
class MyAgent extends Agent<Env> {
workspace = new Workspace({
sql: this.ctx.storage.sql,
r2: this.env.MY_BUCKET,
name: () => this.name
});
async run(code: string) {
const executor = new DynamicWorkerExecutor({ loader: this.env.LOADER });
return executor.execute(code, [
resolveProvider(stateTools(this.workspace))
]);
}
}Example — git operations
import { Agent } from "agents";
import { Workspace } from "@cloudflare/shell";
import { WorkspaceFileSystem } from "@cloudflare/shell";
import { createGit } from "@cloudflare/shell/git";
class MyAgent extends Agent<Env> {
workspace = new Workspace({
sql: this.ctx.storage.sql,
name: () => this.name
});
async run() {
const git = createGit(new WorkspaceFileSystem(this.workspace));
await git.clone({ url: "https://github.com/org/repo", depth: 1 });
await this.workspace.writeFile("/README.md", "# Updated");
await git.add({ filepath: "." });
await git.commit({
message: "update readme",
author: { name: "Agent", email: "[email protected]" }
});
await git.push({ token: this.env.GITHUB_TOKEN });
}
}Git commands
init, clone, status, add, rm, commit, log, branch, checkout, fetch, pull, push, diff, remote
Git ToolProvider for codemode
gitTools(workspace) exposes all git commands inside sandboxed executions. Auth tokens are auto-injected into clone/fetch/pull/push — the LLM never sees secrets.
import { stateTools } from "@cloudflare/shell/workers";
import { gitTools } from "@cloudflare/shell/git";
const providers = [
resolveProvider(stateTools(this.workspace)),
resolveProvider(gitTools(this.workspace, { token: this.env.GITHUB_TOKEN }))
];Design goals
- Structured state operations instead of shell parsing
- Coarse host-side operations like
glob()anddiff()to avoid chatty RPC - Compatibility with both ephemeral in-memory state and durable
Workspace - Secure execution with isolate-level timeouts and outbound network blocking by default
state object API
The state object is available inside every isolate and exposes:
Primitive filesystem
readFile, writeFile, appendFile, readFileBytes, writeFileBytes, mkdir, rm, cp, mv, symlink, readlink, realpath, readdir, glob, stat, lstat, exists, diff, diffContent
JSON helpers
readJson(path), writeJson(path, value, { spaces? }), queryJson(path, query), updateJson(path, operations)
Search and replace
searchText(path, query, options?), searchFiles(glob, query, options?), replaceInFile(path, search, replacement, options?), replaceInFiles(glob, search, replacement, { dryRun?, rollbackOnError?, ...options })
Filesystem queries
find(path, options?), walkTree(path, { maxDepth? }), summarizeTree(path, { maxDepth? })
Archive and compression
createArchive(path, sources), listArchive(path), extractArchive(path, destination), compressFile(path, destination?), decompressFile(path, destination?), hashFile(path, { algorithm? }), detectFile(path)
Structured edit planning
planEdits(instructions), applyEditPlan(plan, { dryRun?, rollbackOnError? }), applyEdits(edits, { dryRun?, rollbackOnError? })
Multi-file workflow
// Preview changes without applying
const preview = await state.replaceInFiles("src/**/*.ts", "foo", "bar", {
dryRun: true
});
// Plan structured edits with intent
const plan = await state.planEdits([
{ kind: "replace", path: "/src/app.ts", search: "foo", replacement: "bar" },
{ kind: "writeJson", path: "/src/config.json", value: { enabled: true } }
]);
await state.applyEditPlan(plan);
// Apply raw edits transactionally
await state.applyEdits([
{ path: "/src/generated.ts", content: "export const generated = true;\n" }
]);Batch writes roll back by default if any write fails. Set rollbackOnError: false to allow partial progress.
Rough command translation
| Shell command | state equivalent |
| -------------------- | -------------------------------------------------------------------------- |
| cat | state.readFile() |
| echo x > file | state.writeFile() |
| mkdir | state.mkdir() |
| ls / find | state.readdir() / state.glob() |
| find with filters | state.find() |
| tree / du | state.walkTree() / state.summarizeTree() |
| cp / mv / rm | state.cp() / state.mv() / state.rm() |
| diff | state.diff() / state.diffContent() |
| grep | state.searchText() / state.searchFiles() |
| sed | state.replaceInFile() / state.replaceInFiles() |
| jq | state.readJson() / state.queryJson() / state.updateJson() |
| tar | state.createArchive() / state.listArchive() / state.extractArchive() |
| gzip / gunzip | state.compressFile() / state.decompressFile() |
| sha256sum / file | state.hashFile() / state.detectFile() |
| git | git.* via @cloudflare/shell/git |
Maybe later
- Stream-oriented transforms inspired by
sort,uniq,comm,cut,paste,tr - Full
rgCLI parity — file types, ignore controls, multiple roots - Richer JSON query semantics closer to
jqfilters - Structured patch helpers covering more of diff / codemod workflows
- A smaller "command" library built on top of
state.*
Relationship to other packages
@cloudflare/codemode: executes sandboxed JavaScript that orchestrates tools@cloudflare/shell: provides filesystem backends andstateTools()ToolProvider for codemode
