Skip to main content

Overview

Maestro supports running AI agents on remote servers via SSH. This enables powerful workflows like running agents on GPU-equipped servers, accessing remote codebases, or distributing work across multiple machines.
CRITICAL: Any feature that spawns agent processes MUST support SSH remote execution. Without proper SSH wrapping, agents will always run locally even when configured for remote execution.

SSH Remote Configuration

Configuration Structure

interface SshRemoteConfig {
  id: string;                  // Unique identifier
  name: string;                // Display name
  host: string;                // Hostname or IP
  port: number;                // SSH port (default: 22)
  username?: string;           // SSH username (optional with config)
  privateKeyPath?: string;     // Path to private key (optional)
  passphrase?: string;         // Key passphrase (if needed)
  useSshConfig: boolean;       // Use ~/.ssh/config
  customOptions?: Record<string, string>; // Custom SSH options
}

Setting Up SSH Remote

1

Open Settings

Press Cmd+, and navigate to “SSH Remote” section.
2

Add SSH Configuration

Click “Add SSH Remote” and configure:
  • Name: Display name (e.g., “GPU Server”)
  • Host: Server hostname or IP
  • Port: SSH port (default 22)
  • Username: SSH username (optional if in ~/.ssh/config)
  • Private Key: Path to SSH key (optional)
3

Test Connection

Click “Test Connection” to verify:
  • SSH connection succeeds
  • Authentication works
  • Remote shell is accessible
4

Enable for Agent

In Agent Settings, enable SSH remote and select the configuration.

Default SSH Options

Maestro uses these default SSH options for all connections:
const defaultSshOptions = {
  BatchMode: 'yes',              // Disable password prompts (key-only)
  StrictHostKeyChecking: 'accept-new', // Auto-accept new host keys
  ConnectTimeout: '10',          // Connection timeout in seconds
  ClearAllForwardings: 'yes',    // Disable port forwarding
  RequestTTY: 'no',              // Don't request TTY for commands
};

Custom SSH Options

Add custom options in the SSH configuration:
customOptions: {
  'ServerAliveInterval': '60',
  'ServerAliveCountMax': '3',
  'Compression': 'yes'
}

Using ~/.ssh/config

Enable “Use SSH Config” to inherit settings from your ~/.ssh/config file.

Example SSH Config

# ~/.ssh/config
Host gpu-server
  HostName 192.168.1.100
  User developer
  Port 22
  IdentityFile ~/.ssh/id_ed25519
  ServerAliveInterval 60
  Compression yes

Maestro Configuration

With SSH config, you only need:
{
  id: 'gpu-1',
  name: 'GPU Server',
  host: 'gpu-server',  // Matches Host in ~/.ssh/config
  port: 22,
  useSshConfig: true,
  // username and privateKeyPath inherited from config
}

Agent-Level SSH Configuration

Per-Agent Settings

Each agent can have its own SSH configuration:
interface AgentSshRemoteConfig {
  enabled: boolean;              // Enable SSH for this agent
  remoteId: string | null;       // SSH config ID to use
  workingDirOverride?: string;   // Override working directory
}

Configuring in UI

1

Open Agent Settings

Select an agent and press Alt+Cmd+, or right-click > Settings.
2

Enable SSH Remote

In the “SSH Remote” section, toggle “Enable SSH Remote Execution”.
3

Select Configuration

Choose the SSH remote configuration to use.
4

Set Working Directory

Optionally override the working directory on the remote host:
/home/developer/projects/my-app

SSH Spawn Wrapper

All code that spawns agent processes must use wrapSpawnWithSsh() to support SSH remote execution.

Wrapper Configuration

interface SshSpawnWrapConfig {
  command: string;                    // Command to execute
  args: string[];                     // Command arguments
  cwd: string;                        // Working directory
  prompt?: string;                    // Prompt to send
  customEnvVars?: Record<string, string>; // Environment variables
  promptArgs?: (prompt: string) => string[]; // Prompt flag builder
  noPromptSeparator?: boolean;        // No -- separator before prompt
  agentBinaryName?: string;           // Agent binary name (e.g., 'claude')
}

Usage Example

import { wrapSpawnWithSsh } from './utils/ssh-spawn-wrapper';
import { createSshRemoteStoreAdapter } from './utils/ssh-remote-resolver';

// Prepare spawn config
const config = {
  command: '/usr/local/bin/claude',
  args: ['--print'],
  cwd: '/path/to/project',
  prompt: 'Hello, Claude!',
  agentBinaryName: 'claude'
};

// Wrap with SSH if enabled
const wrapped = await wrapSpawnWithSsh(
  config,
  session.sshRemoteConfig,  // From agent settings
  createSshRemoteStoreAdapter(settingsStore)
);

// Spawn with wrapped config
processManager.spawn({
  sessionId: session.id,
  toolType: session.agentType,
  command: wrapped.command,     // May be 'ssh' if SSH enabled
  args: wrapped.args,           // SSH args + remote command
  cwd: wrapped.cwd,             // Local home for SSH
  customEnvVars: wrapped.customEnvVars, // undefined for SSH
  prompt: wrapped.prompt        // undefined for small prompts
});

