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

# File System Operations

> Implement secure file reading and writing for agents

## Overview

File system capabilities allow agents to read and write files in the client's environment. These operations require careful security considerations to prevent unauthorized access.

## Advertising Capabilities

Declare file system support during initialization:

```typescript theme={null}
await connection.initialize({
  protocolVersion: acp.PROTOCOL_VERSION,
  clientCapabilities: {
    fs: {
      readTextFile: true,   // Enable file reading
      writeTextFile: true,  // Enable file writing
    },
  },
});
```

<Warning>
  Only advertise capabilities you actually implement. The agent will call these methods if you declare support.
</Warning>

## readTextFile()

Reads content from a text file in the client's file system.

### Method Signature

```typescript theme={null}
async readTextFile(
  params: ReadTextFileRequest
): Promise<ReadTextFileResponse>
```

<ParamField path="params.sessionId" type="string">
  The session making the request.
</ParamField>

<ParamField path="params.uri" type="string">
  Absolute file path to read.
</ParamField>

<ResponseField name="content" type="string" required>
  The file contents as a UTF-8 string.
</ResponseField>

### Basic Implementation

```typescript theme={null}
import { readFile } from "node:fs/promises";
import * as acp from "@agentclientprotocol/acp";

class MyClient implements acp.Client {
  async readTextFile(
    params: acp.ReadTextFileRequest
  ): Promise<acp.ReadTextFileResponse> {
    try {
      const content = await readFile(params.uri, "utf-8");
      return { content };
    } catch (error) {
      if (error.code === "ENOENT") {
        throw new acp.RequestError.resourceNotFound(params.uri);
      }
      throw new Error(`Failed to read file: ${error.message}`);
    }
  }
}
```

### Secure Implementation

Add path validation and sandboxing:

```typescript theme={null}
import { readFile } from "node:fs/promises";
import { resolve, relative } from "node:path";
import * as acp from "@agentclientprotocol/acp";

class SecureClient implements acp.Client {
  constructor(private allowedRoot: string) {}
  
  private isPathAllowed(path: string): boolean {
    const normalizedPath = resolve(path);
    const normalizedRoot = resolve(this.allowedRoot);
    const rel = relative(normalizedRoot, normalizedPath);
    
    // Path must be within root and not use ..
    return !rel.startsWith("..") && !path.isAbsolute(rel);
  }
  
  async readTextFile(
    params: acp.ReadTextFileRequest
  ): Promise<acp.ReadTextFileResponse> {
    // Validate path
    if (!this.isPathAllowed(params.uri)) {
      throw new Error(`Access denied: ${params.uri}`);
    }
    
    // Check file size before reading
    const stats = await stat(params.uri);
    const MAX_SIZE = 10 * 1024 * 1024; // 10MB
    if (stats.size > MAX_SIZE) {
      throw new Error(`File too large: ${params.uri}`);
    }
    
    try {
      const content = await readFile(params.uri, "utf-8");
      return { content };
    } catch (error) {
      if (error.code === "ENOENT") {
        throw acp.RequestError.resourceNotFound(params.uri);
      }
      throw new Error(`Failed to read file: ${error.message}`);
    }
  }
}
```

## writeTextFile()

Writes content to a text file in the client's file system.

### Method Signature

```typescript theme={null}
async writeTextFile(
  params: WriteTextFileRequest
): Promise<WriteTextFileResponse>
```

<ParamField path="params.sessionId" type="string">
  The session making the request.
</ParamField>

<ParamField path="params.uri" type="string">
  Absolute file path to write.
</ParamField>

<ParamField path="params.content" type="string">
  The content to write (UTF-8).
</ParamField>

<ResponseField name="(empty)" type="object">
  Returns an empty object `{}` on success.
</ResponseField>

### Basic Implementation

