Skip to main content
Emdash supports remote development via SSH, allowing you to run coding agents on remote machines while using the Emdash UI locally. This is useful for:
  • Working with large codebases that don’t fit on your local machine
  • Running resource-intensive agents on powerful remote servers
  • Accessing development environments in the cloud
  • Collaborating on shared infrastructure
Remote development uses the same parallel worktree workflow as local projects. Each remote task gets its own isolated worktree on the server.

Architecture

Emdash’s remote development stack:
  • Connection: ssh2 library (Node.js SSH2 client)
  • Authentication: Password, private key, or SSH agent
  • Credential storage: Keytar (OS keychain integration)
  • File operations: SFTP over SSH
  • Remote shells: Interactive PTY via ssh2’s shell API
  • Git operations: Remote execution via RemoteGitService

Key services

ServicePurposeLocation
SshServiceConnection management, command executionsrc/main/services/ssh/SshService.ts
SshCredentialServiceSecure password/passphrase storagesrc/main/services/ssh/SshCredentialService.ts
SshHostKeyServiceHost key verificationsrc/main/services/ssh/SshHostKeyService.ts
RemotePtyServiceRemote terminal sessionssrc/main/services/RemotePtyService.ts
RemoteGitServiceGit operations over SSHsrc/main/services/RemoteGitService.ts

Setting up SSH connections

1

Add SSH connection

Go to Settings → SSH Connections and click Add Connection.
2

Configure connection details

Enter your server information:
  • Host: IP address or hostname (e.g., dev.example.com)
  • Port: SSH port (default: 22)
  • Username: Your SSH username
  • Authentication type: Agent, Key, or Password
3

Choose authentication method

Select one of three authentication types:
4

Test the connection

Click Test Connection to verify Emdash can connect to your server. A successful test will show:
  • Connection established
  • Server OS and shell information
  • Git availability
5

Add a remote project

After connecting, add a remote project:
  • Project path: Absolute path on the remote server (e.g., /home/user/my-project)
  • The project must be a Git repository
  • Emdash will create worktrees at <project>/.emdash/worktrees/<task-slug>/

Credential storage

Emdash uses Keytar (native OS keychain integration) to securely store SSH credentials:
// From SshCredentialService.ts
const SERVICE_NAME = 'emdash-ssh';

// Password storage
await keytar.setPassword(SERVICE_NAME, `${connectionId}:password`, password);

// Passphrase storage (for encrypted keys)
await keytar.setPassword(SERVICE_NAME, `${connectionId}:passphrase`, passphrase);
Keychain locations:
  • macOS: Keychain Access.app
  • Linux: libsecret (GNOME Keyring, KWallet)
  • Windows: Windows Credential Manager
Credentials are never stored in plaintext or in the SQLite database.

Remote PTY sessions

When you run an agent on a remote server, Emdash:
  1. Opens an SSH connection to the server
  2. Creates a shell session via ssh2’s client.shell()
  3. Injects environment variables and changes to the worktree directory
  4. Spawns the agent CLI
  5. Streams output back to the local terminal UI in real-time

Security features

Shell allowlist

Remote PTY restricts shell binaries to a hardcoded allowlist (/bin/bash, /bin/zsh, etc.) to prevent command injection.

Env var validation

Environment variable keys are validated against ^[A-Za-z_][A-Za-z0-9_]*$ to prevent shell escaping.

Argument escaping

All shell arguments pass through quoteShellArg() from src/main/utils/shellEscape.ts.

Connection pooling

Max 10 concurrent SSH connections. Pool warnings at 80% utilization.

Code example

