Every file flow uses one of five built-ins. read pulls bytes (with line ranges, archive members, URL fetches), write creates or overwrites a whole file, edit applies a line-anchored patch, find resolves path globs, and search runs regex content lookups. For structural rewrites and merge conflicts jump to structural edits; for symbol-aware renames see code intelligence.

read

One path argument handles disk files, directories, archives, SQLite databases, PDFs, Office documents, Jupyter notebooks, images, and ordinary web URLs. The same parameter resolves internal schemes: skill://, pr://, issue://, agent://, artifact://, history://, memory://, mcp://, local://, rule://, vault://, conflict://.

Append a selector with : to scope the read. :50-200 is a line range, :50+150 is a count form, :raw bypasses summarization, :conflicts indexes merge-conflict blocks. Output carries a [path#TAG] snapshot header and numbered lines (41:text) so edit can reference exact lines later. A bare path on parseable source returns a structural summary — signatures kept, bodies elided. Re-issue with a range or :raw when you need the bodies.

# line range from a file inside a tarball
read "build/bundle.tar.gz:src/app.ts:120-180"
 
# raw verbatim slice (no summary, no anchors)
read "src/parser.ts:1-40:raw"
 
# fetch and clean a web page
read "https://example.com/docs/api"
 
# URL schemes share the same selector grammar as files
read pr://1234/diff/2
read agent://AuthLoader/findings
read conflict://1

write

write creates a new file or replaces one wholesale. The dispatcher matches read: archive.ext:inner/path writes into an archive, db.sqlite:table inserts a row, db.sqlite:table:key updates or deletes one. Generated files are guarded against accidental overwrite; the format-on-save pass runs before bytes hit disk.

write path="src/routes/health.ts" content="export const ok = () => 'ok';\n"

Reach for edit instead when the file already exists and you only need to change part of it. Whole-file rewrites lose anchor history and are noisier in diff review.

edit

edit applies a line-anchored patch verified against a per-session snapshot store. The model reads a slice, copies the [path#TAG] header off the read output — the four-hex tag fingerprints the whole file — and emits ops against plain line numbers. If the file moved since the read — another agent, a formatter, or a manual save — the tag no longer matches and the patch is recovered or rejected instead of clobbering the wrong line. The fix is always the same: re-read the slice and emit a fresh patch against the new tag.

The patch grammar (variant hashline, the default) is a single input string with one or more file sections. Every section starts with [PATH#TAG]; ops name plain line numbers. Payload lines start with +.

OpEffect
replace N..M:Replace the inclusive line range with the payload lines.
replace block N:Replace the whole syntactic block beginning on line N (tree-sitter resolved).
delete N..MDelete the inclusive line range. No payload.
insert after N: / insert before N:Insert payload lines after/before line N (insert head: / insert tail: target the file ends).
# 1. read the slice first to capture the snapshot tag
read src/auth.ts:80-90
# →  [src/auth.ts#1F2A]
#    87:  return loadUser(id);
#    88:}
 
# 2. patch by line number, anchored to the tag
edit input="[src/auth.ts#1F2A]
replace 87..87:
+  return await loadUser(id);
"

Override the grammar per session with the PI_EDIT_VARIANT environment variable; accepted values are hashline (default), patch, apply_patch, and replace. The matching edit.mode setting in ~/.omp/agent/config.yml does the same persistently.

When the change is structural — rename a symbol, swap an API shape, rewrite every callsite — use ast_edit or lsp rename. Both ignore whitespace and survive formatting churn that breaks line-anchored patches.

find

find resolves path globs. Pass one or more patterns in paths; results are newline-delimited, relative to cwd, sorted by mtime (most recent first). Honors .gitignore by default. Use it to enumerate without reading: the split with search is deliberate so the model can’t accidentally load every matching file into context.

# every TypeScript route file, newest first
find paths=["src/routes/**/*.tsx"]
 
# multiple roots in one call
find paths=["apps/**/package.json", "packages/**/package.json"]

search runs a regex against file content across files, directories, globs, or internal URLs. Matches come back as anchor-prefixed lines (*5th|content); context lines use a leading space. Honors .gitignore. Cross-line patterns auto-enable when the regex contains a literal \n. The native engine paginates — skip walks past earlier matches without re-scanning.

# every TODO with the author tag, case-insensitive
search pattern="TODO\\(\\w+\\)" paths=["src/"] i=true
 
# cross-line: function signature followed by an empty body
search pattern="function \\w+\\([^)]*\\)\\s*\\{\\n\\s*\\}" paths=["src/"]

Use find when you only need the path list and search when you need to see the matched content. For structural matches that ignore whitespace and comments, reach for ast_grep on the structural edits page.

Worked flow

The four most common file-touching turns share one shape: narrow, read, patch, verify.

1. find paths=["src/**/*.ts"]                              # enumerate
2. search pattern="loadUser\\(" paths=["src/"]             # locate callsites
3. read src/auth.ts:80-120                                 # capture anchors
4. edit input="@@ src/auth.ts
   = 87qa..87qa
   ~  return await loadUser(id);
   "                                                      # patch by anchor
5. lsp action=diagnostics file=src/auth.ts                 # verify

See code intelligence for the lsp tool and structural edits for ast_edit and the conflict:// URL surface.\n

Ask Docs

AI assistant to help answer questions about the documentation. Answers are read-only and cite docs/source.

Hi! How can I help you with the documentation today? Answers are read-only and cite docs/source.

Ctrl+Enter to send