> ## 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.

# Terminal Operations

> Implement terminal execution for agents to run commands

## Overview

Terminal capabilities allow agents to execute shell commands in the client's environment. Terminals are created, monitored, and eventually released through a series of client methods.

## Advertising Terminal Support

Declare terminal support during initialization:

```typescript theme={null}
await connection.initialize({
  protocolVersion: acp.PROTOCOL_VERSION,
  clientCapabilities: {
    terminal: true,  // Enable terminal operations
  },
});
```

<Warning>
  Terminal execution poses significant security risks. Implement careful validation and sandboxing.
</Warning>

## Terminal Lifecycle

1. **Create**: Agent calls `createTerminal` to start a command
2. **Monitor**: Agent calls `terminalOutput` to get current output
3. **Wait**: Agent calls `waitForTerminalExit` to wait for completion
4. **Kill** (optional): Agent calls `killTerminal` to terminate the command
5. **Release**: Agent calls `releaseTerminal` to free resources

```typescript theme={null}
Client                                Agent
  |                                     |
  |<-- createTerminal request ----------|
  |-- terminalId response ------------->|
  |                                     |
  |<-- terminalOutput request ----------|
  |-- output response ----------------->|
  |                                     |
  |<-- waitForTerminalExit request -----|
  |-- exitStatus response ------------->|
  |                                     |
  |<-- releaseTerminal request ---------|
  |-- (empty) response ---------------->|
```

## createTerminal()

Creates a new terminal to execute a command.

### Method Signature

```typescript theme={null}
async createTerminal(
  params: CreateTerminalRequest
): Promise<CreateTerminalResponse>
```

<ParamField path="params.sessionId" type="string">
  The session creating the terminal.
</ParamField>

<ParamField path="params.command" type="string">
  The command to execute (e.g., `"npm"`, `"git"`).
</ParamField>

<ParamField path="params.args" type="string[]">
  Command arguments (e.g., `["install", "lodash"]`).
</ParamField>

<ParamField path="params.cwd" type="string" optional>
  Working directory for the command (absolute path).
</ParamField>

<ParamField path="params.env" type="Record<string, string>" optional>
  Additional environment variables.
</ParamField>

<ResponseField name="terminalId" type="string" required>
  Unique identifier for this terminal.
</ResponseField>

### Basic Implementation