```typescript theme={null}
import { writeFile, mkdir } from "node:fs/promises";
import { dirname } from "node:path";
import * as acp from "@agentclientprotocol/acp";

class MyClient implements acp.Client {
  async writeTextFile(
    params: acp.WriteTextFileRequest
  ): Promise<acp.WriteTextFileResponse> {
    try {
      // Ensure directory exists
      await mkdir(dirname(params.uri), { recursive: true });
      
      // Write file
      await writeFile(params.uri, params.content, "utf-8");
      
      return {};
    } catch (error) {
      throw new Error(`Failed to write file: ${error.message}`);
    }
  }
}
```

### Secure Implementation

Add validation, backups, and atomic writes:

```typescript theme={null}
import { writeFile, mkdir, copyFile, stat } from "node:fs/promises";
import { dirname, join } from "node:path";
import * as acp from "@agentclientprotocol/acp";

class SecureClient implements acp.Client {
  constructor(
    private allowedRoot: string,
    private backupDir: string
  ) {}
  
  async writeTextFile(
    params: acp.WriteTextFileRequest
  ): Promise<acp.WriteTextFileResponse> {
    // Validate path
    if (!this.isPathAllowed(params.uri)) {
      throw new Error(`Access denied: ${params.uri}`);
    }
    
    // Validate content size
    const MAX_SIZE = 10 * 1024 * 1024; // 10MB
    if (params.content.length > MAX_SIZE) {
      throw new Error("Content too large");
    }
    
    try {
      // Create backup if file exists
      try {
        await stat(params.uri);
        const backupPath = join(
          this.backupDir,
          `${basename(params.uri)}.${Date.now()}.bak`
        );
        await copyFile(params.uri, backupPath);
      } catch (error) {
        // File doesn't exist, no backup needed
      }
      
      // Ensure directory exists
      await mkdir(dirname(params.uri), { recursive: true });
      
      // Atomic write: write to temp file, then rename
      const tempPath = `${params.uri}.tmp`;
      await writeFile(tempPath, params.content, "utf-8");
      await rename(tempPath, params.uri);
      
      return {};
    } catch (error) {
      throw new Error(`Failed to write file: ${error.message}`);
    }
  }
}
```

## Security Considerations

### Path Sandboxing

Restrict file access to specific directories:

```typescript theme={null}
class SandboxedClient {
  private allowedPaths: string[];
  
  constructor(allowedPaths: string[]) {
    this.allowedPaths = allowedPaths.map(p => resolve(p));
  }
  
  private isPathAllowed(path: string): boolean {
    const normalizedPath = resolve(path);
    
    return this.allowedPaths.some(allowedPath => {
      const rel = relative(allowedPath, normalizedPath);
      return !rel.startsWith("..") && !path.isAbsolute(rel);
    });
  }
}
```

### File Type Restrictions

Restrict which file types can be accessed:

```typescript theme={null}
const ALLOWED_EXTENSIONS = new Set([
  ".ts", ".js", ".tsx", ".jsx",
  ".json", ".md", ".txt",
  ".css", ".scss", ".html",
]);

function isFileTypeAllowed(path: string): boolean {
  const ext = extname(path).toLowerCase();
  return ALLOWED_EXTENSIONS.has(ext);
}

const BLOCKED_PATTERNS = [
  /\.env/,
  /\.secret/,
  /credentials/i,
  /password/i,
  /private.*key/i,
];

function isSensitiveFile(path: string): boolean {
  const filename = basename(path);
  return BLOCKED_PATTERNS.some(pattern => pattern.test(filename));
}
```

### Size Limits

Enforce file size limits:

```typescript theme={null}
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

async function checkFileSize(path: string) {
  const stats = await stat(path);
  if (stats.size > MAX_FILE_SIZE) {
    throw new Error(`File too large: ${path} (${stats.size} bytes)`);
  }
}
```

### Binary File Detection

Prevent reading binary files as text:

```typescript theme={null}
import { readFile } from "node:fs/promises";

function isBinary(buffer: Buffer): boolean {
  // Check first 8KB for null bytes
  const sample = buffer.slice(0, 8192);
  return sample.includes(0);
}

async function readTextFileSafe(path: string): Promise<string> {
  const buffer = await readFile(path);
  
  if (isBinary(buffer)) {
    throw new Error(`File is binary: ${path}`);
  }
  
  return buffer.toString("utf-8");
}
```

## Error Handling

### Standard Errors

Use standard error codes:

```typescript theme={null}
import * as acp from "@agentclientprotocol/acp";

async function readTextFile(params: acp.ReadTextFileRequest) {
  try {
    const content = await readFile(params.uri, "utf-8");
    return { content };
  } catch (error) {
    // File not found
    if (error.code === "ENOENT") {
      throw acp.RequestError.resourceNotFound(params.uri);
    }
    
    // Permission denied
    if (error.code === "EACCES") {
      throw new acp.RequestError(
        -32000,
        "Permission denied",
        { uri: params.uri }
      );
    }
    
    // Other errors
    throw acp.RequestError.internalError({ message: error.message });
  }
}
```

## User Notifications

Notify users about file operations:

```typescript theme={null}
class NotifyingClient implements acp.Client {
  async writeTextFile(
    params: acp.WriteTextFileRequest
  ): Promise<acp.WriteTextFileResponse> {
    // Show notification
    this.showNotification({
      type: "info",
      message: `Writing file: ${basename(params.uri)}`,
    });
    
    try {
      await writeFile(params.uri, params.content, "utf-8");
      
      // Show success
      this.showNotification({
        type: "success",
        message: `Saved ${basename(params.uri)}`,
      });
      
      return {};
    } catch (error) {
      // Show error
      this.showNotification({
        type: "error",
        message: `Failed to save ${basename(params.uri)}: ${error.message}`,
      });
      throw error;
    }
  }
}
```

## Version Control Integration

For editors with git integration:

```typescript theme={null}
import { exec } from "node:child_process";
import { promisify } from "node:util";

const execAsync = promisify(exec);

class GitAwareClient implements acp.Client {
  async writeTextFile(
    params: acp.WriteTextFileRequest
  ): Promise<acp.WriteTextFileResponse> {
    // Write file
    await writeFile(params.uri, params.content, "utf-8");
    
    // Auto-stage if in git repo
    try {
      await execAsync(`git add "${params.uri}"`);
    } catch {
      // Not in a git repo or git not available
    }
    
    return {};
  }
}
```

## Testing

```typescript theme={null}
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

describe("File System Operations", () => {
  let tempDir: string;
  let client: MyClient;
  
  beforeEach(async () => {
    tempDir = await mkdtemp(join(tmpdir(), "acp-test-"));
    client = new MyClient(tempDir);
  });
  
  afterEach(async () => {
    await rm(tempDir, { recursive: true });
  });
  
  it("should write and read text file", async () => {
    const path = join(tempDir, "test.txt");
    const content = "Hello, world!";
    
    // Write
    await client.writeTextFile({
      sessionId: "test",
      uri: path,
      content,
    });
    
    // Read
    const result = await client.readTextFile({
      sessionId: "test",
      uri: path,
    });
    
    expect(result.content).toBe(content);
  });
  
  it("should reject paths outside sandbox", async () => {
    await expect(
      client.readTextFile({
        sessionId: "test",
        uri: "/etc/passwd",
      })
    ).rejects.toThrow("Access denied");
  });
  
  it("should handle missing files", async () => {
    await expect(
      client.readTextFile({
        sessionId: "test",
        uri: join(tempDir, "missing.txt"),
      })
    ).rejects.toThrow();
  });
});
```

## See Also

* [Implementing the Client Interface](/clients/implementing-client-interface)
* [Permission Requests](/clients/permission-requests)
* [Terminal Operations](/clients/terminals)
* [Protocol: Client Capabilities](https://agentclientprotocol.com/protocol/overview#client)
