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.
| Host | Package | Helper | Guide |
|---|---|---|---|
AI SDK 7 generateText or streamText | @better-fs-tools/ai-sdk | createAiSdkFsTools() | AI SDK |
Pi coding agent: the pi command or SDK | @better-fs-tools/pi | createPiFsTools() | Pi |
pi-agent-core or pi-ai | @better-fs-tools/pi/agent-core | createAgentCoreFsTools() | Pi agent core |
pi-durable, experimental | @better-fs-tools/pi-durable | createDurableFsTools() | Pi durable |
| A model framework of your own | @better-fs-tools/write | fsToolDefinitions() | Tool definitions |
| Your own code: Node, Bun, or a Worker | @better-fs-tools/write | createFsTools() | 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/writeWith 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.
| Filesystem | Package | Use it for |
|---|---|---|
nodeFileSystem() | @better-fs-tools/node | Local files on Node 24 or later, or Bun |
memoryFileSystem() | @better-fs-tools/fs | Tests, examples, and files that you keep in memory |
justBashFileSystem() | @better-fs-tools/just-bash | A just-bash sandbox, with no real files |
cloudflareShellFileSystem() | @better-fs-tools/cloudflare-shell | A Cloudflare Shell workspace in a Worker |
cloudflareComputerFileSystem() | @better-fs-tools/cloudflare-computer | A 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/nodegives anodeFileSystem()rooted atctx.cwdon each call. - pi-durable:
durableEnvFileSystem()from@better-fs-tools/pi-durablereads and writes through the execution environment of each call.
Filesystems compares what each filesystem can do.
2. Choose your tools
There are five tools:
| Tool | Package | Does |
|---|---|---|
read | @better-fs-tools/read | Reads a file within line, byte, and size limits |
edit | @better-fs-tools/write | Replaces text in a file that exists |
write | @better-fs-tools/write | Creates a file, or replaces a whole file |
apply_patch | @better-fs-tools/write | Applies a patch that adds, changes, moves, and deletes files |
bash | @better-fs-tools/shell | Runs 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 this | Set | Guide |
|---|---|---|
| Refuse a change to a file that the model has not read | state: memoryStore() | Read before write |
Protect AGENTS.md, CLAUDE.md, and .git | edit and write: authorize: protectPaths() | Write permissions |
Refuse paths to the read tool, such as .env | read: authorize: denyPaths([...]) | Read permissions |
| Change the read limits or add a token budget | read: limits or budget | Read limits |
| Read images, notebooks, PDFs, and folders | read: converters | File types |
Change how edit matches old text | edit: matchers | Matching |
| Change the bash timeouts, output limits, or commands | bash: limits, beforeRun, authorize | Bash limits, Bash policy |
| Rename a tool or its parameters, or change the descriptions | signature on each tool | Tool definitions |
| Change the text that the model sees | formatter and messages on each tool | Read formatting, Write formatting |
This example uses createFsTools() over a nodeFileSystem() and sets two options:
state: memoryStore()turns on read before write. Without it,editandwritechange files that the model has not read.protectPaths()refuses changes toAGENTS.md,CLAUDE.md, and the.gitfolder.
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
- Choose an integration: the defaults and options that every integration shares
- AI SDK and Pi: give the tools to an agent
- Filesystems: the Node, memory, just-bash, and Cloudflare filesystems
- Read tool, write tools, and bash tool: every option