Four tools cover file content over the API: `read`, `write`, `edit` and `bash`. Paths are workspace-absolute (`/Projects/auth.py`), Monofile versions every write, and the writes of one call land in one changeset. The request envelope is the same for all of them, see [[Calling Tools]].

```bash title="Terminal"
curl -X POST "https://www.monofile.ai/api/tools/read" \
  -H "Authorization: Bearer $MONOFILE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path": "/Projects/auth.py", "includeMetadata": true}'
```

## Files

### read

Returns a file's raw content, optionally with its metadata.

| Input | Type | Required | Description |
|---|---|---|---|
| `path` | string | yes | Full path to the file |
| `includeMetadata` | boolean | no | Add `id`, `name`, `path`, `mimeType`, `language`, `size`, `lines` and `modified`. Default `false` |

Scope: `files:read`. Ask for metadata when you plan to edit: `modified` is the timestamp you pass back as `expectedUpdatedAt`.

```http
POST /api/tools/read
{ "path": "/Projects/auth.py", "includeMetadata": true }
```

### write

Creates a file, or overwrites one that already exists. Monofile keeps the previous content as a version, so you can restore it after an overwrite.

| Input | Type | Required | Description |
|---|---|---|---|
| `path` | string | yes | Full path to create or replace |
| `content` | string | yes | The new content |
| `createParents` | boolean | no | Create missing parent folders. Default `true` |

Scope: `files:write`. Two file types have their own tools because their content is machine-generated JSON. Use `create_diagram` for `.draw` and `canvas_edit` for `.canvas`, both on [[Content Tools]].

```http
POST /api/tools/write
{ "path": "/Projects/notes/plan.md", "content": "# Plan\n\n- [ ] draft\n" }
```

### edit

Replaces an exact string inside a file. Prefer it to `write` on an existing file, because it changes one region and leaves the rest of the document alone.

| Input | Type | Required | Description |
|---|---|---|---|
| `path` | string | yes | Full path to the file |
| `old_string` | string | yes | The exact text to find, case-sensitive |
| `new_string` | string | yes | The replacement. Empty deletes the matched text |
| `replace_all` | boolean | no | Replace every occurrence. Default `false` |
| `dryRun` | boolean | no | Preview without writing. Default `false` |
| `showDiff` | boolean | no | Include a unified diff in the response. Default `true` |
| `expectedUpdatedAt` | string | no | ISO timestamp from a prior read. Refuses the edit if the file changed since |

Scope: `files:write`.

```http
POST /api/tools/edit
{
  "path": "/Projects/auth.py",
  "old_string": "def login(user):",
  "new_string": "def login(user, remember=False):"
}
```

### The edit contract

The tool matches `old_string` literally. No regular expressions, no whitespace normalization, no case folding. Most failed calls trip on that, and each failure has its own code:

| Error | What happened | What to do |
|---|---|---|
| `NO_MATCH` | `old_string` is not in the file | Read the file again. Your copy is stale, or the indentation or line endings differ from what you assumed |
| `MULTIPLE_MATCHES` | More than one occurrence and `replace_all` is `false`. The message carries the count | Extend `old_string` with surrounding lines until it is unique, or set `replace_all: true` if you really mean all of them |
| `EDIT_CONFLICT` | You passed `expectedUpdatedAt` and the file changed after that timestamp | Re-read, rebuild `old_string` from the new content, retry |
| `INVALID_INPUT` | `old_string` is empty, or equal to `new_string` | Send a non-empty search string that differs from the replacement |

> [!TIP]
> Two habits make edits reliable. Include enough context that the match can only be the line you mean, and pass `expectedUpdatedAt` whenever another agent or a person might be in the same file. Use `dryRun: true` to see `matchesFound` and the diff before committing to anything destructive.

## Shell

`bash` runs a command against your workspace. Read-only commands answer instantly. Everything else needs a real machine, so it runs in a sandbox, and the sandbox is part of paid plans.

### The two tiers

**The built-in shell** is on every plan. A read-only chain of the commands below, joined by `;`, `&&` or `||`, runs in-process straight against storage. It is instant and always sees the latest content.

