> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/nullclaw/nullclaw/llms.txt
> Use this file to discover all available pages before exploring further.

# File Operations

> Secure file I/O tools with workspace scoping

# File Operations

NullClaw provides four file operation tools with strict path security and workspace scoping:

* `file_read` — Read file contents
* `file_write` — Write or overwrite files
* `file_edit` — Find and replace text
* `file_append` — Append content to files

All file tools enforce:

* **Workspace scoping** — operations restricted to `workspace_dir` by default
* **Absolute path support** — with explicit `allowed_paths` configuration
* **Path traversal protection** — `../` patterns blocked
* **Symlink validation** — links validated after resolution
* **System path blocklist** — sensitive paths like `/etc/passwd` rejected

## file\_read

Read file contents with size limits and path validation.

### Parameters

<ParamField path="path" type="string" required>
  Relative path to the file within the workspace
</ParamField>

### Configuration

```zig theme={null}
const ft = try allocator.create(file_read.FileReadTool);
ft.* = .{
    .workspace_dir = "/path/to/workspace",
    .allowed_paths = &.{},  // Empty = workspace-only
    .max_file_size = 10 * 1024 * 1024,  // 10MB default
};
```

### Usage

```json theme={null}
{
  "tool": "file_read",
  "path": "src/main.zig"
}
```

**Response:**

```
const std = @import("std");

pub fn main() !void {
    std.debug.print("Hello, world!\n", .{});
}
```

### Error Cases

* **File not found** — Returns failure with "Failed to resolve file path"
* **Path traversal** — Returns "Path not allowed: contains traversal or absolute path"
* **Outside workspace** — Returns "Path is outside allowed areas"
* **File too large** — Returns "File too large: X bytes (limit: Y bytes)"
* **Missing parameter** — Returns "Missing 'path' parameter"

### Absolute Path Support

To read files outside the workspace:

```zig theme={null}
ft.* = .{
    .workspace_dir = "/workspace",
    .allowed_paths = &.{
        "/data",
        "/home/user/configs",
    },
};
```

Then read absolute paths:

```json theme={null}
{
  "tool": "file_read",
  "path": "/data/input.json"
}
```

<Warning>
  Absolute paths without `allowed_paths` configuration are rejected with: "Absolute paths not allowed (no allowed\_paths configured)"
</Warning>

### Source

`src/tools/file_read.zig:13-91`

***

## file\_write

Write contents to a file with atomic temp+rename for hard link safety.

### Parameters

<ParamField path="path" type="string" required>
  Relative path to the file within the workspace
</ParamField>

<ParamField path="content" type="string" required>
  Content to write to the file
</ParamField>

### Configuration

```zig theme={null}
const wt = try allocator.create(file_write.FileWriteTool);
wt.* = .{
    .workspace_dir = "/path/to/workspace",
    .allowed_paths = &.{},
};
```

### Usage

```json theme={null}
{
  "tool": "file_write",
  "path": "output.txt",
  "content": "Hello, NullClaw!"
}
```

**Response:**

```
Written 16 bytes to output.txt
```

### Behavior

* **Creates parent directories** — Automatically creates missing parent dirs
* **Overwrites existing files** — Replaces content atomically
* **Preserves executable mode** — On Unix, preserves file mode (e.g., 0755)
* **Symlink-aware** — Writes to canonical target, preserving symlink
* **Hard link isolation** — Uses temp+rename to avoid mutating other inodes

### Atomic Write Strategy

To prevent partial writes and hard link side effects:

1. Create temp file `.nullclaw-write-{timestamp}-{attempt}.tmp`
2. Write content to temp file
3. Preserve file mode from existing file (if any)
4. Rename temp file to target (atomic inode swap)
5. Validate final path is within allowed areas
6. Clean up on failure

### Symlink Handling

**Existing symlink:**

```bash theme={null}
# workspace/link.txt -> target.txt
```

Write to `link.txt` → writes to `target.txt`, preserves symlink

**Symlink escape prevention:**

```bash theme={null}
# workspace/escape.txt -> /etc/passwd
```

Write to `escape.txt` → **rejected** (target outside workspace)

### Hard Link Safety

**Without hard link protection:**

```bash theme={null}
# /outside/file.txt and workspace/file.txt point to same inode
# Write to workspace/file.txt mutates /outside/file.txt ❌
```

**With temp+rename (NullClaw):**

```bash theme={null}
# Write creates new inode via temp+rename
# workspace/file.txt now points to new inode
# /outside/file.txt unchanged ✅
```

### Source

`src/tools/file_write.zig:11-222`

***

## file\_edit

Find and replace the first occurrence of text in a file.

### Parameters

<ParamField path="path" type="string" required>
  Relative path to the file within the workspace
</ParamField>

<ParamField path="old_text" type="string" required>
  Text to find in the file (must be non-empty)
</ParamField>

<ParamField path="new_text" type="string" required>
  Replacement text (can be empty to delete)
</ParamField>

### Configuration

```zig theme={null}
const et = try allocator.create(file_edit.FileEditTool);
et.* = .{
    .workspace_dir = "/path/to/workspace",
    .allowed_paths = &.{},
    .max_file_size = 10 * 1024 * 1024,
};
```

### Usage

```json theme={null}
{
  "tool": "file_edit",
  "path": "README.md",
  "old_text": "version: 0.1.0",
  "new_text": "version: 0.2.0"
}
```

