# How Munder Difflin Handles PTY I/O for Agents: A Deep Dive into `PtyManager`

> Discover how Munder Difflin's PtyManager handles agent PTY I/O, managing lifecycles, routing I/O, and resolving platform-specific commands for efficient agent interaction.

- Repository: [Chaitanya Giri/munder-difflin](https://github.com/chaitanyagiri/munder-difflin)
- Tags: deep-dive
- Published: 2026-08-28

---

**Munder Difflin centralizes all pseudo-terminal (PTY) interactions in the `PtyManager` class, which manages agent lifecycle, routes I/O to specific renderer windows, and handles platform-specific command resolution including Windows shim decoding.**

The open-source **Munder Difflin** project implements a sophisticated PTY I/O system for agent processes. This article examines how the codebase handles spawning, managing, and communicating with terminal-based agents through its `PtyManager` implementation in [`src/main/pty.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/pty.ts).

## The `PtyManager` Architecture

All PTY I/O for agents flows through the **`PtyManager`** class. The manager maintains a `Map<string, Session>` called `this.sessions` where each session tracks:

- The underlying `node-pty.IPty` process
- Working directory (`cwd`) and resolved command
- The owning **Electron `WebContents`** (the window that spawned the terminal)
- Bookkeeping: last output timestamp, output-received flag, and exit handler

This centralized design ensures consistent PTY lifecycle management across the application.

## Spawning an Agent PTY

When starting an agent, `PtyManager.spawn(options: SpawnOptions, owner: WebContents)` executes a precise sequence:

### 1. Directory Expansion and Validation

The method expands tildes in the working directory path and confirms the directory exists before proceeding.

### 2. Command Resolution with Caching

The manager resolves commands against the user's PATH using `resolveCommand` and `resolveCommandUncached`. Successful lookups are cached to avoid repeated filesystem operations.

### 3. Windows Shim Decoding

On Windows, `resolveWindowsShimSpawn` attempts to decode npm-style `.cmd` shims. This extracted logic allows direct launching of the real interpreter (Node.js, Bun, Deno, etc.) rather than routing through `cmd.exe`, which preserves multi-line arguments and avoids parsing quirks.

### 4. Environment Construction

The `buildPtyEnv` helper prepares the PTY environment by stripping Claude-specific markers and merging the user's shell PATH with optional Hive runtime fallbacks.

### 5. PTY Creation via `node-pty`

Finally, the manager calls `node-pty.spawn` with the resolved executable, arguments, columns, rows, and constructed environment.

```typescript
// Main process – start a new agent PTY
import { PtyManager } from './pty';

const ptyMgr = new PtyManager();
ptyMgr.attachWebContents(mainWindow.webContents);

ptyMgr.spawn(
  {
    id: 'agent-42',
    cwd: '/home/user/project',
    command: 'claude',
    args: ['--verbose'],
    env: { HIVE_ROOT: '/path/to/hive' }
  },
  mainWindow.webContents
);

```

## PTY I/O Routing and Event Handling

After spawning, `PtyManager` registers two critical callbacks on each PTY instance:

### `onData` — Output Streaming

Raw terminal bytes are forwarded to the owning renderer window via the channel `pty:data:<id>`. The `safeSend` helper prevents crashes by checking if the target window has been destroyed.

### `onExit` — Cleanup and Notification

When a PTY exits, the manager:
- Notifies the renderer on channel `pty:exit:<id>` with exit code and signal
- Removes the session from the active map
- Executes the user-provided `exitHandler` for custom cleanup (archiving, work-tree removal, etc.)

```typescript
// Renderer process – listen for PTY output
const { ipcRenderer } = require('electron');

ipcRenderer.on('pty:data:agent-42', (_event, chunk) => {
  terminal.write(chunk); // feed to xterm.js or similar
});

ipcRenderer.on('pty:exit:agent-42', (_event, { exitCode, signal }) => {
  console.log('Agent exited', exitCode, signal);
});

```

## Window Ownership and Multi-Session Isolation

Each PTY session stores its **`owner`** — the specific `WebContents` that created it. This design enables **floor isolation**: terminals in different Electron windows receive data only on their own channels. If an owner window is destroyed, the manager falls back to a primary sink configured via `attachWebContents`.

## PTY Control Operations

The manager exposes thin wrappers around stored PTY objects for operational control:

| Method | Purpose | Location |
|--------|---------|----------|
| `write(id, data)` | Send input to a specific PTY | `src/main/pty.ts#L100-L108` |
| `resize(id, cols, rows)` | Adjust terminal dimensions | `src/main/pty.ts#L110-L118` |
| `redraw(id)` | Trigger no-op resize to force screen refresh | `src/main/pty.ts#L122-L130` |
| `kill(id)` | Terminate specific PTY with process tree cleanup | `src/main/pty.ts#L134-L144` |
| `killAll()` | Graceful shutdown of all PTYs (app quit) | `src/main/pty.ts#L172-L184` |
| `list()` / `lastOutputAt(id)` / `idleFor(id)` | Introspection for UI state | `src/main/pty.ts#L150-L169` |

```typescript
// Main process – send input to the PTY
ptyMgr.write('agent-42', 'ls -la\n');

// Clean up when a window closes
window.on('closed', () => {
  ptyMgr.killByOwner(window.webContents);
});

```

## Supporting Infrastructure

Additional modules complete the PTY I/O system:

| File | Responsibility |
|------|--------------|
| [`src/main/ptyEnv.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/ptyEnv.ts) | Environment construction, PATH merging, Claude marker stripping |
| [`src/main/procKill.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/procKill.ts) | Safe process termination (`ensureKilled`, `hardKillTree`) |
| [`src/main/shellEnv.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/shellEnv.ts) | User login-shell PATH capture (`userShellPath`, `captureFromLoginShell`) |

## Key Design Decisions in Munder Difflin's PTY I/O

The implementation prioritizes four qualities:

1. **Reliability** — `safeSend` guards against destroyed window references
2. **Performance** — Command resolution is cached; `node-pty` provides native PTY performance
3. **Portability** — Windows shim handling ensures consistent behavior across platforms
4. **Isolation** — Per-window ownership prevents cross-contamination between agent sessions

## Summary

- **Munder Difflin** centralizes PTY I/O in [`src/main/pty.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/pty.ts)'s `PtyManager` class
- The `this.sessions` Map tracks all active agent terminals with full metadata
- `spawn()` resolves commands safely, handles Windows quirks, and constructs controlled environments
- Output routes to specific renderer windows via `pty:data:<id>` channels protected by `safeSend`
- Full lifecycle control: write, resize, kill, and introspection methods wrap `node-pty` functionality
- Supporting files handle environment setup ([`ptyEnv.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/ptyEnv.ts)), process termination ([`procKill.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/procKill.ts)), and shell PATH resolution ([`shellEnv.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/shellEnv.ts))

## Frequently Asked Questions

### What is `PtyManager` in Munder Difflin?

`PtyManager` is the core class in [`src/main/pty.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/pty.ts) that coordinates all pseudo-terminal operations for agents. It maintains active sessions, spawns PTY processes, routes I/O to renderer windows, and provides lifecycle management methods.

### How does Munder Difflin handle Windows command spawning differently?

The codebase includes `resolveWindowsShimSpawn` to decode npm `.cmd` shims and launch interpreters directly. This bypasses `cmd.exe` and prevents argument-parsing issues that would otherwise corrupt multi-line commands.

### What happens when a renderer window closes while a PTY is active?

The `safeSend` helper detects destroyed `WebContents` and prevents crashes. Additionally, `killByOwner` allows cleanup of all PTYs associated with a closing window, or data falls back to a primary sink if configured.

### Which environment variables does Munder Difflin strip from PTY sessions?

The `buildPtyEnv` function specifically removes Claude-specific markers before constructing the final environment, then merges the user's shell PATH with optional Hive runtime paths.