Better FS Tools

Get started

Choose an integration and a filesystem, choose your tools, and configure them for your use case.

Set up the tools in three steps. All packages release together at one version, so install matching versions.

1. Choose your integration

An integration package has helpers that return the tools in the native format of its host. You give the result to your agent with no more glue.

HostPackageHelperGuide
AI SDK 7 generateText or streamText@better-fs-tools/ai-sdkcreateAiSdkFsTools()AI SDK
Pi coding agent: the pi command or SDK@better-fs-tools/picreatePiFsTools()Pi
pi-agent-core or pi-ai@better-fs-tools/pi/agent-corecreateAgentCoreFsTools()Pi agent core
pi-durable, experimental@better-fs-tools/pi-durablecreateDurableFsTools()Pi durable
A model framework of your own@better-fs-tools/writefsToolDefinitions()Tool definitions
Your own code: Node, Bun, or a Worker@better-fs-tools/writecreateFsTools()Choose an integration

Every integration takes the same options and has the same defaults. Choose an integration lists them.

With a framework of your own, fsToolDefinitions() from @better-fs-tools/write/signature gives each tool's name, description, and JSON Schema, and an execute that takes the model input. With your own code, the core tools from createFsTools() are plain async functions that you call. On Node or Bun, give them the Node filesystem from the next step.

Install the integration with the tool packages and the host's peers:

# AI SDK 7. The peer is ai ^7.0.77.
npm install @better-fs-tools/ai-sdk @better-fs-tools/read @better-fs-tools/write ai

# Pi. The peers are @earendil-works/pi-coding-agent ^1.0.0 and typebox ^1.3.16.
npm install @better-fs-tools/pi @better-fs-tools/node @better-fs-tools/read @better-fs-tools/write @earendil-works/pi-coding-agent typebox

# Your own code with the Node filesystem, on Node 24 or later or on Bun.
npm install @better-fs-tools/node @better-fs-tools/read @better-fs-tools/write

With Bun, use bun add with the same package names. For a bash tool, add @better-fs-tools/shell: the env option comes from its shellEnv().

Choose a filesystem

All tools need a filesystem. The tools do not open files themselves. The filesystem resolves each path and checks it against the folders that you allow.

FilesystemPackageUse it for
nodeFileSystem()@better-fs-tools/nodeLocal files on Node 24 or later, or Bun
memoryFileSystem()@better-fs-tools/fsTests, examples, and files that you keep in memory
justBashFileSystem()@better-fs-tools/just-bashA just-bash sandbox, with no real files
cloudflareShellFileSystem()@better-fs-tools/cloudflare-shellA Cloudflare Shell workspace in a Worker
cloudflareComputerFileSystem()@better-fs-tools/cloudflare-computerA Cloudflare Computer workspace. Experimental

Give the filesystem to the helper as fs. Every helper needs it. fs can also be a function that returns a filesystem for each call. Two hosts have one ready:

  • Pi: piCwdFileSystem() from @better-fs-tools/pi/node gives a nodeFileSystem() rooted at ctx.cwd on each call.
  • pi-durable: durableEnvFileSystem() from @better-fs-tools/pi-durable reads and writes through the execution environment of each call.

Filesystems compares what each filesystem can do.

2. Choose your tools

There are five tools:

ToolPackageDoes
read@better-fs-tools/readReads a file within line, byte, and size limits
edit@better-fs-tools/writeReplaces text in a file that exists
write@better-fs-tools/writeCreates a file, or replaces a whole file
apply_patch@better-fs-tools/writeApplies a patch that adds, changes, moves, and deletes files
bash@better-fs-tools/shellRuns a command, with timeouts and output limits

Each tool has its own constructor, for example createAiSdkReadTool() or createPiEditTool(). Use one when you need only one tool:

import { createAiSdkReadTool } from "@better-fs-tools/ai-sdk";
import { nodeFileSystem } from "@better-fs-tools/node";

const cwd = process.cwd();
const read = createAiSdkReadTool({ fs: nodeFileSystem({ cwd, allowedRoots: [cwd] }) });
const tools = { [read.name]: read };

Or use the single constructor from step 1. It creates read, edit, and write over one filesystem. The tools share one digest, one lock manager, and one clock. Set applyPatch or bash to add those tools:

import { createAiSdkFsTools } from "@better-fs-tools/ai-sdk";
import { nodeFileSystem } from "@better-fs-tools/node";

const cwd = process.cwd();
const { tools } = createAiSdkFsTools({
  fs: nodeFileSystem({ cwd, allowedRoots: [cwd] }),
  applyPatch: true,
});
// tools has the keys read, edit, write, and apply_patch.

Use the single constructor when you install more than one tool. Constructors for single tools share nothing, so edit and write cannot know what read read. Choose an integration lists the constructors of each package.

3. Configure the tools for your use case

Each tool needs only fs. The bash tool needs a runner and an env instead. Every other option has a default. Set the options that your agent needs. In the single constructor, set the shared options once at the top level. Set the options of one tool under read, edit, write, applyPatch, or bash.

To do thisSetGuide
Refuse a change to a file that the model has not readstate: memoryStore()Read before write
Protect AGENTS.md, CLAUDE.md, and .gitedit and write: authorize: protectPaths()Write permissions
Refuse paths to the read tool, such as .envread: authorize: denyPaths([...])Read permissions
Change the read limits or add a token budgetread: limits or budgetRead limits
Read images, notebooks, PDFs, and foldersread: convertersFile types
Change how edit matches old textedit: matchersMatching
Change the bash timeouts, output limits, or commandsbash: limits, beforeRun, authorizeBash limits, Bash policy
Rename a tool or its parameters, or change the descriptionssignature on each toolTool definitions
Change the text that the model seesformatter and messages on each toolRead formatting, Write formatting

This example uses createFsTools() over a nodeFileSystem() and sets two options:

  • state: memoryStore() turns on read before write. Without it, edit and write change files that the model has not read.
  • protectPaths() refuses changes to AGENTS.md, CLAUDE.md, and the .git folder.
import { nodeFileSystem } from "@better-fs-tools/node";
import { memoryStore, textOf } from "@better-fs-tools/read";
import { createFsTools, protectPaths } from "@better-fs-tools/write";

// read, edit, and write over one nodeFileSystem rooted at process.cwd().
// memoryStore() turns on read before write.
const tools = createFsTools({
  fs: nodeFileSystem({ allowedRoots: [process.cwd()] }),
  state: memoryStore(),
  edit: { authorize: protectPaths() },
  write: { authorize: protectPaths() },
});

const created = await tools.write({ path: "config.ts", content: "export const a = 1;\n" });
console.log(textOf(created));

const read = await tools.read({ path: "config.ts" });
console.log(textOf(read));

const edited = await tools.edit({
  path: "config.ts",
  edits: [{ oldText: "a = 1", newText: "a = 2" }],
});
console.log(textOf(edited));

Run it in an empty folder. It prints the text that the model sees:

Created config.ts (1 line).
1|export const a = 1;
Edited config.ts: 1 replacement at line 1.

config.ts now contains export const a = 2;.

With the same tools, an edit of a file that the model has not read, and a write to AGENTS.md, return status: "error":

[edit:not-read] Read notes.txt with the read tool before changing it.
[write:denied] AGENTS.md was refused by policy (the path is protected).

tools.invalidate(path) makes the next change to path need a new read, for example after a shell command changed the file.

Where next

On this page