From RemotePtyService.ts:65-100:
async startRemotePty(options: RemotePtyOptions): Promise<RemotePty> {
  const connection = this.sshService.getConnection(options.connectionId);
  if (!connection) {
    throw new Error(`Connection ${options.connectionId} not found`);
  }

  const client = connection.client;

  return new Promise((resolve, reject) => {
    client.shell((err, stream) => {
      if (err) {
        reject(err);
        return;
      }

      // Validate env var keys to prevent injection
      const envEntries = Object.entries(options.env || {}).filter(([k]) => {
        if (!isValidEnvVarName(k)) {
          console.warn(`[RemotePtyService] Skipping invalid env var name: ${k}`);
          return false;
        }
        return true;
      });
      const envVars = envEntries
        .map(([k, v]) => `export ${k}=${quoteShellArg(v)}`)
        .join(' && ');

      const cdCommand = options.cwd ? `cd ${quoteShellArg(options.cwd)}` : '';

      // Validate shell against allowlist
      const shellBinary = options.shell.split(/\s+/)[0];
      if (!ALLOWED_SHELLS.has(shellBinary)) {
        reject(new Error(`Shell not allowed: ${shellBinary}`));
        return;
      }

      // ...
    });
  });
}

Remote worktree management

Remote tasks use the same worktree isolation as local tasks:
  • Location: <project>/.emdash/worktrees/<task-slug>/
  • Branch: emdash/<task-slug>-<hash>
  • Preserved files: .env, .envrc, etc. (same as local)

Differences from local

The following features are local-only and do not yet work with remote projects:
  • File diffs in the UI
  • File watching / live updates
  • Branch push automation
  • Worktree pooling (instant task starts)
  • GitHub PR creation
  • CI/CD check tracking
Remote Git operations (commit, branch, status) work via RemoteGitService, which executes Git commands over SSH.

Connection management

Emdash maintains a connection pool with automatic cleanup:
// From SshService.ts:12-16
const MAX_CONNECTIONS = 10;
const POOL_WARNING_THRESHOLD = 0.8;

// Connection lifecycle
await sshService.connect(config);       // Establish
await sshService.executeCommand(...);   // Use
await sshService.disconnect(id);        // Close
await sshService.disconnectAll();       // Cleanup on shutdown

Connection events

SshService extends EventEmitter and emits:
  • connected: Connection established
  • error: Connection error
  • disconnected: Connection closed

Command execution

All remote commands run in a login shell to ensure user configs are loaded:
// From SshService.ts:275-288
async executeCommand(
  connectionId: string,
  command: string,
  cwd?: string
): Promise<ExecResult> {
  const innerCommand = cwd
    ? `cd ${quoteShellArg(cwd)} && ${command}`
    : command;
  const fullCommand = `bash -l -c ${quoteShellArg(innerCommand)}`;

  // Execute via ssh2 client.exec()
  // Returns { stdout, stderr, exitCode }
}
This ensures:
  • ~/.ssh/config is loaded
  • ~/.gitconfig is available
  • Shell aliases and functions work

Troubleshooting

Symptom: Error: “SSH agent authentication failed: no agent socket found”Solutions:
  1. Start SSH agent: eval $(ssh-agent -s)
  2. Add your key: ssh-add ~/.ssh/id_ed25519
  3. Verify: ssh-add -l
  4. If launching from GUI, restart Emdash from a terminal
  5. Or use private key/password auth instead
Symptom: Error: “Host key verification failed”Solutions:
  1. Ensure the host is in ~/.ssh/known_hosts
  2. Run ssh user@host manually to accept the host key
  3. Or temporarily disable strict host checking in ~/.ssh/config:
    Host example.com
      StrictHostKeyChecking no
      UserKnownHostsFile /dev/null
    
Symptom: “Git is not installed on the remote server”Solution: Install Git on the remote server:
# Debian/Ubuntu
sudo apt-get install git

# RHEL/CentOS
sudo yum install git

# macOS
brew install git
Symptom: “Failed to create worktree: Permission denied”Solution: Ensure your SSH user has write access to the project directory:
chmod +w /path/to/project
# Or change ownership
chown -R your-user:your-group /path/to/project

Security best practices

  1. Use SSH agent auth instead of storing passwords/keys
  2. Rotate credentials regularly
  3. Use dedicated SSH keys for Emdash (not your personal keys)
  4. Restrict remote user permissions to project directories only
  5. Enable SSH key-based auth and disable password auth on the server:
    # /etc/ssh/sshd_config
    PasswordAuthentication no
    PubkeyAuthentication yes
    
  6. Monitor SSH logs for suspicious activity

Build docs developers (and LLMs) love