Wrapper Result

interface SshSpawnWrapResult {
  command: string;              // 'ssh' or original command
  args: string[];               // Full args including SSH flags
  cwd: string;                  // Local home or original
  customEnvVars?: Record<string, string>; // undefined for SSH
  prompt?: string;              // undefined if embedded in args
  sshRemoteUsed: SshRemoteConfig | null; // Config if SSH used
}

Prompt Handling

SSH remote execution handles prompts differently based on size to avoid command-line length limits.
Embedded in Command LineSmall prompts are embedded directly in the SSH command:
ssh user@host 'cd /project && claude --print -- "Your prompt here"'
Advantages:
  • Single command execution
  • No stdin complexity
  • Works with all agents

Context Grooming with SSH

Context grooming fully supports SSH remote execution, allowing you to groom context on the remote host.

Example

const result = await window.maestro.context.groomContext(
  '/path/to/project',
  'claude-code',
  'Summarize this conversation focusing on key decisions',
  {
    sshRemoteConfig: {
      enabled: true,
      remoteId: 'gpu-server',
      workingDirOverride: '/home/dev/project'
    },
    customPath: '/opt/claude/bin/claude',
    customEnvVars: {
      'ANTHROPIC_API_KEY': 'sk-...'
    }
  }
);

console.log('Groomed summary:', result);

SSH Command Building

Command Structure

ssh [SSH_OPTIONS] [USER@]HOST [COMMAND]

Built Command Example

ssh \
  -o BatchMode=yes \
  -o StrictHostKeyChecking=accept-new \
  -o ConnectTimeout=10 \
  -o ClearAllForwardings=yes \
  -o RequestTTY=no \
  -p 22 \
  -i ~/.ssh/id_ed25519 \
  user@gpu-server \
  'cd /home/dev/project && export VAR=value && claude --print -- "prompt"'

Environment Variables

Environment variables are set in the remote command:
customEnvVars: {
  'ANTHROPIC_API_KEY': 'sk-...',
  'NODE_ENV': 'production'
}

// Becomes:
'export ANTHROPIC_API_KEY="sk-..." NODE_ENV="production" && command'

Testing SSH Connections

Connection Test

interface SshRemoteTestResult {
  success: boolean;           // Overall success
  message: string;            // Status message
  details?: {                 // Connection details
    hostname: string;
    username: string;
    platform: string;
    agentAvailable?: boolean; // If agent command tested
  };
  error?: string;             // Error message if failed
}

Test via API

const result = await sshRemoteManager.testConnection(
  config,
  'claude'  // Optional: test if agent binary is available
);

if (result.success) {
  console.log('Connected to:', result.details.hostname);
  console.log('Platform:', result.details.platform);
  console.log('Agent available:', result.details.agentAvailable);
} else {
  console.error('Connection failed:', result.error);
}

Troubleshooting

Symptoms:
  • “Permission denied (publickey)”
  • Connection hangs then times out
Solutions:
  1. Verify SSH key is correct and readable
  2. Check key is added to remote ~/.ssh/authorized_keys
  3. Ensure BatchMode: yes doesn’t conflict with your setup
  4. Try using ~/.ssh/config with useSshConfig: true
Symptoms:
  • “claude: command not found” on remote
  • Agent binary not accessible
Solutions:
  1. Use full path in customPath (e.g., /usr/local/bin/claude)
  2. Set PATH in customEnvVars
  3. Check agent is installed on remote host
  4. Verify binary name matches remote (use agentBinaryName)
Symptoms:
  • “No such file or directory”
  • Agent can’t find project files
Solutions:
  1. Use absolute paths for cwd
  2. Set workingDirOverride in agent SSH config
  3. Verify directory exists on remote host
  4. Check permissions on remote directory
Symptoms:
  • Connections hang and timeout
  • “Connection timed out” errors
Solutions:
  1. Increase ConnectTimeout in custom options
  2. Add ServerAliveInterval to keep connection alive
  3. Check firewall rules
  4. Verify network connectivity

Security Best Practices

Follow these security practices when using SSH remote execution:

1. Use SSH Keys, Not Passwords

# Generate ED25519 key (recommended)
ssh-keygen -t ed25519 -C "maestro@myhost"

# Add to remote
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host

2. Protect Private Keys

# Set correct permissions
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

3. Use Key Passphrases

  • Always set a passphrase on private keys
  • Use ssh-agent to avoid repeated passphrase entry
  • Never store passphrases in Maestro config

4. Limit Key Scope

# In remote ~/.ssh/authorized_keys, restrict key usage:
command="/usr/local/bin/claude",no-port-forwarding ssh-ed25519 AAAA...

5. Monitor SSH Access

# On remote host, monitor SSH logs
tail -f /var/log/auth.log  # Linux
tail -f /var/log/system.log  # macOS

Next Steps

Build docs developers (and LLMs) love