Better FS Tools
Read tool

Read tool

Create the read function with createReadTool, and configure it with options.

To give a read tool to an agent, use a provider integration such as AI SDK or Pi. The integration gives you the tool definition: the name, the description, and the parameter schema. It takes the options on this page, plus a signature for the tool definition. The defaults are the same in every integration.

createReadTool is the lower-level API. It creates the function that implements the tool. Use it when you write your own integration, or when your code reads files for the agent.

createReadTool needs a filesystem in fs. All other options are optional:

import { nodeFileSystem } from "@better-fs-tools/node";
import { createReadTool } from "@better-fs-tools/read";

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

On Node or Bun, pass nodeFileSystem() from @better-fs-tools/node, as above. In a Worker or a sandbox, pass another filesystem. fs can also be a function that returns a filesystem for each call.

OptionDefaultUse it to
fsRequiredSet the filesystem, the allowed folders, and the symlink policy
formatterlineNumberFormatter()Change the text the tool returns
messagesdefaultReadMessagesChange the wording of notes
resolvenullRewrite paths before the tool opens them
suggestdefaultSuggest()Suggest names when a file is missing
authorizenull (allow all)Refuse paths, large files, or ask the user
classifiersdefaultClassifiers()Decide which files are text, images, PDFs, or binary
converters[imageConverter()]Read images, notebooks, PDFs, and folders
limitsdefaultReadLimitsChange the line, byte, and size limits
budgetnullStop a read at a token budget
hooks[]Change a result after the read, for example to redact secrets
statenullStore each read, so later calls and tools know what was read
digestsha256Digest()Hash file content. state needs it. null turns it off
clock() => new Date()Set the time used in stored reads

limits and messages merge with their defaults key by key. Every other option replaces its default. createReadTool throws TypeError for an unknown option or an option with the wrong shape.

The read function

createReadTool returns read(input, ctx?). input is { path, offset?, limit? }. offset is the first line, from 1. limit is the number of lines. read returns a Promise<ReadResult>:

import { textOf } from "@better-fs-tools/read";

const result = await read({ path: "src/greet.ts", limit: 3 });

result.status; // "ok"
textOf(result); // the text for the model
if (result.status === "ok") {
  result.view.lines; // [{ number: 1, text: "export function greet(name: string): string {", ... }, ...]
  result.continuation.next; // { path: "src/greet.ts", offset: 4, limit: 3 }
}

status is ok, media, unsupported, or error. result.content holds the parts to send to the model, and textOf(result) joins its text parts. result.notes holds each note as data. The read reference lists every field.

ctx is optional. It holds signal to cancel the read, callId, and host. The tool passes host to your options, for example to find the store for a session. See Hooks and state.

Formatting

By default the tool returns line-numbered text, followed by notes for errors, warnings, and continuation:

1|export function greet(name: string): string {
2|  return `Hello, ${name}!`;
3|}

[read:continue] Output stopped at the line limit. Continue with {"path":"src/greet.ts","offset":4,"limit":3}.

Every other option can add a note. A failed read returns only a note:

[read:not-found] docs/setpu.md was not found. Nearby names: "setup.md".

Set formatter to change the layout, and messages to change the wording of a note:

import { plainFormatter } from "@better-fs-tools/read";

const read = createReadTool({
  fs,
  formatter: plainFormatter(),
  messages: {
    notFound: ({ request }) => `No file at ${request.path}. List the folder first.`,
  },
});
[read:not-found] No file at docs/setpu.md. List the folder first.

The formatter changes only result.content. The other fields of the result stay the same. Customise formatting shows the built-in formatters, the formats of other harnesses, line hashes, JSON output, and how to hide notes.

Path handling

The filesystem decides which paths the tool can open. nodeFileSystem refuses a path outside allowedRoots:

[read:outside-allowed-roots] ../outside.txt is outside every configured allowed root.

Set resolve to rewrite paths before the tool opens them, for example to accept @src/greet.ts or ~/notes.md:

import { homedir } from "node:os";
import { expandHome, pathResolvers, stripPrefixes } from "@better-fs-tools/read";

const read = createReadTool({
  fs,
  resolve: pathResolvers(stripPrefixes(), expandHome({ home: homedir() })),
});

A resolver cannot give access to more files. The filesystem checks the rewritten path. result.file.resolvedFrom holds the original path when a resolver changed it.

When a file is missing, suggest finds similar names in the same folder, as in the not-found note above. Set suggest: null to turn this off. See Path handling.

Permissions

Set authorize to refuse reads that the filesystem allows:

import { denyPaths, readAuthorizers, sizeCeiling } from "@better-fs-tools/read";

const read = createReadTool({
  fs,
  authorize: readAuthorizers(
    denyPaths(["**/.env", "**/*.pem"]),
    sizeCeiling({ maxBytes: 64 * 1024, unrangedOnly: true }),
  ),
});

A refused read returns status error with code DENIED:

[read:denied] .env was refused by policy (the path matches a denied pattern).

askUser asks the user before each read. Some backends fetch the whole file before authorize runs. See Permissions.

File types

By default the tool reads text and images. It returns unsupported for PDFs, Office documents, and binary files, and error for a folder. A Jupyter notebook is JSON text, so the tool reads it as lines. Set converters to read the other types:

import { directoryListing, imageConverter } from "@better-fs-tools/read";

const read = createReadTool({ fs, converters: [imageConverter(), directoryListing()] });

An image returns status media, with the image in result.content. A folder returns one entry for each line. A list that you set replaces the default, so keep imageConverter() in it to read images. Set converters: [] to turn images off. Set classifiers to change how the tool decides the file type. See File types.

Limits

A read stops at 2,000 lines or 50 KiB, and a line stops at 2,000 characters. The result then has a continuation for the next part. Set limits to change these values, and budget to stop at a token count:

import { charsPerToken } from "@better-fs-tools/read";

const read = createReadTool({
  fs,
  limits: { maxLines: 500 },
  budget: charsPerToken({ ratio: 4, max: 2_000 }),
});

result.truncation tells you which limit stopped the read. See Limits for every limit.

Hooks

Hooks run after the read and before the formatter. A hook can change the lines, the notes, and the content. redact replaces secrets:

import { redact } from "@better-fs-tools/read";

const read = createReadTool({
  fs,
  hooks: [redact({ patterns: [/AKIA[0-9A-Z]{16}/g] })],
});
1|export const region = "eu-west-1";
2|export const accessKeyId = "[REDACTED]";

[read:view-modified] The redact hook changed this view, so it is not the exact file text.

A hook cannot turn a failed read into a successful one. If a hook throws, the read returns EXTENSION_FAILED. See Hooks and state to write your own hook.

State

Set state to store a record of each read. When the edit and write tools share the store, they use the records to check that the model read a file before it changes it. state needs a digest. The default digest is sha256Digest(), and on Node nodeDigest() is faster:

import { nodeDigest } from "@better-fs-tools/node";
import { memoryStore, repeatReadGuard } from "@better-fs-tools/read";

const read = createReadTool({
  fs,
  digest: nodeDigest(),
  state: memoryStore(),
  hooks: [repeatReadGuard()],
});

repeatReadGuard uses the records. When the model reads the same unchanged lines again, it returns a note in place of the lines. nodeDigest() and sha256Digest() give the same values, so records made with one are valid for the other. To use one store for each session, pass a function as state. See Hooks and state.

On this page