ptyManager spawns and manages pseudo-terminals (PTYs) for CLI agents in Emdash. It handles three spawn modes, session isolation for multi-chat scenarios, environment variable passthrough, and agent command construction.
Overview
Emdash runs CLI agents in PTYs using the node-pty library. The PTY manager provides:- Three spawn modes: Shell-based, direct spawn, and SSH
- Session isolation: Multi-chat support via deterministic session UUIDs (Claude only)
- Environment control: Minimal clean environment with selective passthrough
- Provider detection: Automatic CLI detection and argument construction
- Shell respawn: Drops user into a shell after agent exits
Spawn Modes
startPty (Shell-based)
Spawn an agent via the user’s login shell. This is the default mode that loads shell configuration (.bashrc, .zshrc, etc.).
PTY ID in format
{providerId}-main-{taskId} or {providerId}-chat-{conversationId}Working directory (defaults to
process.cwd() or user home)Shell binary (defaults to
$SHELL or platform default)Additional environment variables (merged with defaults)
Terminal columns (default: 80)
Terminal rows (default: 24)
Enable auto-approve mode (passes
autoApproveFlag to CLI)Initial prompt to send to agent on startup
Skip resume flag (for new sessions)
Shell commands to run before spawning agent (e.g.,
cd /tmp && source venv/bin/activate)Wrap session in tmux for persistence (Linux/macOS only)
claude), the manager builds:
exec {shell} -il drops the user into a clean interactive shell.
Shell flag combinations:
- zsh:
-lic(login + interactive + command) - bash:
-lic - fish:
-l -i -c - sh:
-lc(POSIX sh doesn’t support-iwith-c)
startDirectPty (Direct spawn)
Spawn an agent directly without a shell wrapper. This is ~2-3x faster because it skips shell config loading (oh-my-zsh, nvm, rbenv, etc.).Provider ID (e.g.,
claude, codex, qwen)Whether to resume an existing session
null if:
- CLI path is not cached in
providerStatusCache - Custom CLI requires shell parsing (contains pipes, aliases, etc.)
- Tmux is enabled (requires shell wrapper)
- Looks up CLI path from
providerStatusCache.get(providerId).path - Builds CLI arguments (same logic as shell-based mode)
- Spawns process directly:
pty.spawn(cliPath, cliArgs, { ... }) - On agent exit, spawns a shell via
onDirectCliExitCallback(id, cwd)
startSshPty (SSH sessions)
Spawn an interactive SSH session in a PTY.SSH target (alias from
~/.ssh/config or user@host)Additional SSH arguments (e.g.,
['-p', '2222', '-i', '/path/to/key'])Command to execute on remote host (runs via remote shell)
SSH_AUTH_SOCK for agent forwarding and all AGENT_ENV_VARS for API keys.
Example:
Session Isolation
applySessionIsolation
Apply session isolation for Claude multi-chat scenarios. Generates deterministic UUIDs for--session-id and --resume flags.
CLI arguments array (modified in-place)
Provider definition (must have
sessionIdFlag)PTY ID
Working directory (used for session validation)
Whether this is a resume operation
true if session isolation was applied, false otherwise.
Decision tree:
- Known session in map →
--resume <uuid> - Additional chat (new) →
--session-id <uuid>(create) - Multi-chat transition →
--session-id <discovered-uuid>(adopt existing) - First-time main chat →
--session-id <uuid>(create, proactive) - Existing single-chat resume → (no isolation, caller uses generic
-c -r)
{userData}/pty-session-map.json:
task_abc or conv_123).
Session discovery:
For the main chat transitioning to multi-chat mode, the service scans ~/.claude/projects/{encoded-path}/ for existing session files and adopts the most recently modified one.
Session validation (Claude only):
Before resuming, validates:
- Session file exists:
~/.claude/projects/{encoded-path}/{uuid}.jsonl - Working directory matches (sessions are cwd-scoped)
PTY ID Format
Format
- providerId:
claude,codex,qwen, etc. - kind:
main(main conversation) orchat(additional conversation) - suffix:
{taskId}for main,{conversationId}for chat
Parsing
Defined insrc/shared/ptyId.ts:
Environment Variables
Minimal Environment
Why minimal? When Emdash runs as an AppImage (or other packaged Electron apps),process.env contains packaging artifacts (PYTHONHOME, APPDIR, APPIMAGE) that break user tools (especially Python virtual environments).
Solution: Build a clean environment and let login shells rebuild from user config (.bashrc, .zshrc, etc.).
Base environment:
Agent Environment Variables
Passthrough list (AGENT_ENV_VARS):
- Add to
AGENT_ENV_VARSinptyManager.ts - Add provider to
src/shared/providers/registry.ts
Display Environment Variables
For GUI operations (browser launching, clipboard, etc.) from within PTY sessions:Agent Event Hooks
For CLI hooks to call back to Emdash:Provider Command Configuration
resolveProviderCommandConfig
Resolve CLI command and flags for a provider, merging default config with user overrides.Provider metadata from registry
CLI binary name or path
Resume flag (e.g.,
-c -r for Claude)Default arguments to always pass
Auto-approve flag (e.g.,
--dangerously-skip-permissions)Initial prompt flag (e.g.,
-i, -t, or empty string for positional)User-provided extra arguments from settings
Provider-specific environment variables
buildProviderCliArgs
Build CLI arguments array from provider config and runtime options.- Resume flag (if
resumeandresumeFlagprovided) - Default args
- Auto-approve flag (if
autoApproveandautoApproveFlagprovided) - Initial prompt flag + prompt (if not using keystroke injection)
- Extra args
Tmux Integration
Wrap PTY sessions in tmux for persistence (Linux/macOS only).getTmuxSessionName
Generate a deterministic tmux session name from PTY ID.killTmuxSession
Kill a tmux session by PTY ID (fire-and-forget, ignores errors).Tmux spawn
Whentmux: true is passed to startPty:
-As flag creates or attaches to an existing session.
PTY Management
writePty
Write data to a PTY (send keystrokes to agent).resizePty
Resize a PTY (update terminal dimensions).killPty
Kill a PTY process and remove its record.hasPty
Check if a PTY exists.getPty
Get the rawIPty instance.
getPtyKind
Get PTY kind (local or ssh).
getPtyTmuxSessionName
Get tmux session name if PTY is wrapped in tmux.Utility Functions
parseShellArgs
Parse a shell-style argument string into an array.- Single quotes (literal, no escapes)
- Double quotes (allows
\"escapes) - Backslash escapes (POSIX-style, except inside single quotes)
- Windows path preservation (e.g.,
C:\Program Fileswith backslashes)
setOnDirectCliExit
Register a callback for direct CLI exit (to spawn shell afterward).ptyIpc.ts to spawn a shell when startDirectPty agent exits.
Location
Source:src/main/services/ptyManager.ts
Singleton exports:
Related Services
- ProviderRegistry (
src/shared/providers/registry.ts): Defines all 21 CLI agents - AgentEventService (
src/main/services/AgentEventService.ts): Agent callback hooks - ProviderStatusCache (
src/main/services/providerStatusCache.ts): CLI path cache
See Also
- Provider Registry - All 21 CLI agent definitions
- Agent Event Service - CLI hook system
- Task Lifecycle Service - PTY orchestration