**Response:**

```
Replaced 14 bytes with 14 bytes in README.md
```

### Behavior

* **First occurrence only** — Replaces only the first match
* **Exact match required** — Text must match exactly (case-sensitive)
* **Empty new\_text** — Deletes old\_text
* **Empty old\_text** — Rejected with "old\_text must not be empty"
* **No match** — Returns failure "old\_text not found in file"

### Example: Multiple replacements

**File: config.toml**

```toml theme={null}
port = 8080
host = "localhost"
port = 9090  # This port NOT replaced
```

**Edit:**

```json theme={null}
{
  "tool": "file_edit",
  "path": "config.toml",
  "old_text": "port = 8080",
  "new_text": "port = 3000"
}
```

**Result:**

```toml theme={null}
port = 3000
host = "localhost"
port = 9090  # Unchanged (second occurrence)
```

<Warning>
  For global find-replace, call `file_edit` multiple times or use `shell` with `sed`.
</Warning>

### Source

`src/tools/file_edit.zig:13-116`

***

## file\_append

Append content to the end of a file (creates file if missing).

### Parameters

<ParamField path="path" type="string" required>
  Relative path to the file within the workspace
</ParamField>

<ParamField path="content" type="string" required>
  Content to append to the file
</ParamField>

### Configuration

```zig theme={null}
const at = try allocator.create(file_append.FileAppendTool);
at.* = .{
    .workspace_dir = "/path/to/workspace",
    .allowed_paths = &.{},
    .max_file_size = 10 * 1024 * 1024,
};
```

### Usage

**Append to existing file:**

```json theme={null}
{
  "tool": "file_append",
  "path": "log.txt",
  "content": "2026-03-01 12:00:00 INFO: Task completed\n"
}
```

**Response:**

```
Appended 45 bytes to log.txt
```

**Create new file:**

```json theme={null}
{
  "tool": "file_append",
  "path": "notes.txt",
  "content": "First line\n"
}
```

**Response:**

```
Appended 11 bytes to notes.txt
```

### Behavior

* **Creates file if missing** — No error if file doesn't exist
* **Reads existing content** — Loads existing file first
* **Concatenates** — Appends new content to end
* **Writes atomically** — Truncate+write ensures consistency
* **Validates new files** — Ensures created files are within allowed areas

### Use Cases

**Log file:**

```json theme={null}
{
  "tool": "file_append",
  "path": "agent.log",
  "content": "[2026-03-01 12:00] Started task\n"
}
```

**Multi-append:**

```bash theme={null}
# Initial: "A"
file_append("file.txt", "B")  # -> "AB"
file_append("file.txt", "C")  # -> "ABC"
```

<Warning>
  Concurrent appends are not atomic. Use locking or sequential writes for multi-agent scenarios.
</Warning>

### Source

`src/tools/file_append.zig:18-124`

***

## Path Security

### workspace\_only Mode

By default, all file tools are restricted to `workspace_dir`:

```zig theme={null}
ft.* = .{ .workspace_dir = "/workspace", .allowed_paths = &.{} };
```

**Allowed:**

* `src/main.zig` → `/workspace/src/main.zig`
* `data/input.json` → `/workspace/data/input.json`
* `./file.txt` → `/workspace/file.txt`

**Blocked:**

* `/etc/passwd` → "Absolute paths not allowed"
* `../../../etc/passwd` → "Path not allowed: contains traversal"
* `src/../../../etc/passwd` → "Path is outside allowed areas" (resolved)

### Absolute Path Support

With `allowed_paths` configuration:

```zig theme={null}
ft.* = .{
    .workspace_dir = "/workspace",
    .allowed_paths = &.{
        "/data",
        "/home/user/configs",
    },
};
```

**Allowed:**

* `src/main.zig` → `/workspace/src/main.zig` (workspace)
* `/data/input.json` → `/data/input.json` (allowed\_paths)
* `/home/user/configs/app.yaml` → allowed

**Blocked:**

* `/etc/passwd` → "Path is outside allowed areas"
* `/tmp/file.txt` → "Path is outside allowed areas"

### Path Resolution

All paths are resolved with `realpathAlloc()` to:

* Canonicalize paths (remove `.` and `..`)
* Follow symlinks to final destination
* Validate against workspace + allowed\_paths

**Example:**

```bash theme={null}
# workspace/link -> /etc/passwd
```

Read `link` → resolves to `/etc/passwd` → **rejected**

### System Blocklist

The following paths are always blocked by `isResolvedPathAllowed()`:

* `/etc/*` (except on macOS where some system paths are safe)
* `/var/run/*`
* `/sys/*`
* `/proc/*`
* `/dev/*`

### Null Byte Injection

Paths containing null bytes are rejected:

```json theme={null}
{
  "tool": "file_read",
  "path": "file.txt\u0000/etc/passwd"
}
```

**Response:**

```
Path contains null bytes
```

***

## Testing

All file tools include comprehensive test coverage in `src/tools/file_*.zig`:

* **Basic operations** — read, write, edit, append
* **Path security** — traversal, symlinks, hard links
* **Error cases** — missing files, missing params
* **Absolute paths** — with/without allowed\_paths
* **Edge cases** — empty files, empty content, UTF-8 boundaries

Run tests:

```bash theme={null}
zig build test --summary all
```

Test count: **3,371 tests** across entire codebase