| Command | Flags it reads |
|---|---|
| `pwd` | none |
| `ls` | `-l`, `-a`, `-h` |
| `tree` | `-L N`, `--depth N` |
| `cat` | `-n` |
| `head` / `tail` | `-n N` |
| `stat` | none |
| `wc` | `-l`, `-w`, `-c` |
| `find` | `-name GLOB`, `-type f` or `-type d`, `-maxdepth N`, `-mtime N`, `-size N`. Predicates are AND-ed, there is no `-not` or `-o` |
| `grep` | `-i`, `-n`, `-l`, `-c`, `-v`, `-w`, `-A N`, `-B N`, `-C N`, `--include=GLOB`, `--exclude=GLOB`. Always recursive, JavaScript regex, line numbers on by default |
| `mkdir` | `-p` |
| `rm` | `-r`, `-R`, `-f` |
| `mv` / `cp` | `cp` takes `-r`, `-R` |
| `touch` | none |
| `echo` / `printf` | redirection with `>` and `>>`. `printf` handles `%s`, `\n`, `\t` |

One path per command, no glob expansion, and flag values are space-separated (`-n 5`, not `-n5`). This tier has no pipes, `<`, `2>`, command substitution, background `&`, `sed`, `awk`, `sort` or interpreters.

**The full shell** is part of paid plans (Pro on a personal workspace, Team on an org workspace). Anything that is not a read-only built-in chain runs in a real `sh` with real coreutils, inside a persistent per-workspace sandbox whose disk mirrors your files. That is where pipes, redirects, `sed`, `awk`, `sort`, `jq`, `git`, `python3`, `node` and multi-line scripts work. Monofile writes the file changes the command leaves behind into the workspace as versioned edits, one changeset per call.

### bash

| Input | Type | Required | Description |
|---|---|---|---|
| `command` | string | yes | The command to run |
| `timeout` | number | no | Full shell only. Seconds before the command is killed. Default 60, larger values are clamped to 240 |
| `cwd` | string | no | Full shell only. Workspace-absolute working directory without `..` segments. Default `/` |
| `debug` | boolean | no | Full shell only. Adds sandbox and usage detail to the result |

The scope a command needs follows from what it does. Reading needs `files:read`. `mkdir`, `mv`, `cp`, `touch` and `echo`/`printf` with `>` need `files:write`. `rm` needs `files:delete`. The full shell checks the write-back diff, so a missing scope rejects the whole diff and writes nothing.

```http
POST /api/tools/bash
{"command": "grep -rn \"TODO\" /Projects --include=*.ts"}
```

```bash title="Terminal"
curl -X POST "https://www.monofile.ai/api/tools/bash" \
  -H "Authorization: Bearer $MONOFILE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": "grep -rn \"TODO\" /Projects --include=*.ts"}'
```

`grep` and `find` exit 1 when they match nothing. Treat that as an empty result.

### What the result tells you

A built-in run returns `output`, `exitCode` and, on failure, `stderr`. A full-shell run adds these fields:

| Field | Meaning |
|---|---|
| `changes` | `created`, `updated`, `deleted` and `renamed` paths the run wrote back |
| `conflicts` | Files that changed elsewhere while your command ran. Monofile kept your version, and the one it replaced is in that file's versions |
| `skipped` | Paths the command touched but Monofile did not write back, each with a reason such as `symlink`, `binary`, `too-large` or `media` |
| `warnings` | Readable notes for the timeout, conflicts and skips above |
| `truncated`, `timedOut` | Output hit the cap of 400 lines or 30 KB, or the command was killed. Changes made before a kill are still written back |
| `timings` | Where the milliseconds went. Always present |

Errors you can plan for: `SANDBOX_BUSY` (one command per workspace at a time, retry), `RATE_LIMITED`, `DIFF_TOO_LARGE` (more than 200 changed files), `INSUFFICIENT_CREDITS`, `SANDBOX_UNAVAILABLE` and `TIMEOUT`.

:::details Without the paid tier
When a workspace is not entitled to the sandbox, any command the built-in shell knows still runs there, so listing, reading, searching and basic file management keep working. A pipe, a script or an interpreter needs the sandbox, so it comes back as `SANDBOX_NOT_ENTITLED`, and the message names the built-in alternatives.

`GET /api/tools` describes the `bash` tool as your own key sees it, including which tier it has.
:::

## Next steps

:::cards
- [[Calling Tools]] — The one-endpoint pattern, the MCP-shaped envelope, and the index of every tool.
- [[Content Tools]] — Canvas, diagram, version and checkpoint tools.
- [[Errors and Limits]] — Every error code, what causes it, and how to recover.
- [[Filesystem API]] — The manifest and batch endpoints for bulk sync clients.
:::
