Reads and edits files and directories. The action that matters most is picking the cheapest deterministic way to make a change: an AI-assisted edit is the last resort, never the first.
edit — exact text. Pass old (which must occur exactly once in the file) and new. Read the file first and copy old verbatim, including indentation. This is the default for almost every change, and it either applies exactly or fails loudly — it cannot quietly edit the wrong line.edit_lines — line numbers. Pass an operations array when you know the exact lines ("change line 15"). Deterministic, no AI call.smart_edit — AI-assisted, for changes that genuinely cannot be expressed as text or line numbers: "rename this symbol everywhere", "add type hints to every function". It costs an LLM call and reads the whole file. Try the two above first, always.write — create a file or rewrite it entirely (pass content).query — ask a question about a file's content and get an answer instead of the file.read, append, list, find, find_up, grep, diff, info, copy, move, delete, exists, mkdir.| Parameter | Used by | Description | ||
|---|---|---|---|---|
action |
required | One of the actions listed above. | ||
path |
required | File or directory. ~ is expanded. |
||
content |
write, append |
Text to write or append. | ||
old / new |
edit |
Exact text to replace, and its replacement. | ||
operations |
edit_lines |
Array of `{"op": "replace"\ | "delete"\ | "insert", "start", "end", "after", "content"}. Lines are 1-based and inclusive;insertusesafter`. |
destination |
move, copy, diff |
Target path, or the second file to compare against. Missing parent directories are created. | ||
pattern |
find, find_up, grep |
Glob for find (e.g. *.log), exact filename for find_up, search text for grep. |
||
glob |
grep |
Optional filename filter for a recursive search, e.g. *.py. |
||
regex |
grep |
true for pattern matching, default false = literal substring. |
||
question |
query |
The question to answer from the file. | ||
instruction |
smart_edit |
The change to make, in natural language. | ||
offset / limit |
read |
First line (1-based) and how many to return. | ||
numbered |
read |
Default true, prefixed with 1-based line numbers. Set false for raw content you are about to copy into an edit. |
||
recursive |
list |
Full tree instead of the top level. |
{"action": "info", "path": "src/app.py"}
{"action": "read", "path": "src/app.py", "offset": 40, "limit": 30}
{"action": "read", "path": "src/app.py", "numbered": false}
{"action": "edit", "path": "src/app.py", "old": " return None", "new": " return {}"}
{"action": "edit_lines", "path": "src/app.py",
"operations": [{"op": "replace", "start": 15, "end": 15, "content": " limit = 50"}]}
{"action": "grep", "path": "src", "pattern": "TODO", "glob": "*.py"}
{"action": "find", "path": ".", "pattern": "*.log"}
{"action": "find_up", "path": "src/deep/file.py", "pattern": "pyproject.toml"}
{"action": "diff", "path": "src/app.py", "destination": "src/app.py.bak"}
info before read on an unknown file — it reports the size, which tells you whether a full read is going to flood the context.find_up walks from path towards the root looking for an exact filename — the way to locate pyproject.toml/.git from a nested directory.diff is a unified diff between two files — path against destination (both required; directories are rejected).query and smart_editBoth spend an LLM call, so both are for what the deterministic actions cannot do.
query answers a question instead of returning the file — use it to avoid reading a large file whose content you only need one fact from:
{"action": "query", "path": "src/app.py", "question": "What does calculate() return?"}
{"action": "query", "path": "src/app.py", "question": "Where is class UserManager defined?"}
{"action": "query", "path": "deploy.sh", "question": "Which environment variables does this read?"}
smart_edit takes an instruction in natural language:
{"action": "smart_edit", "path": "src/app.py", "instruction": "Rename process to handle_request"}
{"action": "smart_edit", "path": "src/app.py", "instruction": "Add type hints to every function"}
{"action": "smart_edit", "path": "src/app.py", "instruction": "Replace the hardcoded URL with a constant BASE_URL"}
It reads the whole file and can touch more than you asked. When the change is expressible as exact text or line numbers, edit/edit_lines do it exactly, for free, and fail loudly instead of approximately.
In multi-user mode every path is resolved inside user_data/<user_id>/; a path that escapes it returns Access denied: ... outside allowed paths. Do not work around it by writing to /tmp or an absolute path — ask for the file to be placed in the workspace instead. In single-user/admin mode the allowed roots are the working directory and the configured paths.
edit with an old string that appears twice — the call is refused, correctly. Include more surrounding context until it is unique.old copied from a numbered read — the line-number column is not part of the file. Read with numbered: false when copying text into an edit.write to change one line: it replaces the whole file, and anything you did not retype is gone. Use edit.smart_edit for something edit could do — an LLM call to change a line is a waste, and it can touch more than you asked.delete on a directory — it removes the whole tree recursively, with no prompt and no undo.mkdir for a path whose parent does not exist: it does not create intermediate directories. write does.