```typescript theme={null}
import { spawn, ChildProcess } from "node:child_process";
import * as acp from "@agentclientprotocol/acp";

class MyClient implements acp.Client {
  private terminals = new Map<string, {
    process: ChildProcess;
    output: string;
    exitStatus?: ExitStatus;
  }>();
  
  async createTerminal(
    params: acp.CreateTerminalRequest
  ): Promise<acp.CreateTerminalResponse> {
    const terminalId = `term-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    
    // Spawn process
    const proc = spawn(params.command, params.args, {
      cwd: params.cwd || process.cwd(),
      env: { ...process.env, ...params.env },
      shell: false,
    });
    
    // Capture output
    let output = "";
    
    proc.stdout?.on("data", (data) => {
      output += data.toString();
    });
    
    proc.stderr?.on("data", (data) => {
      output += data.toString();
    });
    
    // Store terminal state
    const terminal = { process: proc, output: "" };
    this.terminals.set(terminalId, terminal);
    
    // Update output reference
    const updateOutput = () => {
      terminal.output = output;
    };
    proc.stdout?.on("data", updateOutput);
    proc.stderr?.on("data", updateOutput);
    
    // Handle exit
    proc.on("exit", (code, signal) => {
      updateOutput();
      if (signal) {
        terminal.exitStatus = { type: "signal", signal };
      } else {
        terminal.exitStatus = { type: "exit_code", code: code ?? -1 };
      }
    });
    
    return { terminalId };
  }
}
```

## terminalOutput()

Gets the current output and exit status of a terminal.

### Method Signature

```typescript theme={null}
async terminalOutput(
  params: TerminalOutputRequest
): Promise<TerminalOutputResponse>
```

<ParamField path="params.sessionId" type="string">
  The session ID.
</ParamField>

<ParamField path="params.terminalId" type="string">
  The terminal ID.
</ParamField>

<ResponseField name="output" type="string" required>
  The terminal output (combined stdout/stderr).
</ResponseField>

<ResponseField name="exitStatus" type="ExitStatus" optional>
  Exit status if the command has completed:

  * `{ type: "exit_code", code: number }`
  * `{ type: "signal", signal: string }`
</ResponseField>

### Implementation

```typescript theme={null}
class MyClient implements acp.Client {
  async terminalOutput(
    params: acp.TerminalOutputRequest
  ): Promise<acp.TerminalOutputResponse> {
    const terminal = this.terminals.get(params.terminalId);
    
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    return {
      output: terminal.output,
      exitStatus: terminal.exitStatus,
    };
  }
}
```

## waitForTerminalExit()

Waits for a terminal command to exit and returns its exit status.

### Method Signature

```typescript theme={null}
async waitForTerminalExit(
  params: WaitForTerminalExitRequest
): Promise<WaitForTerminalExitResponse>
```

<ParamField path="params.sessionId" type="string">
  The session ID.
</ParamField>

<ParamField path="params.terminalId" type="string">
  The terminal ID.
</ParamField>

<ResponseField name="exitStatus" type="ExitStatus" required>
  The exit status:

  * `{ type: "exit_code", code: number }`
  * `{ type: "signal", signal: string }`
</ResponseField>

### Implementation

```typescript theme={null}
class MyClient implements acp.Client {
  async waitForTerminalExit(
    params: acp.WaitForTerminalExitRequest
  ): Promise<acp.WaitForTerminalExitResponse> {
    const terminal = this.terminals.get(params.terminalId);
    
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    // If already exited, return immediately
    if (terminal.exitStatus) {
      return { exitStatus: terminal.exitStatus };
    }
    
    // Wait for exit
    return new Promise((resolve) => {
      terminal.process.on("exit", (code, signal) => {
        const exitStatus = signal
          ? { type: "signal" as const, signal }
          : { type: "exit_code" as const, code: code ?? -1 };
        
        terminal.exitStatus = exitStatus;
        resolve({ exitStatus });
      });
    });
  }
}
```

## killTerminal()

Kills a terminal command without releasing the terminal.

### Method Signature

```typescript theme={null}
async killTerminal(
  params: KillTerminalRequest
): Promise<KillTerminalResponse | void>
```

<ParamField path="params.sessionId" type="string">
  The session ID.
</ParamField>

<ParamField path="params.terminalId" type="string">
  The terminal ID.
</ParamField>

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

### Implementation

```typescript theme={null}
class MyClient implements acp.Client {
  async killTerminal(
    params: acp.KillTerminalRequest
  ): Promise<void> {
    const terminal = this.terminals.get(params.terminalId);
    
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    // Kill the process
    if (!terminal.exitStatus) {
      terminal.process.kill();
    }
  }
}
```

<Note>
  The terminal remains valid after killing, allowing you to get final output with `terminalOutput()`. Call `releaseTerminal()` when done.
</Note>

## releaseTerminal()

Releases a terminal and frees all associated resources.

### Method Signature

```typescript theme={null}
async releaseTerminal(
  params: ReleaseTerminalRequest
): Promise<ReleaseTerminalResponse | void>
```

<ParamField path="params.sessionId" type="string">
  The session ID.
</ParamField>

<ParamField path="params.terminalId" type="string">
  The terminal ID to release.
</ParamField>

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

### Implementation

```typescript theme={null}
class MyClient implements acp.Client {
  async releaseTerminal(
    params: acp.ReleaseTerminalRequest
  ): Promise<void> {
    const terminal = this.terminals.get(params.terminalId);
    
    if (!terminal) {
      // Already released, silently succeed
      return;
    }
    
    // Kill if still running
    if (!terminal.exitStatus) {
      terminal.process.kill();
    }
    
    // Remove from map
    this.terminals.delete(params.terminalId);
  }
}
```

<Warning>
  Always call `releaseTerminal()` when done to prevent resource leaks.
</Warning>

## Complete Implementation

Here's a complete, production-ready terminal implementation:

```typescript theme={null}
import { spawn, ChildProcess } from "node:child_process";
import { resolve } from "node:path";
import * as acp from "@agentclientprotocol/acp";

interface Terminal {
  process: ChildProcess;
  output: string;
  exitStatus?: acp.ExitStatus;
}

class ProductionClient implements acp.Client {
  private terminals = new Map<string, Terminal>();
  private allowedCwd: string;
  
  constructor(allowedCwd: string) {
    this.allowedCwd = resolve(allowedCwd);
  }
  
