# FileSystemTools A comprehensive file manipulation toolkit providing read, write, and edit operations for working with files in the local filesystem. **Features:** - Read files with line range support and pagination - Write new files or overwrite existing ones - Precise string replacement editing - Line number formatting for easy reference - Long line truncation (2000 chars max) - Automatic parent directory creation - UTF-8 encoding support - Replace all or single occurrence editing - Optional `allowedDirectory` to restrict all file operations to one or more configured paths ## Builder Configuration ### allowedDirectory / allowedDirectories — Restrict File Operations to a Directory By default `FileSystemTools` can access any path on the filesystem. Configure one or more allowed directories to restrict all `read`, `write`, and `edit` operations to paths within them. Access to any path outside all allowed directories returns an error. **Usage:** ```java // Single directory (Path or String) FileSystemTools tools = FileSystemTools.builder() .allowedDirectory(Path.of("/workspace/project")) .build(); // Multiple directories — chained FileSystemTools tools = FileSystemTools.builder() .allowedDirectory("/workspace/project") .allowedDirectory("/tmp/uploads") .build(); // Multiple directories — varargs FileSystemTools tools = FileSystemTools.builder() .allowedDirectories(Path.of("/workspace/project"), Path.of("/tmp/uploads")) .build(); // No restriction (default — backward compatible) FileSystemTools tools = FileSystemTools.builder().build(); // Workspace shorthand — confines operations to the workspace root FileSystemTools tools = FileSystemTools.builder() .workspace(Workspace.local(Path.of("/workspace/project"))) .build(); ``` **Security guarantees:** Three layers block bypass attempts for each candidate directory: 1. **Path traversal** — raw `..` components are rejected before normalization, blocking `/allowed/../etc/passwd`-style attacks. 2. **Normalized containment** — the resolved path must start with the allowed directory (component-aware, so `/allowed-evil` does not match `/allowed`). 3. **Symlink resolution** — real paths of existing path components are verified, blocking symlinks that point outside the allowed directory. Dangling symlinks are always denied. **Error returned on denied access:** ``` Error: Access denied. Path is outside the allowed directories: /etc/passwd ``` **Notes:** - When no directory is configured, all paths are allowed (backward compatible). - `allowedDirectory(null)` is a no-op — it does not add an entry. - Non-existent allowed directories are supported; writes into them are permitted (symlink check is skipped until the directory exists). ## Available Tools ### 1. Read - Read File Contents Reads a file from the local filesystem with optional line range support for handling large files. **Parameters:** - `filePath` (required) - The absolute path to the file to read - `offset` (optional) - The line number to start reading from (1-indexed) - `limit` (optional) - The number of lines to read (default: 2000) **Basic Usage:** ```java FileSystemTools fileTools = FileSystemTools.builder().build(); // Read entire file (up to 2000 lines) String content = fileTools.read( "/path/to/file.txt", null, // offset (start from beginning) null // limit (read up to 2000 lines) ); // Read specific line range String content = fileTools.read( "/path/to/large-file.log", 100, // Start from line 100 50 // Read 50 lines ); // Read from line 500 onwards String content = fileTools.read( "/path/to/file.java", 500, // Start from line 500 2000 // Read up to 2000 lines ); ``` **Output Format:** ``` File: /path/to/file.txt Showing lines 1-10 of 150 1→First line of content 2→Second line of content 3→Third line of content ... 10→Tenth line of content ``` **Key Features:** - **Line numbers**: Results formatted with `cat -n` style line numbers (right-aligned, 6 chars, arrow separator) - **Line truncation**: Lines longer than 2000 characters are truncated with "... (line truncated)" suffix - **Pagination**: Read large files in chunks using offset and limit - **Empty file detection**: Returns "File is empty" message for empty files - **Error handling**: Clear error messages for non-existent files or directories **Important Notes:** - File path must be absolute, not relative - Default limit is 2000 lines - recommended to read full file when possible - Line numbers are 1-indexed (first line is line 1) - Cannot read directories (use Bash tool with `ls` command) - Supports reading various file types (text, images, PDFs, Jupyter notebooks with appropriate handling) ### 2. Write - Create or Overwrite Files Writes content to a file, creating new files or overwriting existing ones. **Parameters:** - `filePath` (required) - The absolute path to the file to write (must be absolute) - `content` (optional) - The content to write to the file (defaults to empty string if null) **Basic Usage:** ```java // Create a new file String result = fileTools.write( "/path/to/new-file.txt", "This is the file content\nWith multiple lines" ); // Returns: "Successfully created file: /path/to/new-file.txt (45 bytes)" // Create an empty file String result = fileTools.write( "/path/to/empty-file.txt", null // content (creates empty file) ); // Returns: "Successfully created file: /path/to/empty-file.txt (0 bytes)" // Overwrite an existing file String result = fileTools.write( "/path/to/existing-file.txt", "New content replacing old content" ); // Returns: "Successfully overwrote file: /path/to/existing-file.txt (33 bytes)" // Create file with parent directories String result = fileTools.write( "/path/to/new/directory/file.txt", "Content" ); // Automatically creates /path/to/new/directory/ if it doesn't exist ``` **Important Notes:** - **MUST read first**: If overwriting an existing file, you MUST use the Read tool first - **Prefer Edit**: ALWAYS prefer editing existing files instead of writing new ones - **No emojis**: Avoid writing emojis unless explicitly requested by the user - **No proactive docs**: Never create documentation files (*.md, README) unless explicitly requested - **Parent directories**: Automatically creates parent directories if they don't exist - **Complete replacement**: Overwrites the entire file content (does not append) **When to Use:** - Creating new files explicitly requested by the user - Generating configuration files, scripts, or source files - Writing output from data transformations **When NOT to Use:** - Modifying existing files (use Edit instead) - Making small changes to code (use Edit instead) ### 3. Edit - Precise String Replacement Performs exact string replacements in files with safety checks to prevent unintended changes. **Parameters:** - `filePath` (required) - The absolute path to the file to modify - `old_string` (required) - The exact text to replace - `new_string` (required) - The text to replace it with (must be different from old_string) - `replace_all` (optional) - Replace all occurrences (default: false) **Basic Usage:** ```java // Single replacement (default) String result = fileTools.edit( "/path/to/file.java", "public void oldMethod() {", // old_string "public void newMethod() {", // new_string null // replace_all (false) ); // Replace all occurrences (useful for variable renaming) String result = fileTools.edit( "/path/to/file.java", "oldVariableName", // old_string "newVariableName", // new_string true // replace_all ); // Multi-line replacement String result = fileTools.edit( "/path/to/config.yml", "database:\n host: localhost\n port: 5432", "database:\n host: prod-server\n port: 3306", null ); ``` **Output Format:** ``` The file /path/to/file.java has been updated. Here's the result of running `cat -n` on a snippet of the edited file: 15→ private static final String NAME = "newValue"; 16→ 17→ public void newMethod() { 18→ // method implementation 19→ } ``` **Safety Features:** 1. **Uniqueness Check**: If `old_string` appears multiple times and `replace_all=false`, the edit fails with an error: ``` Error: old_string appears 5 times in the file. Either provide a larger string with more surrounding context to make it unique or use replace_all=true to change all instances. ``` 2. **Existence Check**: Returns error if `old_string` is not found in the file 3. **Different Strings**: Validates that `old_string` and `new_string` are different 4. **Must Read First**: Should read the file first to see exact content and indentation **Indentation Handling:** When copying text from Read tool output, preserve exact indentation AFTER the line number prefix: ``` Read output: 42→ public void method() { Correct old_string: " public void method() {" Incorrect old_string (includes line number): "42→ public void method() {" ``` **Best Practices:** 1. **Read before editing**: Always use Read tool first to see exact content ```java String content = fileTools.read("/path/to/file.java", null, null); // Now edit based on what you see ``` 2. **Include context for uniqueness**: If replacement string appears multiple times, include surrounding lines ```java // Instead of just "foo" fileTools.edit(filePath, "bar\nfoo\nbaz", "bar\nnewValue\nbaz", null); ``` 3. **Use replace_all for renaming**: When renaming variables or constants throughout a file ```java fileTools.edit(filePath, "OLD_CONSTANT", "NEW_CONSTANT", true); ``` 4. **Preserve indentation**: Copy exact whitespace from Read output (spaces/tabs) 5. **Multi-line edits**: Use `\n` for newlines in both old_string and new_string **Example Workflow:** ```java FileSystemTools fileTools = FileSystemTools.builder().build(); // 1. Read the file to see current content String content = fileTools.read( "/src/main/java/Example.java", null, null ); // 2. Identify exact string to replace (preserving indentation) String oldCode = """ public void calculate() { return value * 2; }"""; String newCode = """ public void calculate() { return value * 3; }"""; // 3. Perform the edit String result = fileTools.edit( "/src/main/java/Example.java", oldCode, newCode, null ); // Output shows snippet with context around the edit ``` **Common Use Cases:** | Task | Configuration | Example | |------|--------------|---------| | Update single method | `replace_all=false` | Modify one method implementation | | Rename variable | `replace_all=true` | Change `oldName` to `newName` everywhere | | Update configuration | `replace_all=false` | Change one config value | | Fix typos | `replace_all=true` | Fix misspelled word throughout file | | Refactor imports | `replace_all=false` | Update specific import statement | **Error Messages:** | Error | Meaning | Solution | |-------|---------|----------| | "File does not exist" | File path is invalid | Check file path is correct | | "old_string not found" | Text doesn't exist in file | Verify exact string including whitespace | | "appears N times" | Multiple matches found | Add more context or use `replace_all=true` | | "must be different" | old_string equals new_string | Ensure you're actually changing the content | ## Confining the search tools too Since the search tools share this check, configure them together so the confinement is consistent: `FileSystemTools`, `GrepTool`, `GlobTool` and `ListDirectoryTool` all accept `allowedDirectory(...)` / `allowedDirectories(...)` with identical semantics (one shared implementation) — without it on the search tools, an explicit `path` argument can read outside the workspace even when Read/Write/Edit are confined.