Search and edit files
Search file contents, edit files with patches that check the file's revision, and read a suspended workspace's files without waking it.
Install once with npm i -g shardflux, then use the shard command. If npm reports EACCES or EPERM, follow the user-directory install and use "$HOME/.local/bin/shard".
What the file tools do
Besides reading, writing, listing and removing files, a workspace can:
- search file contents under a directory, like
grep -rn, without starting a command; - patch a file: replace exact text, or the whole content, atomically, optionally only if the file still has the revision (the SHA-256 of its content) your change is based on;
- answer reads of a suspended workspace from its disk, without resuming it.
| Client | Version | Search | Patch | Revision of a file |
|---|---|---|---|---|
| TypeScript SDK | @shardflux/sdk (0.9.0+) |
cell.files.search() |
cell.files.patch() |
cell.files.stat(path, { revision: true }), cell.files.readWithInfo() |
| Python SDK | shardflux (0.5.0+) |
ws.files.search() |
ws.files.patch() |
ws.files.stat(path, revision=True), ws.files.read_with_info() |
| CLI | @shardflux/cli (0.5.0+) |
shard files search |
shard files patch |
shard files stat --revision |
| Agent tools and MCP server | @shardflux/sdk (0.9.0+), @shardflux/mcp (0.4.0+) |
search_files |
edit_file |
read by edit_file itself |
| HTTP (cell endpoint) | /v1 |
POST .../files/search |
POST .../files/patch |
GET .../files/stat?revision=true, X-File-Revision |
They work the same on file-first workspaces.
Search file contents
const cell = workspace.cell();
const hits = await cell.files.search('/home/user/project', 'TODO', { include: ['*.py'], contextLines: 1 });
for (const m of hits.matches) console.log(`${m.path}:${m.line}:${m.column}: ${m.text}`);
if (hits.truncated) console.log('stopped early:', hits.stop_reason);hits = ws.files.search("/home/user/project", "TODO", include=["*.py"], context_lines=1)
for m in hits["matches"]:
print(f"{m['path']}:{m['line']}:{m['column']}: {m['text']}")shard files search customer-42/main /home/user/project TODO --include '*.py'
shard files search customer-42/main /home/user/project 'def \w+_test' --regex -i --json- The pattern is literal text, or an RE2 regular expression with
regex(--regex).caseInsensitive/case_insensitive(-i) ignores case. pathis a directory, or one file. Matches come in path order, each withpath, the 1-basedline, the 1-based bytecolumnof the first match on the line, andtext(the line, up to 1,000 bytes).contextLines(0-5) addsbeforeandafterlines. The CLI printspath:line:column: text, and context lines aspath-line- text.includeandexcludetake up to 32 gitignore-style globs each:*.pymatches a name at any depth,src/**/*.tsa path relative topath, a leading/anchors topath, and a trailing/matches directories only (build/). By default.gitandnode_modulesare skipped; anexcludeyou give replaces that default ([]searches everything).- Symbolic links are not followed. Binary files (a NUL byte in the first 8 KiB), special files,
/procand/sys, and files larger thanmaxFileBytes/max_file_bytes(default 1 MiB, at most 64 MiB) are skipped. Each line is searched in its first 1 MiB. - A search stops at
maxMatches/max_matches(default 200, at most 5,000;--maxin the CLI), after 10 seconds, or at 4 MiB of results. Thentruncatedistrueandstop_reasonismax_matches,budgetormax_bytes.files_scannedcounts the files searched. - A search is read-only: the SDKs retry it like a
GET.
Edit a file with a patch
const path = '/home/user/project/app.py';
const { revision } = await cell.files.stat(path, { revision: true });
const patched = await cell.files.patch({
path,
edits: [{ oldText: 'DEBUG = True', newText: 'DEBUG = False' }], // must occur exactly once (or replaceAll)
expectedRevision: revision, // refused if the file changed meanwhile
});
patched.revision; // the file's new revision: the expectedRevision of your next patchpath = "/home/user/project/app.py"
revision = ws.files.stat(path, revision=True)["revision"]
patched = ws.files.patch(
path,
edits=[{"old_text": "DEBUG = True", "new_text": "DEBUG = False"}], # must occur exactly once
expected_revision=revision, # refused if the file changed meanwhile
)
patched["revision"] # the next expected_revisionREV=$(shard files stat customer-42/main /home/user/project/app.py --revision --json | jq -r .revision)
shard files patch customer-42/main /home/user/project/app.py --old "DEBUG = True" --new "DEBUG = False" --expected-revision "$REV"- A patch takes exactly one of
editsorcontent. Each edit's old text must occur exactly once in the file, unlessreplaceAll/replace_all(--replace-all) replaces every occurrence. Edits apply in order to the file's UTF-8 text, all of them or none.contentreplaces the whole file, or creates it (--content-file F, or-for standard input). expectedRevision/expected_revision(--expected-revision) makes the patch apply only to that revision of the file.absentrequires that the file does not exist yet.- A patch is atomic and durable: it is acknowledged after the file and its directory are written to disk. An existing
file keeps its mode and owner. A new file gets
mode(default0644);createParents/create_parentscreates missing directories. - The SDKs send an
Idempotency-Keywith every patch, so a retried request is applied once. - The result has the file's new
revision, itsprevious_revision,bytes_written,durable,replacements(the number of replaced occurrences) andfile(its metadata). The CLI prints the new revision. - A request is at most 7 MiB and a file at most 64 MiB; write larger files with a normal write. A patch has at most 100 edits. A patch through a symbolic link is refused.
| Refusal | details |
Meaning |
|---|---|---|
409 conflict, reason revision_mismatch |
current_revision |
The file changed since the revision you gave (or exists, with absent). Nothing changed: read it again and redo the edit. |
422 validation_failed, reason edit_not_found |
index |
The edit at that position (from 0) does not occur in the file. |
422 validation_failed, reason edit_ambiguous |
index |
That edit's old text occurs more than once: include more surrounding text, or replace all. |
422 validation_failed, reason edit_not_text |
The file is not UTF-8 text. Replace it with content instead. |
|
422 validation_failed, reason patch_invalid |
Not exactly one of edits and content. |
|
404 not_found |
No such file, and the patch has edits (only content creates a file). |
|
413 payload_too_large |
max_bytes |
The request is over 7 MiB. |
Revisions
A file's revision is the SHA-256 of its content, as 64 lowercase hex characters. It is what expectedRevision takes.
| Where | Returns the revision |
|---|---|
stat(path, { revision: true }), stat(path, revision=True), shard files stat --revision, GET .../files/stat?revision=true |
For regular files up to 256 MiB. |
readWithInfo(path) (TypeScript), read_with_info(path) (Python), the X-File-Revision header of a read |
For regular files up to 16 MiB: { data, size, revision, servedFrom }. A read continued over several requests has a revision only if every part had the same one. |
| Every write and patch result | revision, the file's revision after the change (for a replace, the same value as a write's sha256). |
read() and readText() / read_text() still return bytes and text. On a file-first
workspace revisions are returned for files of any size.
Read a suspended workspace without waking it
Reads of a suspended workspace are answered from its saved disk: no resume, no compute, and the files as they were when it was suspended. The workspace stays suspended.
| Client | Reads served from the disk | How you can tell |
|---|---|---|
| TypeScript SDK (0.9.0+) | read, readText, readWithInfo, stat, list, search |
servedFrom: 'disk' (readWithInfo), served_from: 'disk' (search) |
| Python SDK (0.5.0+) | read, read_text, read_with_info, stat, list, search |
served_from == "disk" |
| CLI (0.5.0+) | files read, ls, stat, search |
a note on stderr; served_from with --json |
| MCP server (0.4.0+) | read_file, list_files, search_files |
|
| HTTP (cell endpoint) | GET .../files, files/stat, files/list, POST .../files/search |
X-Served-From: disk |
- Every other call wakes the workspace as before: writes, patches, commands.
- When a read cannot be served from the disk, it is refused with
409 workspace_not_running(details.reason:offline_unavailableoroffline_budget), and the SDKs, the CLI and the MCP server wake the workspace and read again. - These reads are not tool activity and are not billed as compute.
- A running workspace that its host has parked may be read
the same way, without waking it, so
served_from: 'disk'can appear for a running workspace too. - The API issues tool tokens for suspended workspaces for these reads, so a client that had no token before the suspend can read too.
Agent tools: search_files and edit_file
workspaceTools() in the TypeScript SDK (0.9.0+) and the MCP server (0.4.0+) add two tools with the files
permission:
| Tool | Arguments (required in bold) | Returns |
|---|---|---|
search_files |
path, pattern, regex, case_insensitive, include, exclude, max_matches (1-5,000), context_lines (0-5) |
matches, truncated, stop_reason, omitted_matches, files_scanned |
edit_file |
path, edits (1-100 of {old_text, new_text, replace_all}), expected_revision |
path, revision, previous_revision, replacements, bytes_written |
search_filesreturns whole matches up to the tools' output limit (64 KiB by default) and counts the rest inomitted_matches.edit_filepins every edit to a revision. When the model gives noexpected_revision, the tool reads the file's revision first, so a change made in between fails the edit (revision_mismatch) instead of being overwritten. Therevisionit returns is theexpected_revisionof the next edit of the same file.- Python:
workspace_tools()has both tools (shardflux0.6.0+), andws.files.search()andws.files.patch()call the same endpoints directly.
See Give your agent workspace tools for the rest of the tools.
When search or patches are not available
If a workspace does not offer search or patches, they are refused with 409 conflict,
details.reason: host_feature_unavailable and details.feature (file_search or file_patch), retryable: false,
and reads and stats come without revisions. Use grep -rn in a command instead of a search, or a read and a write
instead of a patch.