  async createTerminal(
    params: acp.CreateTerminalRequest
  ): Promise<acp.CreateTerminalResponse> {
    // Validate working directory
    const cwd = resolve(params.cwd || this.allowedCwd);
    if (!cwd.startsWith(this.allowedCwd)) {
      throw new Error("Access denied: working directory outside allowed path");
    }
    
    // Validate command
    this.validateCommand(params.command, params.args);
    
    // Generate terminal ID
    const terminalId = `term-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    
    // Spawn process
    const proc = spawn(params.command, params.args, {
      cwd,
      env: { ...process.env, ...params.env },
      shell: false,
    });
    
    // Setup output capture
    let output = "";
    const terminal: Terminal = { process: proc, output: "", exitStatus: undefined };
    
    const appendOutput = (data: Buffer) => {
      const text = data.toString();
      output += text;
      terminal.output = output;
    };
    
    proc.stdout?.on("data", appendOutput);
    proc.stderr?.on("data", appendOutput);
    
    // Handle exit
    proc.on("exit", (code, signal) => {
      if (signal) {
        terminal.exitStatus = { type: "signal", signal };
      } else {
        terminal.exitStatus = { type: "exit_code", code: code ?? -1 };
      }
    });
    
    // Handle errors
    proc.on("error", (error) => {
      output += `\nError: ${error.message}\n`;
      terminal.output = output;
    });
    
    // Store terminal
    this.terminals.set(terminalId, terminal);
    
    return { terminalId };
  }
  
  async terminalOutput(
    params: acp.TerminalOutputRequest
  ): Promise<acp.TerminalOutputResponse> {
    const terminal = this.terminals.get(params.terminalId);
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    return {
      output: terminal.output,
      exitStatus: terminal.exitStatus,
    };
  }
  
  async waitForTerminalExit(
    params: acp.WaitForTerminalExitRequest
  ): Promise<acp.WaitForTerminalExitResponse> {
    const terminal = this.terminals.get(params.terminalId);
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    if (terminal.exitStatus) {
      return { exitStatus: terminal.exitStatus };
    }
    
    return new Promise((resolve) => {
      terminal.process.on("exit", (code, signal) => {
        const exitStatus = signal
          ? { type: "signal" as const, signal }
          : { type: "exit_code" as const, code: code ?? -1 };
        resolve({ exitStatus });
      });
    });
  }
  
  async killTerminal(params: acp.KillTerminalRequest): Promise<void> {
    const terminal = this.terminals.get(params.terminalId);
    if (!terminal) return;
    
    if (!terminal.exitStatus) {
      terminal.process.kill("SIGTERM");
      
      // Force kill after timeout
      setTimeout(() => {
        if (!terminal.exitStatus) {
          terminal.process.kill("SIGKILL");
        }
      }, 5000);
    }
  }
  
  async releaseTerminal(params: acp.ReleaseTerminalRequest): Promise<void> {
    const terminal = this.terminals.get(params.terminalId);
    if (!terminal) return;
    
    if (!terminal.exitStatus) {
      terminal.process.kill();
    }
    
    this.terminals.delete(params.terminalId);
  }
  
  private validateCommand(command: string, args: string[]) {
    const BLOCKED_COMMANDS = ["rm -rf /", "mkfs", "dd if="];
    const fullCommand = `${command} ${args.join(" ")}`;
    
    for (const blocked of BLOCKED_COMMANDS) {
      if (fullCommand.includes(blocked)) {
        throw new Error(`Blocked dangerous command: ${blocked}`);
      }
    }
  }
}
```

## Security Considerations

### Command Validation

```typescript theme={null}
const ALLOWED_COMMANDS = new Set([
  "npm", "npx", "node",
  "git",
  "tsc", "eslint", "prettier",
]);

function validateCommand(command: string) {
  if (!ALLOWED_COMMANDS.has(command)) {
    throw new Error(`Command not allowed: ${command}`);
  }
}
```

### Argument Sanitization

```typescript theme={null}
function sanitizeArgs(args: string[]): string[] {
  return args.map(arg => {
    // Remove shell metacharacters
    if (/[;&|`$()]/.test(arg)) {
      throw new Error(`Unsafe argument: ${arg}`);
    }
    return arg;
  });
}
```

### Resource Limits

```typescript theme={null}
import { spawn } from "node:child_process";

function createTerminalWithLimits(command: string, args: string[]) {
  const proc = spawn(command, args, {
    shell: false,
    timeout: 5 * 60 * 1000, // 5 minute timeout
  });
  
  // Limit output size
  let outputSize = 0;
  const MAX_OUTPUT = 10 * 1024 * 1024; // 10MB
  
  proc.stdout?.on("data", (data: Buffer) => {
    outputSize += data.length;
    if (outputSize > MAX_OUTPUT) {
      proc.kill();
      throw new Error("Output size limit exceeded");
    }
  });
  
  return proc;
}
```

## See Also

* [Implementing the Client Interface](/clients/implementing-client-interface)
* [File System Operations](/clients/file-system-operations)
* [Permission Requests](/clients/permission-requests)
* [Protocol: Terminals](https://agentclientprotocol.com/protocol/terminals)
