Cross-platform and multi-environment considerations for the Maestro codebase. For the main guide, see [[CLAUDE.md]].
| Feature | macOS | Windows | Linux | SSH Remote |
|---|---|---|---|---|
| Claude Code | Full | Full | Full | Full |
| OpenAI Codex | Full | Full | Full | Full |
| OpenCode | Full | Full | Full | Full |
| Factory Droid | Full | Full | Full | Full |
| File watching (chokidar) | Yes | Yes | Yes | No |
| Git worktrees | Yes | Yes | Yes | Yes |
| PTY terminal | Yes | Yes | Yes | N/A |
Path separators differ:
// WRONG - hardcoded separator
const fullPath = folder + '/' + filename;
// CORRECT - use path.join or path.posix for SSH
import * as path from 'path';
const fullPath = path.join(folder, filename); // Local
const remotePath = path.posix.join(folder, filename); // SSH remotePath delimiters differ:
// Windows uses ';', Unix uses ':'
const delimiter = path.delimiter; // Use this, don't hardcodeTilde expansion:
// Node.js fs does NOT expand ~ automatically
import { expandTilde } from '../shared/pathUtils';
const expanded = expandTilde('~/.config/file'); // Always use thisMinimum path lengths:
// Validation must account for platform differences
const minPathLength = process.platform === 'win32' ? 4 : 5; // C:\a vs /a/bWindows reserved names:
// CON, PRN, AUX, NUL, COM1-9, LPT1-9 are invalid on Windows
const reservedNames = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;Default shells differ:
// Windows: $SHELL doesn't exist; default to PowerShell
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : 'bash';Command lookup differs:
// 'which' on Unix, 'where' on Windows
const command = process.platform === 'win32' ? 'where' : 'which';Executable permissions:
// Unix requires X_OK check; Windows does not
if (process.platform !== 'win32') {
await fs.promises.access(filePath, fs.constants.X_OK);
}Windows shell execution:
// Some Windows commands need shell: true
const useShell = isWindows && needsWindowsShell(command);Two SSH identifiers with different lifecycles:
// sshRemoteId: Set AFTER AI agent spawns (via onSshRemote callback)
// sessionSshRemoteConfig.remoteId: Set BEFORE spawn (user configuration)
// WRONG - fails for terminal-only SSH agents
const sshId = session.sshRemoteId;
// CORRECT - works for all SSH agents
const sshId = session.sshRemoteId || session.sessionSshRemoteConfig?.remoteId;File watching not available for SSH:
// chokidar can't watch remote directories
if (sshRemoteId) {
// Use polling instead of file watching
}Prompts must go via stdin for SSH:
// IMPORTANT: ALL agent prompts are passed via stdin passthrough for SSH.
// This avoids shell escaping issues and command line length limits.
if (isSSH) {
// Pass prompt via stdin, not as command line argument
}Path resolution on remote:
// Don't resolve paths locally when operating on remote
// The remote may have different filesystem structure
if (isRemote) {
// Use path as-is, normalize slashes only
}Provider session ID terminology:
// Claude Code: session_id
// Codex: thread_id
// Different field names, same conceptStorage locations differ per platform:
// Claude Code: ~/.claude/projects/<encoded-path>/
// Codex: ~/.codex/sessions/YYYY/MM/DD/*.jsonl
// OpenCode:
// - macOS/Linux: ~/.config/opencode/storage/
// - Windows: %APPDATA%/opencode/storage/Resume flags differ:
// Claude Code: --resume <session-id>
// Codex: resume <thread_id> (subcommand, not flag)
// OpenCode: --session <session-id>Read-only mode flags differ:
// Claude Code: --permission-mode plan
// Codex: --sandbox read-only
// OpenCode: --agent planmacOS Alt key produces special characters:
// Alt+L = '¬', Alt+P = 'π', Alt+U = 'ü'
// Must use e.code for Alt key combos, not e.key
if (e.altKey && process.platform === 'darwin') {
const key = e.code.replace('Key', '').toLowerCase(); // 'KeyL' -> 'l'
}Windows command line length limit:
// cmd.exe has ~8KB command line limit
// Use sendPromptViaStdin to bypass this for long promptsGit stat format differs:
// GNU stat vs BSD stat have different format specifiers
// Use git commands that work cross-platformCase sensitivity:
// macOS with case-insensitive filesystem:
// Renaming "readme.md" to "README.md" may not trigger expected eventsWhen making changes that involve any of the above areas, verify:
- Works on macOS (primary development platform)
- Works on Windows (PowerShell default, path separators)
- Works on Linux (standard Unix behavior)
- Works with SSH remote agents (no file watching, stdin passthrough)
- Path handling uses
path.joinorpath.posixas appropriate - No hardcoded path separators (
/or\) - Shell commands use platform-appropriate lookup (
which/where) - Agent-specific code handles all supported agents, not just Claude
| Concern | Primary Files |
|---|---|
| Path utilities | src/shared/pathUtils.ts |
| Shell detection | src/main/utils/shellDetector.ts |
| WSL detection | src/main/utils/wslDetector.ts |
| CLI detection | src/main/utils/cliDetection.ts |
| SSH spawn wrapper | src/main/utils/ssh-spawn-wrapper.ts |
| SSH command builder | src/main/utils/ssh-command-builder.ts |
| Agent path probing | src/main/agents/path-prober.ts |
| Windows diagnostics | src/main/debug-package/collectors/windows-diagnostics.ts |
| Safe exec | src/main/utils/execFile.ts |