Tabby is a cross-platform terminal emulator built on Electron 38, Angular 15, and xterm.js v6. It replaces monolithic terminal applications with a modular, plugin-based architecture where every major feature — local terminals, SSH, serial, telnet, settings UI, link detection, color schemes — is a separate Angular module loaded at runtime through dependency injection. This guide covers the full architecture, every configuration surface, deployment packaging, security model, performance tuning, and the internal developer APIs needed to extend or maintain the project.
For usage tutorials, keyboard shortcuts, and day-to-day workflow guidance, see the companion User Guide.
1. Architecture Overview
1.1 High-Level System Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ Electron 38 Shell │
│ ┌──────────────────────┐ ┌────────────────────────────────────┐ │
│ │ Main Process │ │ Renderer Process │ │
│ │ │ │ │ │
│ │ app/lib/index.ts │ │ Angular 15 Application │ │
│ │ ├── Application │◄──►│ ├── AppService (tabs) │ │
│ │ │ ├── Window[] │IPC │ ├── ConfigService (YAML) │ │
│ │ │ ├── PTYManager │ │ ├── ProfilesService │ │
│ │ │ ├── PluginManager│ │ ├── HotkeysService │ │
│ │ │ ├── DockingServ. │ │ ├── VaultService │ │
│ │ │ ├── Updater │ │ ├── ThemesService │ │
│ │ │ └── Tray │ │ ├── CommandService │ │
│ │ │ │ │ └── TabRecoveryService │ │
│ │ ├── config.ts (I/O) │ │ │ │
│ │ ├── pty.ts (node-pty)│ │ Plugin Modules (Angular DI) │ │
│ │ ├── cli.ts (yargs) │ │ ├── tabby-terminal (xterm.js) │ │
│ │ ├── sentry.ts │ │ ├── tabby-local (shells) │ │
│ │ └── window.ts │ │ ├── tabby-ssh (russh) │ │
│ │ │ │ ├── tabby-serial (serialport) │ │
│ └──────────────────────┘ │ ├── tabby-telnet │ │
│ │ ├── tabby-settings │ │
│ │ ├── tabby-plugin-manager │ │
│ │ ├── tabby-linkifier │ │
│ │ ├── tabby-community-color-schemes│ │
│ │ └── tabby-auto-sudo-password │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ node-pty │ │ Native Addons │
│ (PTY mgr) │ │ russh (SSH) │
│ │ │ serialport │
│ │ │ keytar │
└──────────┘ └──────────────┘
1.2 Process Model
Tabby follows Electron's dual-process architecture:
Main process (app/lib/): Manages the application lifecycle, BrowserWindow creation, PTY spawning, native plugin management, tray icon, global hotkeys, docking, auto-updates, and Sentry error reporting. Config file I/O (YAML read/write with atomic saves) happens here. The main process exposes services to the renderer via Electron IPC.
Renderer process (app/src/ + all tabby-* plugins): Runs the Angular 15 application. All UI rendering, tab management, terminal emulation (xterm.js v6), SSH session management, and plugin UI happens here. The renderer communicates with the main process through a well-defined IPC boundary for operations requiring Node.js APIs (file access, PTY control, native module calls).
Note
Tabby enforces single-instance mode. When a second instance is launched, it forwards its CLI arguments to the running instance via Electron's
app.requestSingleInstanceLock()mechanism and exits. The running instance receives the arguments and processes them (opening new tabs, running commands, etc.).
1.3 Plugin System Architecture
The plugin system is the central architectural decision in Tabby. Every feature is a plugin — there is no monolithic core that handles terminals, SSH, or settings directly. Even the "core" (tabby-core) is structured as a plugin, albeit one that provides the foundational services other plugins depend on.
Plugin Loading Sequence
- Discovery: On startup,
pluginManager.tsscans the built-in plugin directories and the user plugin directory ({userData}/plugins/) - Module Registration: Each plugin exports an Angular
NgModulethat declares its components and registers providers - Dependency Injection: Angular's multi-provider system allows plugins to contribute to shared extension points without knowing about each other
- Service Initialization: Services decorated with
@Injectable()are instantiated lazily by Angular's DI container when first injected
Built-in Plugins
| Plugin | Package | Responsibility |
|---|---|---|
tabby-core |
Core framework | Config management, hotkeys, tab lifecycle, profiles, command palette, vault, locale, themes, selectors |
tabby-electron |
Electron bindings | Platform-specific services, shell providers, docking, touchbar, auto-updater, winston logging, file I/O |
tabby-local |
Local terminals | Shell detection across platforms, PTY session management, terminal tab components |
tabby-terminal |
Terminal emulation | xterm.js v6 frontend, color scheme management, search, middleware pipeline, terminal decorators |
tabby-ssh |
SSH connections | SSH sessions via russh, SFTP, port forwarding (local/remote/dynamic), key management, multiplexing |
tabby-serial |
Serial ports | Serial connections via the serialport library, baud rate management |
tabby-telnet |
Telnet/socket | Telnet protocol and raw TCP socket connections |
tabby-settings |
Settings UI | Settings tab panels, profile editor, hotkey editor, vault configuration, config sync |
tabby-plugin-manager |
Plugin manager | npm-based third-party plugin installation, removal, and updates using a bundled npm binary |
tabby-linkifier |
Link detection | URL, IP address, and file path detection in terminal output with click handlers |
tabby-community-color-schemes |
Color schemes | 150+ community-contributed terminal color schemes |
tabby-auto-sudo-password |
Auto sudo | Detects sudo password prompts and auto-pastes a saved password |
tabby-web |
Web platform | Platform service bindings for browser-based deployment |
tabby-web-demo |
Demo mode | Browser-based demo using v86 x86 emulator |
tabby-uac |
UAC helper | Windows UAC elevation via a C++ native helper |
Extension Point System (Angular Multi-Providers)
Plugins register functionality through Angular's multi-provider tokens. Each token defines a contract; any plugin can provide implementations:
| Extension Point | Interface | Purpose |
|---|---|---|
ConfigProvider |
Contributes default config blocks | Merged via deepmerge at startup |
HotkeyProvider |
Registers keyboard shortcuts | Collected by HotkeysService |
ProfileProvider |
Defines connection profile types | SSH, local, serial, telnet profiles |
QuickConnectProfileProvider |
Parses quick-connect strings | user@host:port to SSH profile |
TabRecoveryProvider |
Persists/restores tab state | Called every 30s + on change |
TabContextMenuItemProvider |
Adds right-click menu items | Per-tab context menus |
ToolbarButtonProvider |
Adds toolbar buttons | Main toolbar customization |
CommandProvider |
Registers command palette entries | Fuzzy-searchable commands |
SettingsTabProvider |
Adds settings panels | Plugin-specific settings UI |
TerminalColorSchemeProvider |
Registers color schemes | Built-in + community + custom |
TerminalDecorator |
Decorates terminal instances | Overlays, modifications |
CLIHandler |
Handles CLI subcommands | Extend tabby CLI |
ShellProvider |
Detects available shells | Platform-specific shell discovery |
FileProvider |
File access abstraction | Platform-independent file operations |
SFTPContextMenuItemProvider |
SFTP right-click actions | File manager context menus |
SSHProfileImporter |
Imports SSH profiles | OpenSSH config, static files |
AutoPrivateKeyLocator |
Discovers SSH keys | Auto-find keys on disk |
SessionMiddleware |
Terminal I/O pipeline | Transform input/output streams |
1.4 Data Flow
Terminal Session Lifecycle
User action (click profile / CLI)
│
▼
ProfilesService.openProfile()
│
▼
TabsService.open(TerminalTabComponent)
│
▼
SessionFactory creates session:
├── LocalSession → PTYManager.spawn() → node-pty
├── SSHSession → russh native → remote PTY
├── SerialSession → serialport → hardware
└── TelnetSession → net.Socket → remote host
│
▼
SessionMiddlewareStack attaches:
InputProcessor → LoginScriptProcessor → OSCProcessor
→ TerminalStreamProcessor → UTF8SplitterMiddleware
│
▼
xterm.js Terminal instance renders output
│
▼
User input → middleware pipeline (reverse) → session.write()
Config Data Flow
config.yaml (disk)
│
▼ atomically.readFile() [main process]
│
ConfigService.readRawConfig()
│
▼ js-yaml.load()
│
Raw config object
│
▼ deepmerge(allDefaults, rawConfig)
│
ConfigProxy (reactive, Observable)
│
├──► changed$ Observable → subscribers react
│
▼ On save: diff against defaults, strip default values
│
Minimal diff written to config.yaml via atomically.writeFile()
1.5 IPC Boundary
The main process and renderer communicate through Electron IPC channels. Key channels include:
| Channel | Direction | Purpose |
|---|---|---|
pty:spawn |
Renderer to Main | Create new PTY process |
pty:write |
Renderer to Main | Send input to PTY |
pty:resize |
Renderer to Main | Resize PTY dimensions |
pty:kill |
Renderer to Main | Terminate PTY process |
pty:data |
Main to Renderer | PTY output data |
pty:exit |
Main to Renderer | PTY process exited |
config:read |
Renderer to Main | Read config from disk |
config:write |
Renderer to Main | Write config to disk |
app:toggle-window |
Main to Renderer | Docking toggle |
app:update-available |
Main to Renderer | Auto-update notification |
1.6 Dependency Graph
The plugin dependency graph flows strictly downward. No circular dependencies exist between plugin packages:
tabby-core (foundation — no plugin dependencies)
│
├── tabby-electron (platform bindings)
│ │
│ ├── tabby-local (depends on core + electron)
│ │
│ └── tabby-plugin-manager (depends on core + electron)
│
├── tabby-terminal (depends on core)
│ │
│ ├── tabby-ssh (depends on core + terminal)
│ │
│ ├── tabby-serial (depends on core + terminal)
│ │
│ ├── tabby-telnet (depends on core + terminal)
│ │
│ ├── tabby-linkifier (depends on core + terminal)
│ │
│ ├── tabby-community-color-schemes (depends on core + terminal)
│ │
│ └── tabby-auto-sudo-password (depends on core + terminal)
│
├── tabby-settings (depends on core)
│
├── tabby-web (depends on core — alternative to tabby-electron)
│
└── tabby-web-demo (depends on core + web)
2. System Requirements
2.1 Supported Platforms
| Platform | Versions | Architecture | Package Formats |
|---|---|---|---|
| Windows | 10, 11 | x64, ARM64 | MSIX, NSIS installer, portable zip |
| macOS | 12+ (Monterey) | x64, ARM64 (Apple Silicon) | DMG, zip |
| Linux | Ubuntu 22.04+, Fedora 38+, Arch | x64, ARM64 | AppImage, deb, rpm, pacman, snap |
2.2 Runtime Dependencies
| Component | Version | Purpose |
|---|---|---|
| Electron | 38 | Desktop shell, Chromium renderer |
| Node.js | 22+ (bundled with Electron) | Main process runtime |
| OpenSSL | 3.x (bundled) | TLS for SSH, HTTPS |
No external runtime dependencies are required. Electron bundles Node.js and Chromium. Native modules (node-pty, russh, serialport, keytar) are compiled and bundled during the build process.
2.3 Build Dependencies
| Tool | Version | Purpose |
|---|---|---|
| Node.js | 22.x | Build toolchain |
| Yarn | 1.x (Classic) | Package manager, workspace management |
| Python | 3.x | Native module compilation (node-gyp) |
| C/C++ compiler | Platform-specific | Native addons (node-pty, russh, serialport, keytar) |
| Rust toolchain | Latest stable | russh SSH library compilation |
Platform-specific build requirements:
Windows:
- Visual Studio Build Tools 2019 or 2022 with the "Desktop development with C++" workload
- Windows 10 SDK or later
- Rust toolchain (rustup + stable)
- Python 3.x for node-gyp
macOS:
- Xcode Command Line Tools (
xcode-select --install) - Rust toolchain (rustup + stable)
- Python 3.x for node-gyp
Linux:
build-essential(gcc, g++, make)libx11-dev,libxkbfile-dev(X11 keyboard support)libsecret-1-dev(keytar — OS credential store)rpmanddpkg(for building distribution packages)- Rust toolchain (rustup + stable)
- Python 3.x for node-gyp
2.4 Hardware Recommendations
| Resource | Minimum | Recommended | Notes |
|---|---|---|---|
| RAM | 256 MB | 512 MB+ | Per window. Each tab adds ~10-25 MB for scrollback |
| GPU | OpenGL ES 2.0 | Dedicated GPU | WebGL terminal renderer; falls back to canvas |
| Disk | 200 MB | 500 MB+ | Base install; plugins add ~5-20 MB each |
| CPU | Dual-core | Quad-core+ | Multiple PTY sessions are CPU-bound on the main process |
| Display | 1024x768 | 1920x1080+ | Tabby renders at native resolution |
2.5 Network Requirements
Tabby itself has no mandatory network requirements for local terminal use. Network access is needed for:
| Feature | Destination | Protocol | Port |
|---|---|---|---|
| SSH connections | User-configured hosts | TCP | 22 (default) |
| Telnet connections | User-configured hosts | TCP | 23 (default) |
| Plugin installation | registry.npmjs.org | HTTPS | 443 |
| Auto-updates | github.com (releases) | HTTPS | 443 |
| Error reporting (Sentry) | Sentry ingest endpoint | HTTPS | 443 |
| Crowdin translations | crowdin.com | HTTPS | 443 |
3. Configuration Reference
3.1 Config File Location
Tabby stores its configuration as a single YAML file. The location depends on platform and environment:
| Platform | Default Path |
|---|---|
| Windows | %APPDATA%\tabby\config.yaml |
| macOS | ~/Library/Application Support/tabby/config.yaml |
| Linux | ~/.config/tabby/config.yaml |
| Override | $TABBY_CONFIG_DIRECTORY/config.yaml |
| Portable | <exe_dir>/data/config.yaml |
A backup is maintained at config.yaml.backup in the same directory. The backup is written before every config save operation.
Important
Tabby stores only the diff from defaults in
config.yaml. Values that match the default are stripped on save. This means a freshconfig.yamlmay be nearly empty — even a single line with justversion: 7. To see the full effective configuration (defaults merged with overrides), inspectConfigService.storeat runtime via the developer console (Ctrl+Shift+Ithen Console tab). This diff-only storage is a deliberate design choice: it ensures that when a new version of Tabby ships with updated defaults, users automatically get the new defaults for any setting they have not explicitly overridden.
3.2 Config File Format
The config file uses YAML syntax parsed by js-yaml. Writes are atomic via the atomically library (write to temp file, then rename). On every save, the current config is backed up first.
# Example config.yaml showing commonly customized values
version: 7
appearance:
theme: Standard
colorScheme: Dracula
font: "MesloLGS NF"
fontSize: 14
fontWeight: 400
fontWeightBold: 700
ligatures: true
opacity: 1
vibrancy: false
frame: full # full | thin | none
dock: off # off | top | bottom | left | right
dockFill: 0.5
dockSpace: 1
dockScreen: current
dockAlwaysOnTop: true
tabsOnTop: false
tabsLocation: top # top | bottom | left | right
cycleTabs: true
flexTabs: true
terminal:
frontend: xterm-webgl # xterm-webgl | xterm-canvas
scrollbackLines: 25000
cursorBlink: true
cursorStyle: block # block | underline | bar
bell: "off" # off | visual | audible
bracketedPaste: true
rightClick: menu # menu | paste | clipboard
pasteOnMiddleClick: true
copyOnSelect: false
wordSeparator: " ()[]{}'\"\\/"
drawBoldTextInBrightColors: true
sixel: true
autoOpen: false
warnOnMultilinePaste: false
altIsMeta: false # macOS: use Option as Meta
scrollOnInput: true
unicode: true
detectProgress: true
hideTabIndex: false
ssh:
warnOnClose: true
winSCPPath: null
agentAccounts: []
knownHosts: []
serial:
warnOnClose: true
hotkeys: {} # See §3.6
profiles: [] # See §3.4
groups: [] # Profile groups
vault: null # Encrypted vault blob
recoverTabs: true
warnOnClose: true
enableAnalytics: true
enableAutomaticUpdates: true
providerBlacklist: [] # Disabled extension points
commandBlacklist: [] # Hidden commands
3.3 Config Proxy System
ConfigService exposes configuration through a ConfigProxy — a JavaScript Proxy object that intercepts property access and merges defaults with stored values in real-time.
Key behaviors:
- Default merging: Accessing
config.store.terminal.scrollbackLinesreturns the stored value if set, otherwise the default from allConfigProvidercontributions - Diff-only storage: When a value is set back to its default, it is deleted from the store (only deviations are persisted)
- Change notification: Every mutation triggers
ConfigService.changed$(an RxJS Observable), allowing all subscribers to react - Ready signal:
ConfigService.ready$is anAsyncSubjectthat completes once config is fully loaded and all providers have contributed defaults - Platform-specific defaults:
ConfigProviderimplementations can return different defaults per platform (Windows, macOS, Linux, Web) - Array-replace merge strategy: When merging defaults with stored config, arrays are replaced entirely (not appended). This prevents duplicate entries when defaults change between versions.
// How config providers contribute defaults (simplified)
@Injectable()
export class MyPluginConfigProvider extends ConfigProvider {
defaults = {
myPlugin: {
enabled: true,
maxRetries: 3,
},
}
platformDefaults = {
[Platform.Windows]: {
myPlugin: {
shellPath: 'C:\\Windows\\System32\\cmd.exe',
},
},
[Platform.macOS]: {
myPlugin: {
shellPath: '/bin/zsh',
},
},
}
}
3.4 Profile System
Profiles define connection parameters for terminals. Each profile type (local, SSH, serial, telnet) has its own schema with type-specific defaults.
Profile Structure
profiles:
- id: "uuid-here"
type: local # local | ssh | serial | telnet
name: "My Profile"
group: "group-uuid" # Optional group membership
icon: "fas fa-terminal" # FontAwesome icon
color: "#ff0000" # Tab color
weight: 0 # Sort order
isBuiltin: false
options:
# Type-specific options (see below)
Profile Groups
Profiles can be organized into groups for the UI:
groups:
- id: "group-uuid"
name: "Production Servers"
icon: "fas fa-server"
color: "#e74c3c"
SSH Profile Options (Complete Reference)
options:
host: "server.example.com"
port: 22
user: "root"
auth: null # null = auto-detect
privateKeys: [] # Paths to private key files
keepaliveInterval: 5000 # ms between keepalive packets
keepaliveCountMax: 10 # max missed keepalives before disconnect
readyTimeout: 30000 # ms to wait for connection
x11: false # X11 forwarding
skipBanner: false # Skip SSH banner display
jumpHost: null # UUID of another SSH profile (ProxyJump)
agentForward: false # Forward local SSH agent
reuseSession: true # SSH multiplexing
socksProxyHost: null # SOCKS proxy hostname
socksProxyPort: null # SOCKS proxy port
httpProxyHost: null # HTTP proxy hostname
httpProxyPort: null # HTTP proxy port
proxyCommand: null # ProxyCommand equivalent
forwardedPorts: [] # See §3.5
scripts: [] # Login scripts (expect/send)
algorithms:
kex: [...] # Key exchange (see §7.3)
serverHostKey: [...]
cipher: [...]
hmac: [...]
Local Profile Options (Complete Reference)
options:
command: "" # Shell executable (empty = default shell)
args: [] # Command-line arguments
cwd: "" # Working directory (empty = home)
env: {} # Environment variable overrides
Serial Profile Options (Complete Reference)
options:
port: "COM3" # Windows: COM1-COM256; Linux: /dev/ttyUSB0, /dev/ttyS0
baudRate: 115200
dataBits: 8 # 5, 6, 7, 8
stopBits: 1 # 1, 1.5, 2
parity: "none" # none, even, odd, mark, space
rtscts: false # RTS/CTS hardware flow control
xon: false # XON/XOFF software flow control
xany: false # XANY flow control
slowSend: false # Character-by-character send with delay
inputMode: "readline" # readline | local-echo
outputMode: "CRLF" # CR | LF | CRLF
scripts: [] # Login scripts
Supported baud rates: 110, 150, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1500000
Telnet Profile Options (Complete Reference)
options:
host: "server.example.com"
port: 23 # 23 for Telnet, 1234 for raw socket
inputMode: "readline" # readline | local-echo
outputNewlines: "CRLF" # CR | LF | CRLF
scripts: [] # Login scripts
Built-in Profiles
Tabby automatically generates built-in profiles from detected shells (via ShellProvider implementations) and imported SSH configurations (via SSHProfileImporter). These have isBuiltin: true and do not appear in the config file. They can be overridden by creating a user profile with the same type and equivalent options.
Recent Profiles
The most recently used profiles are tracked and shown in the quick-access UI:
# Controlled by internal settings
# Default: last 3 profiles
The count of recent profiles is configurable (default 3).
3.5 Port Forwarding Configuration
SSH profiles support three types of port forwarding:
forwardedPorts:
# Local forward: listen on local:8080, forward to remote-host:80
- type: local
host: "localhost"
port: 8080
targetAddress: "remote-host"
targetPort: 80
# Remote forward: listen on remote:9090, forward to local:3000
- type: remote
host: "0.0.0.0"
port: 9090
targetAddress: "localhost"
targetPort: 3000
# Dynamic (SOCKS proxy): listen on local:1080
- type: dynamic
host: "localhost"
port: 1080
Port forwards are established automatically when the SSH connection opens. They persist for the lifetime of the SSH session.
3.6 Hotkey Configuration
Hotkeys are registered by HotkeyProvider implementations across all plugins. The user can override any hotkey in the config:
hotkeys:
new-tab:
- "Ctrl+Shift+T"
close-tab:
- "Ctrl+Shift+W"
toggle-window:
- "Ctrl+Space"
copy:
- "Ctrl+Shift+C"
paste:
- "Ctrl+Shift+V"
split-right:
- "Ctrl+Shift+E"
split-bottom:
- "Ctrl+Shift+D"
focus-next-pane:
- "Ctrl+Tab"
command-palette:
- "Ctrl+Shift+P"
search:
- "Ctrl+Shift+F"
zoom-in:
- "Ctrl+="
zoom-out:
- "Ctrl+-"
reset-zoom:
- "Ctrl+0"
Hotkeys use Electron's accelerator syntax. Multiple bindings per action are supported (array values). Setting an empty array disables the hotkey entirely.
Global vs. local hotkeys: The toggle-window hotkey is a global hotkey (works even when Tabby is not focused). All other hotkeys are local (only active when Tabby has focus).
3.7 Vault Configuration
The vault stores encrypted secrets (SSH passwords, key passphrases, auto-sudo passwords). It uses a master passphrase to derive an encryption key.
vault:
# This is the encrypted blob — do not edit manually
enabled: true
# Encrypted content stored as base64
Vault operations:
- Lock: Clears the decrypted vault from memory; requires passphrase to unlock
- Unlock: Decrypts the vault blob with the master passphrase
- Not configured:
vault: null— secrets stored in plaintext in profile options - Change passphrase: Decrypts with old passphrase, re-encrypts with new one
Tip
The vault encrypts all values stored via
PasswordStorageService. When the vault is locked, SSH connections requiring stored passwords will prompt for the vault passphrase first. If you use Tabby in an environment where you frequently lock/unlock your workstation, consider setting a shorter vault auto-lock timeout.
3.8 Environment Variables
| Variable | Purpose | Default |
|---|---|---|
TABBY_CONFIG_DIRECTORY |
Override config file directory | Platform-specific (see §3.1) |
TABBY_PLUGINS |
Additional plugin search paths (colon/semicolon-separated) | None |
TABBY_DEV |
Enable development mode (disables Angular prod mode, enables debugging) | Not set |
TABBY_FORCE_ANGULAR_PROD |
Force Angular production mode even in dev builds | Not set |
PORTABLE_EXECUTABLE_FILE |
Detected automatically for portable builds; triggers portable mode | Not set |
NODE_PATH |
Additional Node.js module resolution paths | Standard |
3.9 Portable Mode
Tabby supports a portable installation mode where all data (config, plugins, logs) is stored next to the executable rather than in user-specific directories.
Detection: The main process (portable.ts) checks for a data/ directory adjacent to the executable. If found, userData and appData paths are redirected to data/.
Setup:
- Download the portable ZIP distribution
- Extract to any directory (e.g., a USB drive)
- Create a
data/subdirectory next to the executable - Launch Tabby — all state will be stored in
data/
This is particularly useful for USB-drive deployments, environments where per-user directories are restricted, or when you want a completely self-contained installation that leaves no traces on the host system.
3.10 Config Sync
Tabby supports configuration synchronization through the settings UI (tabby-settings). The sync mechanism allows exporting and importing the config file, enabling manual sync across machines via cloud storage, version control, or direct file copy.
3.11 Provider and Command Blacklists
Two blacklist mechanisms allow administrators to disable features:
# Disable specific extension points (prevents plugins from registering)
providerBlacklist:
- "tabby-serial:SerialProfileProvider"
- "tabby-telnet:TelnetProfileProvider"
# Hide specific commands from the command palette
commandBlacklist:
- "tabby-plugin-manager:install"
These are powerful tools for managed deployments where certain features should be unavailable to end users.
3.12 Legacy Terminus Migration
Tabby was originally named Terminus. On first launch, config.ts in the main process checks for a Terminus config directory and automatically migrates:
- Config file (
config.yaml) - Plugin directory
- Log files
The migration is one-time and non-destructive (copies, does not move).
4. Deployment & Scaling
4.1 Build System
Tabby uses a monorepo with Yarn workspaces. Each plugin is a separate npm package with its own webpack.config.mjs and package.json.
Repository Structure
tabby/
├── app/ # Electron main process + renderer entry
│ ├── lib/ # Main process source (TypeScript)
│ │ ├── index.ts # Entry point
│ │ ├── app.ts # Application class
│ │ ├── window.ts # Window management
│ │ ├── cli.ts # CLI parser (yargs)
│ │ ├── config.ts # Config I/O (YAML, atomic writes)
│ │ ├── pty.ts # PTY manager (node-pty, flow control)
│ │ ├── pluginManager.ts # Plugin install/uninstall (npm)
│ │ ├── portable.ts # Portable mode detection
│ │ ├── sentry.ts # Error reporting setup
│ │ ├── lru.ts # Path resolution cache
│ │ └── utfSplitter.ts # UTF-8 stream splitting
│ ├── src/ # Renderer entry (Angular bootstrap)
│ └── patches/ # patch-package patches for app deps
├── tabby-core/ # Core framework plugin
│ └── src/
│ ├── api/ # Public API types (extension points)
│ └── services/ # Core services (config, app, profiles, etc.)
├── tabby-electron/ # Electron platform plugin
│ └── src/
│ ├── services/ # Electron-specific services
│ └── shells/ # Shell provider implementations (13 providers)
├── tabby-local/ # Local terminal plugin
│ └── src/
│ ├── services/ # Terminal and session services
│ └── cli.ts # Local CLI handler
├── tabby-terminal/ # Terminal emulation plugin
│ └── src/
│ ├── api/ # Terminal API types
│ ├── middleware/ # I/O middleware pipeline (5 processors)
│ └── services/ # Terminal services
├── tabby-ssh/ # SSH plugin
│ └── src/
│ ├── api/ # SSH API types
│ └── services/ # SSH, SFTP, multiplexer, known hosts
├── tabby-serial/ # Serial plugin
│ └── src/services/ # Serial port services
├── tabby-telnet/ # Telnet plugin
│ └── src/ # Telnet session and provider
├── tabby-settings/ # Settings UI plugin
│ └── src/services/ # Settings services
├── tabby-plugin-manager/ # Plugin manager plugin
│ └── src/services/ # Plugin manager service
├── tabby-linkifier/ # Link detection plugin
│ └── src/ # URL/IP/path detectors
├── tabby-community-color-schemes/
│ ├── schemes/ # 150+ color scheme YAML files
│ └── src/ # Color scheme provider
├── tabby-auto-sudo-password/ # Auto-sudo plugin
│ └── src/ # Prompt detection + auto-paste
├── tabby-web/ # Web platform plugin
│ └── src/services/ # Web-specific platform services
├── tabby-web-demo/ # Demo plugin
│ ├── data/ # v86 emulator data
│ └── src/ # Demo setup
├── tabby-uac/ # Windows UAC helper
│ └── UAC/ # C++ native module source
├── locale/ # i18n translation files (24+ .po files)
├── patches/ # Root-level dependency patches
├── scripts/ # Build scripts (per-platform)
├── extras/ # Extras
│ ├── clink/ # Clink integration scripts
│ └── automator-workflows/ # macOS Automator workflows
├── snap/ # Snapcraft packaging
├── electron-builder.yml # Electron Builder packaging config
├── package.json # Root workspace definition
├── tsconfig.json # TypeScript configuration
└── .eslintrc.yml # ESLint configuration
Build Commands
# Install dependencies (must use yarn, not npm — workspace resolution)
yarn install
# Build all plugins + main app
yarn run build
# Watch mode for development (incremental rebuilds)
yarn run watch
# Start in development mode
yarn run start
# Lint all code
yarn run lint
# Generate TypeDoc API documentation
yarn run docs
# i18n operations
yarn run i18n:extract # Extract translatable strings
yarn run i18n:compile # Compile .po files to runtime format
Webpack Configuration
Each plugin has its own webpack.config.mjs that:
- Compiles TypeScript via
ts-loader - Processes Angular templates via
@angular/compiler-cli - Processes SCSS/CSS styles
- Externalizes shared dependencies (
@angular/*,tabby-core,rxjs,electron, etc.) to avoid duplication across plugin bundles - Outputs a single bundle file to the plugin's
dist/directory
The main app has two webpack configs:
app/webpack.main.config.mjs— Electron main process (Node.js target)app/webpack.config.mjs— Renderer process (web target with Node.js integration)
4.2 Packaging
Tabby uses electron-builder for creating distributable packages. Platform-specific build scripts handle native module compilation, code signing, and packaging.
Windows Packaging
# Build NSIS installer + portable + MSIX
node scripts/build-windows.mjs
# Outputs (in dist/):
# tabby-setup-{version}.exe (NSIS installer)
# tabby-{version}-portable.zip (Portable — no install needed)
# tabby-{version}.msix (Microsoft Store / sideload)
Code signing: Windows builds can be code-signed with an EV certificate for SmartScreen trust. Configure signing in electron-builder.yml under win.signingHashAlgorithms and win.certificateSubjectName.
macOS Packaging
# Build DMG + zip for both architectures
node scripts/build-macos.mjs
# Outputs (in dist/):
# tabby-{version}-macos-x64.dmg
# tabby-{version}-macos-arm64.dmg
# tabby-{version}-macos-x64.zip
# tabby-{version}-macos-arm64.zip
Notarization: macOS builds require notarization for Gatekeeper compatibility. Configure Apple ID credentials in electron-builder.yml under mac.notarize.
Linux Packaging
# Build all Linux package formats
node scripts/build-linux.mjs
# Outputs (in dist/):
# tabby-{version}-linux-x64.AppImage
# tabby-{version}-linux-x64.deb
# tabby-{version}-linux-x64.rpm
# tabby-{version}-linux-x64.pacman
# tabby-{version}-linux-x64.snap
# tabby-{version}-linux-arm64.AppImage (if ARM build host)
# tabby-{version}-linux-arm64.deb
Native Module Rebuilding
Native modules (node-pty, russh, serialport, keytar) must be compiled against Electron's Node.js ABI. The build system uses @electron/rebuild:
# Rebuild all native modules for current Electron version
npx @electron/rebuild
Note
The Rust-based
russhmodule requires a working Rust toolchain during build. Ensurerustcandcargoare in your PATH. The first build of russh can take 3-5 minutes as it compiles the entire Rust SSH implementation from source. Subsequent builds use Cargo's incremental compilation cache.
patch-package
The project uses patch-package to apply fixes to dependencies that have not been released upstream. Patches are stored in:
patches/— Root-level dependency patchesapp/patches/— App-specific dependency patchestabby-terminal/patches/— Terminal plugin dependency patches
These patches are applied automatically during yarn install via the postinstall script.
4.3 Plugin Distribution
Third-party plugins are distributed as npm packages. The built-in tabby-plugin-manager provides install/uninstall/update functionality.
Plugin installation paths:
- User plugins:
{userData}/plugins/node_modules/ - The plugin manager bundles its own npm binary to avoid requiring a system-wide npm installation
Plugin naming convention:
- npm packages must be named
tabby-*orterminus-*(legacy name) - The plugin manager searches the npm registry for packages with the
tabby-pluginkeyword
Plugin development workflow:
# 1. Create plugin directory
mkdir tabby-my-plugin && cd tabby-my-plugin
# 2. Initialize package.json
npm init
# Set "keywords": ["tabby-plugin"]
# Set "peerDependencies": { "tabby-core": "*" }
# 3. Create webpack.config.mjs (copy from existing plugin)
# 4. Create src/index.ts with NgModule export
# 5. Build
yarn build
# 6. Link for local development
ln -s $(pwd) ~/.config/tabby/plugins/node_modules/tabby-my-plugin
# 7. Restart Tabby — plugin is loaded automatically
# 8. Publish to npm
npm publish
4.4 Snap Packaging
The snap/ directory contains the Snapcraft configuration:
# Key snap configuration points
name: tabby
grade: stable
confinement: classic # Required — terminal emulators need unrestricted access
Classic confinement is mandatory because Tabby needs unrestricted access to the filesystem (PTY device files), network stack, and process spawning. Strict confinement would prevent core terminal functionality.
4.5 Auto-Updates
Tabby uses electron-updater for automatic updates:
- On startup (if
enableAutomaticUpdates: true), the updater checks GitHub Releases for newer versions - If an update is available, a notification is shown to the user via
ngx-toastr - The update is downloaded in the background
- On next restart, the update is applied
Update channel: By default, only stable releases are offered. Pre-release/beta channels can be configured in electron-builder.yml.
Disabling auto-updates:
enableAutomaticUpdates: false
This is recommended for managed deployments where IT controls software versions.
4.6 Mass Deployment Considerations
For deploying Tabby across an organization:
1. Configuration management:
- Use
TABBY_CONFIG_DIRECTORYenvironment variable to point to a shared or managed config location - Deploy a pre-configured
config.yamlto each user's config directory - Use Group Policy (Windows) or configuration management tools to manage the config file
2. Plugin control:
- Pre-install required plugins in the user plugins directory
- Use
providerBlacklistto disable unwanted features (serial, telnet, plugin manager, etc.) - Block npm registry access if third-party plugins are not desired
3. Update control:
- Set
enableAutomaticUpdates: falsein the managed config - Manage updates through your software distribution system (SCCM, Intune, Munki, etc.)
4. Portable mode:
- Deploy the portable version with a pre-configured
data/directory for zero-install environments - Useful for jump boxes, shared workstations, or locked-down desktops
5. Telemetry control:
- Set
enableAnalytics: falseto disable Sentry error reporting - No other telemetry is collected
5. Operations
5.1 Logging
Main Process Logging (Winston)
The Electron main process uses Winston with two transports:
| Transport | Destination | Format |
|---|---|---|
| File | {userData}/log.txt |
Timestamped JSON lines |
| Console | stdout/stderr | Colorized text |
Log file location by platform:
| Platform | Path |
|---|---|
| Windows | %APPDATA%\tabby\log.txt |
| macOS | ~/Library/Application Support/tabby/log.txt |
| Linux | ~/.config/tabby/log.txt |
| Portable | <exe_dir>/data/log.txt |
The log file is useful for diagnosing startup failures, PTY spawn errors, and native module loading issues — problems that occur before the renderer (and its developer console) is available.
Renderer Process Logging
The renderer process logs to the Chromium developer console. Access via:
Ctrl+Shift+I(Windows/Linux) orCmd+Option+I(macOS)- Navigate to the Console tab
Renderer logs include Angular lifecycle events, config changes, plugin loading, session state, and xterm.js events.
Sentry Error Reporting
Tabby integrates Sentry for automated crash reporting (app/lib/sentry.ts). This captures:
- Unhandled exceptions in the main process
- Unhandled promise rejections
- Renderer process crashes
Error reporting is controlled by the enableAnalytics config option:
# Disable all telemetry and error reporting
enableAnalytics: false
Tip
When diagnosing startup failures, check the main process log file directly. If the renderer fails to load, the main process log will contain the error (the developer console is not available when the renderer crashes during bootstrap). On Linux with snap, also check
journalctl --user -u snap.tabby.tabbyfor additional output.
5.2 Tab Recovery
Tabby automatically saves tab state for crash recovery:
- Auto-save interval: Every 30 seconds
- Debounced save: 1 second after any tab change (open, close, reorder, split)
- History depth: Last 5 closed tabs are recoverable via "Reopen Closed Tab"
- Storage: In the config file under the recovery key
Recovery data includes:
- Tab type (local, SSH, serial, telnet, split)
- Profile reference (UUID) and full options
- Working directory (for local terminals)
- Connection parameters (for SSH/serial/telnet)
- Split layout geometry (nested horizontal/vertical splits)
- Tab order and active tab indicator
On startup, if recoverTabs: true (default), Tabby restores the previous session's tabs. The recovery process:
- Reads serialized tab state from config
- Matches each serialized tab to a
TabRecoveryProvider - The provider reconstructs the tab component and reconnects if appropriate
- Failed recoveries are silently skipped (the tab is not restored)
5.3 CLI Reference
Tabby provides a CLI interface parsed by yargs (app/lib/cli.ts). Plugins can extend the CLI via CLIHandler providers.
# Open Tabby (or focus existing instance)
tabby
# Open a new tab with a specific profile
tabby open --profile "My SSH Server"
# Open a specific directory in a local terminal
tabby open --directory /path/to/dir
# Run a command in a new tab
tabby run -- htop
# Connect via SSH using quick-connect syntax
tabby connect ssh://user@host:port
# List available profiles
tabby profiles
# Open settings
tabby settings
# Show version
tabby --version
# Show help
tabby --help
Tip
The CLI respects single-instance mode. If Tabby is already running, CLI commands are forwarded to the running instance via IPC — a new window is not created. This enables scripting workflows: shell scripts or other tools can open SSH connections in Tabby with
tabby connect ssh://user@host.
5.4 Window Management
Docking (Quake-Style Dropdown)
Tabby supports docking the window to a screen edge, creating a dropdown terminal similar to Quake console or Guake:
appearance:
dock: top # off | top | bottom | left | right
dockFill: 0.5 # Width/height ratio (0-1), how much of the edge to fill
dockSpace: 1 # Screen utilization ratio (0-1), margin from edge
dockAlwaysOnTop: true
dockScreen: current # current | <screen-name>
The docking service (tabby-electron/src/services/docking.service.ts) handles:
- Window repositioning and resizing based on dock position and fill/space ratios
- Screen edge snapping with pixel-perfect alignment
- Auto-reposition when displays change (connect/disconnect monitors, resolution change)
- Global hotkey toggle (
toggle-windowhotkey, defaultCtrl+Space) - Show/hide animation
Multi-monitor behavior: When dockScreen is current, the window docks to the screen where the mouse cursor is located at the time of the toggle. Setting it to a specific screen name pins the docked window to that display.
Frameless Window Mode
appearance:
frame: none # full | thin | none
When frame is none, Tabby renders its own title bar with window controls:
- macOS: Traffic light buttons (close/minimize/maximize) are repositioned within the custom title bar area using
trafficLightPosition - Windows: Window Controls Overlay (WCO) is used when supported, providing native-feeling minimize/maximize/close buttons in the custom title bar
- Linux: Custom drawn buttons matching the desktop environment's theme when possible
Vibrancy/Transparency
appearance:
vibrancy: true
opacity: 0.9 # 0-1
Platform-specific behavior:
- Windows: Glasstron provides Aero-style blur effects (acrylic/mica on Windows 11)
- macOS: Native
NSVisualEffectViewvibrancy - Linux: Compositor-dependent — works with KDE/Kwin, may not work with all window managers
5.5 Tray Icon
The system tray icon is available on Windows and macOS (not Linux, due to inconsistent tray support across Linux desktop environments). The tray provides:
- Left-click: Toggle window visibility
- Right-click: Context menu with:
- Profile quick-launch submenu
- Show/hide toggle
- Quit action
When the window is closed (not quit), it minimizes to the tray instead of exiting. This allows the global hotkey to work even when no window is visible.
5.6 GPU Configuration
Tabby's terminal renderer can use WebGL for hardware-accelerated rendering:
terminal:
frontend: xterm-webgl # WebGL (GPU-accelerated) — default
# frontend: xterm-canvas # Canvas 2D (CPU, fallback)
The Application class (app/lib/app.ts) manages GPU settings at the Electron level. GPU acceleration can be disabled entirely via Electron command-line switches for systems with problematic drivers or when running in virtual machines:
# Launch with GPU disabled
tabby --disable-gpu
If WebGL rendering causes visual artifacts, black terminal areas, or crashes, switch to xterm-canvas in the settings.
Note
The WebGL renderer maintains a glyph texture atlas in GPU memory. With many open tabs using different fonts or font sizes, this atlas can grow. Switching to
xterm-canvaseliminates GPU memory usage entirely at the cost of rendering performance.
5.7 Multi-Window Support
Tabby supports multiple windows, each managed as a separate Window instance by the Application class. All windows:
- Share the same configuration (
ConfigServiceis singleton) - Share the same plugin set
- Have independent tab state and layouts
- Can be individually docked, resized, and positioned
- Are independently recoverable (tab recovery saves per-window state)
5.8 Internationalization
Tabby supports 24+ languages via @ngx-translate/core with ICU MessageFormat compilation:
| Language | Code | Status |
|---|---|---|
| English | en-US | Complete (source language) |
| Chinese (Simplified) | zh-CN | Community-maintained |
| Chinese (Traditional) | zh-TW | Community-maintained |
| Japanese | ja-JP | Community-maintained |
| Korean | ko-KR | Community-maintained |
| German | de-DE | Community-maintained |
| French | fr-FR | Community-maintained |
| Russian | ru-RU | Community-maintained |
| Spanish | es-ES | Community-maintained |
| Portuguese (Brazil) | pt-BR | Community-maintained |
| Turkish | tr-TR | Community-maintained |
| Arabic | ar-SA | Community-maintained |
| Italian | it-IT | Community-maintained |
| Polish | pl-PL | Community-maintained |
| Ukrainian | uk-UA | Community-maintained |
| Czech | cs-CZ | Community-maintained |
| Dutch | nl-NL | Community-maintained |
| Croatian | hr-HR | Community-maintained |
| Indonesian | id-ID | Community-maintained |
| And others... | ... | ... |
Translation files are .po format in the locale/ directory. The Crowdin integration manages translation workflows: string extraction, translator assignment, review, and download of completed translations.
Adding a new locale:
# Extract translatable strings from source
yarn run i18n:extract
# Add new .po file to locale/ directory
# Upload to Crowdin or translate manually
# Compile translations to runtime format
yarn run i18n:compile
6. Performance
6.1 PTY Flow Control
The PTY manager (app/lib/pty.ts) implements back-pressure-based flow control to prevent the renderer from being overwhelmed by fast-producing processes (e.g., cat /dev/urandom, large find outputs, or compile logs):
PTY output → node-pty → Buffer → Flow control gate → IPC → Renderer → xterm.js
Flow control parameters:
| Parameter | Value | Purpose |
|---|---|---|
maxChunk |
100 KB | Maximum bytes sent per IPC message |
maxDelta |
500 KB | Maximum buffered bytes before pausing PTY reads |
Flow control algorithm:
On PTY data received:
1. Append data to internal buffer
2. Update bufferSize counter
3. If bufferSize > maxDelta (500KB):
Pause PTY reading (back-pressure signal)
4. Extract up to maxChunk (100KB) from buffer
5. Send chunk to renderer via IPC
6. Wait for renderer acknowledgment
On renderer acknowledgment:
1. Decrement bufferSize by acknowledged bytes
2. If bufferSize < maxDelta AND PTY is paused:
Resume PTY reading
This prevents memory exhaustion when a process outputs data faster than the terminal can render it. Without flow control, a fast cat /dev/urandom would fill the IPC queue and consume unbounded memory.
6.2 Terminal Rendering Performance
WebGL Renderer (xterm-webgl)
The default frontend uses GPU-accelerated rendering via WebGL 2.0:
- Glyph atlas: Characters are rendered to a texture atlas once, then reused as textured quads for subsequent frames. This dramatically reduces the number of draw calls — instead of drawing each character individually, the renderer batches all cells into a small number of GPU draw calls
- Batched rendering: Cells with the same foreground/background colors are drawn in a single GPU pass
- Sixel/image support: Images embedded in terminal output (via Sixel protocol) are rendered as GPU textures, offloading image scaling to the GPU
- Selection rendering: Text selection is rendered as a separate overlay pass
Performance characteristics:
- 60 FPS rendering of terminal output on most hardware
- Handles 100,000+ lines of scrollback without rendering lag
- GPU memory usage proportional to font atlas size + scrollback
Canvas Fallback (xterm-canvas)
The Canvas 2D API fallback:
- CPU-rendered: All text is drawn using Canvas 2D
fillTextcalls - No GPU memory: Suitable for VMs, remote desktop sessions, or systems without GPU
- Universal compatibility: Works on any system with a Canvas implementation
- Performance: Noticeably slower than WebGL for rapid output, but adequate for interactive use
Performance-Related Config
terminal:
frontend: xterm-webgl # GPU-accelerated (default, fastest)
scrollbackLines: 25000 # Reduce for lower memory; increase for more history
ligatures: false # Disable font ligatures for faster text measurement
sixel: false # Disable image protocol if unused
unicode: true # Unicode 11 width calculation (slight overhead)
drawBoldTextInBrightColors: true # No perf impact, aesthetic choice
Note
Each terminal tab maintains its own xterm.js instance with its own scrollback buffer. With
scrollbackLines: 25000and a 200-column terminal, each tab's scrollback buffer can consume ~10-25 MB of memory. With 20 tabs open, that is 200-500 MB for scrollback alone. If you routinely run many concurrent sessions, consider reducingscrollbackLinesto 5000-10000.
6.3 Path Resolution Cache
The main process uses an LRU cache (app/lib/lru.ts) for fs.realpathSync results. This is a targeted optimization for:
- Plugin loading: Plugin directories are resolved repeatedly during startup
- Shell detection: Shell executable paths are resolved for each
ShellProvider - Native module loading:
requirecalls resolve paths through the filesystem
On Windows, realpathSync is particularly slow on NTFS junctions and symlinks. The LRU cache avoids redundant filesystem calls, reducing startup time by 100-300ms on Windows systems with antivirus software that hooks filesystem operations.
6.4 SSH Multiplexing
SSH multiplexing (SSHMultiplexerService) reuses existing SSH connections for new sessions to the same host:
# Per-profile setting
options:
reuseSession: true # Default: true
How it works:
- When opening an SSH connection,
SSHMultiplexerServicechecks if an active transport exists for the same host:port:user combination - If found, a new SSH channel is opened on the existing transport (no TCP handshake, no key exchange, no authentication)
- If not found, a new SSH connection is established and registered in the multiplexer pool
- When the last channel on a transport closes, the transport is kept alive for a grace period before disconnection
Performance impact:
- Second connection to the same host: ~50ms (channel open) vs. ~2-5 seconds (full connection)
- Reduced network overhead: one TCP connection handles multiple sessions
- Reduced server-side resource usage: one SSH session with multiple channels vs. multiple sessions
6.5 UTF-8 Stream Splitting
The UTF8SplitterMiddleware and utfSplitter.ts handle splitting byte streams at valid UTF-8 code point boundaries. Without this:
- A 3-byte UTF-8 character (e.g., CJK characters) could be split across two IPC messages
- xterm.js would receive an invalid byte sequence and render a replacement character
- The next message would start with trailing bytes that do not form a valid sequence
The splitter buffers incomplete sequences (1-3 trailing bytes) and emits them once the full code point arrives in the next data event. This is especially important for SSH sessions over high-latency connections where TCP segmentation does not respect character boundaries.
6.6 Memory Management Strategies
| Consumer | Typical Usage | Mitigation |
|---|---|---|
| Scrollback buffers | 10-25 MB per tab | Reduce scrollbackLines |
| WebGL glyph atlas | 4-32 MB (GPU) | Switch to xterm-canvas |
| Sixel images | Variable per image | Disable sixel: false |
| SSH transports | 1-5 MB per connection | Multiplexing reduces count |
| Plugin modules | 2-10 MB per plugin | Uninstall unused plugins |
| Angular component tree | 5-20 MB baseline | Close unused split panes |
| Electron/Chromium overhead | 80-150 MB baseline | Irreducible |
Monitoring: Use Electron's built-in process manager:
- Windows/Linux:
Windowmenu → look for process information, or useShift+Escin some builds - Developer Tools → Performance tab → Memory snapshots
chrome://tracingin Developer Tools for detailed GPU/rendering analysis
6.7 Startup Performance
Tabby's startup involves:
- Electron initialization: ~500ms (platform-dependent)
- Config file read: ~10ms (YAML parse)
- Plugin discovery: ~50-200ms (filesystem scan)
- Angular bootstrap: ~200-500ms (module compilation, DI setup)
- Shell detection: ~100-500ms (parallel filesystem probes)
- Tab recovery: ~50-200ms per recovered tab
- xterm.js initialization: ~30-50ms per terminal tab
Total cold start: typically 1-3 seconds. Hot start (from tray): <100ms (window show only).
Optimization techniques applied:
- LRU cache for path resolution (§6.3)
- Lazy instantiation of services via Angular DI
- Parallel shell detection across all providers
- WebGL glyph atlas reuse across tabs
7. Security
7.1 Threat Model
Tabby is a terminal emulator that executes arbitrary commands with the user's privileges. The security surface includes:
- Local process execution: Any command the user can run, Tabby can run. Tabby does not sandbox spawned processes.
- SSH key management: Private keys are read from disk for authentication. Key paths are stored in config.
- Credential storage: Passwords may be stored in the vault (encrypted), OS keychain (keytar), or plaintext in config.
- Network connections: SSH, telnet, and raw sockets connect to arbitrary remote hosts.
- Plugin system: Third-party plugins run with full Node.js + Electron privileges. No sandboxing.
- Auto-update: Update packages are downloaded from GitHub and executed. Signature verification depends on platform.
- Config file: Contains connection details, key paths, and potentially plaintext secrets.
- Clipboard access: Terminal content can be copied; paste operations inject text into the active session.
- IPC boundary: Main/renderer communication could be intercepted by a compromised renderer.
7.2 Credential Storage
Tabby provides three tiers of credential storage, in order of security:
| Method | Security Level | Storage Location | Encryption |
|---|---|---|---|
| Vault | High | config.yaml (encrypted blob) |
AES with passphrase-derived key |
| Keytar (OS keychain) | High | OS credential store | OS-managed |
| Plaintext in config | None | config.yaml |
None |
Vault architecture (VaultService in tabby-core):
- All passwords are encrypted with a master passphrase
- The encrypted blob is stored as a base64 string in
config.yaml - When locked, the decryption key is cleared from memory
- Operations requiring stored passwords prompt for the vault passphrase
- Changing the master passphrase re-encrypts all stored secrets
Keytar integration (PasswordStorageService in tabby-ssh):
- Uses the
keytarnative module for OS-level credential storage - Windows: Windows Credential Manager (encrypted by DPAPI)
- macOS: macOS Keychain (encrypted, access-controlled)
- Linux: libsecret (GNOME Keyring or KDE Wallet)
Warning
If the vault is not configured (
vault: null), SSH passwords and key passphrases stored in profiles are kept in plaintext inconfig.yaml. Any process with read access to the config file can extract these credentials. Always configure the vault for environments where credential security is important. Runchmod 600 config.yaml(or equivalent restrictive ACLs on Windows) as a baseline defense.
7.3 SSH Cryptographic Algorithms
Tabby's SSH implementation (via the russh Rust library, version 0.1.37) supports modern cryptographic algorithms including post-quantum options:
Key Exchange (KEX) — Default Order
| Algorithm | Security Level | Notes |
|---|---|---|
mlkem768x25519-sha256 |
Post-quantum | ML-KEM 768 (Kyber) hybridized with X25519. Provides quantum resistance while maintaining classical security. |
curve25519-sha256 |
High | Modern elliptic curve Diffie-Hellman. Preferred classical algorithm. |
curve25519-sha256@libssh.org |
High | Legacy identifier for the same algorithm (interop with older implementations) |
diffie-hellman-group16-sha512 |
High | 4096-bit modular DH with SHA-512 |
diffie-hellman-group14-sha256 |
Medium | 2048-bit modular DH with SHA-256. Included for compatibility. |
ext-info-c / ext-info-s |
N/A | RFC 8308 extension negotiation (client/server) |
kex-strict-c-v00@openssh.com |
N/A | Terrapin attack mitigation (client strict mode) |
kex-strict-s-v00@openssh.com |
N/A | Terrapin attack mitigation (server strict mode) |
Host Key Algorithms — Default Order
| Algorithm | Key Size | Notes |
|---|---|---|
ssh-ed25519 |
256-bit | Preferred. Small, fast, no known weaknesses. |
ecdsa-sha2-nistp256 |
256-bit | NIST P-256 curve |
ecdsa-sha2-nistp521 |
521-bit | NIST P-521 curve |
rsa-sha2-256 |
2048+ bit | RSA with SHA-256 digest |
rsa-sha2-512 |
2048+ bit | RSA with SHA-512 digest |
ssh-rsa |
2048+ bit | Legacy RSA with SHA-1 digest. Least preferred due to SHA-1 deprecation. |
Ciphers — Default Order
| Algorithm | Block/Key Size | Notes |
|---|---|---|
chacha20-poly1305@openssh.com |
256-bit key | Preferred. AEAD cipher, constant-time, no AES-NI dependency. |
aes256-gcm@openssh.com |
256-bit key | AEAD cipher. Fast on hardware with AES-NI. |
aes256-ctr |
256-bit key | AES-256 in counter mode (requires separate HMAC). |
aes192-ctr |
192-bit key | AES-192 in counter mode. |
aes128-ctr |
128-bit key | AES-128 in counter mode. Smallest key size offered. |
HMAC — Default Order
| Algorithm | Digest Size | Notes |
|---|---|---|
hmac-sha2-512-etm@openssh.com |
512-bit | Encrypt-then-MAC (preferred pattern) with SHA-512 |
hmac-sha2-256-etm@openssh.com |
256-bit | Encrypt-then-MAC with SHA-256 |
hmac-sha2-512 |
512-bit | MAC-then-encrypt with SHA-512 |
hmac-sha2-256 |
256-bit | MAC-then-encrypt with SHA-256 |
hmac-sha1-etm@openssh.com |
160-bit | Legacy ETM variant. SHA-1 is weak but no practical attack on HMAC-SHA1. |
hmac-sha1 |
160-bit | Legacy. Least preferred. |
Note on compression: SSH compression is none by default. Tabby does not enable zlib compression by default because modern connections are typically fast enough that the CPU overhead of compression exceeds the bandwidth savings.
Algorithm Customization
Per-profile algorithm overrides allow restricting or reordering the negotiated algorithms:
profiles:
- type: ssh
options:
algorithms:
kex:
- curve25519-sha256
- diffie-hellman-group16-sha512
- kex-strict-c-v00@openssh.com
cipher:
- aes256-gcm@openssh.com
- chacha20-poly1305@openssh.com
hmac:
- hmac-sha2-512-etm@openssh.com
serverHostKey:
- ssh-ed25519
Caution
Removing
kex-strict-c-v00@openssh.comandkex-strict-s-v00@openssh.comfrom the KEX list disables the Terrapin attack mitigation (CVE-2023-48795). These entries should always be included unless you are connecting to legacy servers that do not support strict KEX and you understand the risk. The Terrapin attack allows a man-in-the-middle to inject packets at the beginning of the SSH connection.
7.4 SSH Known Hosts
SSHKnownHostsService manages host key verification using the Trust On First Use (TOFU) model:
- First connection: The user is prompted to accept the host key. If accepted, it is stored in
ssh.knownHostsin the config. - Subsequent connections: The stored key is compared against the presented key. A match allows connection; a mismatch triggers a prominent warning.
- OpenSSH interop:
~/.ssh/known_hostsis also checked, providing compatibility with OpenSSH's known hosts database.
ssh:
knownHosts:
- host: "server.example.com"
port: 22
type: "ssh-ed25519"
key: "AAAAC3NzaC1lZDI1NTE5AAAA..."
7.5 SSH Agent Forwarding
Agent forwarding (agentForward: true) allows the remote server to use the local SSH agent for authentication to third-party servers:
options:
agentForward: true
agent: auto # auto | <named-pipe-path> | <socket-path>
The auto agent setting detects the local SSH agent:
- Windows: Checks for Pageant named pipe, then OpenSSH agent pipe (
\\.\pipe\openssh-ssh-agent) - macOS/Linux: Uses
SSH_AUTH_SOCKenvironment variable
Warning
SSH agent forwarding exposes your local SSH keys to the remote server's administrator. A malicious server administrator could use your forwarded agent to authenticate to other servers as you. Only enable agent forwarding to trusted servers. Consider using ProxyJump (
jumpHost) instead, which does not expose your agent.
7.6 SSH Jump Hosts (ProxyJump)
Jump host support allows chaining SSH connections through an intermediate bastion host:
profiles:
- name: "Bastion"
type: ssh
id: "bastion-uuid"
options:
host: "bastion.example.com"
user: "jumpuser"
- name: "Internal Server"
type: ssh
options:
host: "10.0.0.5"
user: "admin"
jumpHost: "bastion-uuid" # Reference to bastion profile by UUID
The connection flow:
- Tabby connects to the bastion host
- Through the bastion, it opens a direct-tcpip channel to the internal server
- The SSH handshake for the internal server happens through this tunneled connection
- The internal server never sees the client's real IP — only the bastion's IP
Multiple jump hosts can be chained (A → B → C) by having the intermediate profile also reference a jump host.
7.7 X11 Forwarding
options:
x11: true
When enabled, Tabby sets up X11 forwarding channels through the SSH connection. An X11Socket class manages the local X11 display connection. This allows running graphical applications on the remote server with display on the local machine.
Requirements:
- An X11 server running locally (X.Org, XQuartz on macOS, or Xming/VcXsrv on Windows)
DISPLAYenvironment variable set appropriately
7.8 Proxy Support
SSH connections support SOCKS and HTTP proxies:
options:
# SOCKS proxy (e.g., via SSH dynamic forwarding or Tor)
socksProxyHost: "127.0.0.1"
socksProxyPort: 1080
# HTTP CONNECT proxy (e.g., corporate proxy)
httpProxyHost: "proxy.corp.example.com"
httpProxyPort: 8080
# ProxyCommand (arbitrary command for connection)
proxyCommand: "nc -X 5 -x proxy:1080 %h %p"
7.9 Plugin Security
Third-party plugins run with the same privileges as Tabby itself: full Node.js APIs (filesystem, network, child process), full Electron APIs (IPC, clipboard, shell), and access to all Angular services (config, vault, credentials).
There is no plugin sandboxing or permission system.
Mitigations:
- Plugin installation requires explicit user action through the plugin manager UI
- Plugins are sourced from the npm registry, which provides package signing and some supply-chain security
- The
providerBlacklistconfig option can disable specific extension points - Administrators can block plugin installation by blacklisting
tabby-plugin-managerproviders or restricting npm registry access
Warning
Installing third-party plugins grants those plugins full access to your filesystem, network, and credentials (including vault contents if unlocked). Only install plugins from trusted sources. Review plugin source code when possible, especially plugins that interact with SSH sessions or credentials.
7.10 Auto-Sudo Security
The tabby-auto-sudo-password plugin detects sudo password prompts in terminal output via pattern matching and automatically sends the stored password.
Security considerations:
- The password is stored in the vault (encrypted) or keytar (OS-managed)
- A malicious program could output text matching the sudo prompt pattern, causing the password to be sent to it
- The plugin should be used only on trusted systems where you control what runs in the terminal
- Consider disabling this plugin on shared or multi-user systems
7.11 Config File Security
The config file (config.yaml) may contain sensitive information:
- SSH connection details (hosts, ports, usernames)
- Port forwarding rules (revealing internal network topology)
- SSH key file paths (revealing key locations)
- Passwords and passphrases (if vault is not configured)
- Known host keys (fingerprinting your infrastructure)
Hardening recommendations:
- Enable the vault for credential encryption — this is the single most important step
- Restrict file permissions:
chmod 600 config.yaml(Linux/macOS) or equivalent restrictive ACLs on Windows - Exclude from backups that are not themselves encrypted
- Use
TABBY_CONFIG_DIRECTORYto place config in an encrypted volume (e.g., LUKS, FileVault, BitLocker) if the system disk is not encrypted - Audit config.yaml periodically for plaintext credentials that may have been stored before vault configuration
7.12 Electron Security Baseline
Tabby's Electron configuration:
- Node integration: Enabled in the renderer (required for plugin system)
- Context isolation: The renderer has access to Node.js APIs (necessary for plugin architecture)
- Content Security Policy: Configured to allow
tabby://protocol and inline scripts required by Angular - Remote content: The terminal does not render remote web content — all UI is local Angular components
The lack of full context isolation is a deliberate trade-off: the plugin system requires renderer-side Node.js access for native module loading (russh, serialport, keytar). This means a compromised renderer could access local filesystem and network. The mitigation is that the renderer only processes locally-generated Angular UI — it does not load arbitrary web content.
8. Developer Internals
8.1 Module Reference
8.1.1 app/lib/ — Main Process
index.ts — Entry Point
The application entry point. Execution sequence:
- Sets up crash reporting (Sentry) early, before any async operations
- Loads the config file from disk via
config.ts - Creates the
Applicationinstance - Registers the
tabby://custom protocol for loading plugin resources - Handles second-instance detection: if another instance is already running, forwards CLI args and exits
- Configures Electron security settings (CSP headers, webPreferences)
- On
app.ready, creates the first window and processes any CLI arguments
// Simplified startup sequence (pseudocode from source)
setupSentry()
const config = loadConfig()
const application = new Application(config)
// Custom protocol for plugin resource loading
protocol.registerFileProtocol('tabby', (request, callback) => {
// Maps tabby://plugin-name/resource/path → actual filesystem path
const pluginPath = resolvePluginPath(request.url)
callback({ path: pluginPath })
})
// Single instance enforcement
if (!app.requestSingleInstanceLock()) {
app.quit() // Another instance is already running
}
app.on('second-instance', (event, argv) => {
application.handleSecondInstance(argv)
})
app.ts — Application Class
The central orchestrator for the main process. This is the largest and most important class in the main process.
| Method/Property | Purpose |
|---|---|
constructor(config) |
Initializes tray, global hotkeys, plugin manager IPC handlers, GPU settings |
newWindow() |
Creates a new Window instance and adds it to the window list |
toggleWindow() |
Shows/hides the focused window (used by docking toggle hotkey) |
setupTray() |
Creates system tray icon with context menu (Windows/macOS only) |
setupGlobalHotkey() |
Registers the global toggle-window hotkey via Electron globalShortcut |
handleSecondInstance(argv) |
Receives CLI args from a second instance, forwards to active window |
windows: Window[] |
Active window list |
pluginManager |
Plugin install/uninstall IPC bridge to renderer |
Platform-specific handling:
- Wayland detection: Checks
XDG_SESSION_TYPEandWAYLAND_DISPLAYfor Wayland sessions. On Wayland, global hotkey registration may behave differently depending on the compositor. - KDE Plasma detection: KDE Plasma has its own global shortcut system. The Application class detects KDE and warns the user if global hotkey registration fails.
- GPU settings: Applies Electron command-line switches (
--disable-gpu,--disable-software-rasterizer, etc.) based on config and platform.
window.ts — Window Class
Manages a single BrowserWindow instance. Each window is independent with its own tab set.
| Method/Property | Purpose |
|---|---|
constructor(config) |
Creates BrowserWindow with webPreferences, sets up event handlers |
present() |
Shows and focuses the window, bringing it to the foreground |
hide() |
Hides the window (to tray on Windows/macOS, or minimizes on Linux) |
toggleVisibility() |
Show/hide toggle for docking hotkey |
setBounds(bounds) |
Sets window position and size |
setDocking(position) |
Applies docking configuration (repositions to screen edge) |
setVibrancy(enabled) |
Enables/disables window transparency and blur effects |
isVisible |
Current visibility state |
focused |
Whether window has keyboard focus |
Frameless window internals:
frame: falseremoves the OS title bartitleBarStyle: 'hidden'on macOS preserves traffic lights but hides the title- Window Controls Overlay (WCO) on Windows provides native minimize/maximize/close buttons within the custom title bar region
- Glasstron (
glasstronnpm package) on Windows provides Aero blur/acrylic/mica effects behind the window
Dark mode handling: Subscribes to Electron's nativeTheme.on('updated') event to react to OS dark/light mode changes, updating the window's theme accordingly.
config.ts — Config File I/O
Handles reading and writing the YAML config file in the main process.
| Function | Purpose |
|---|---|
loadConfig() |
Reads config.yaml via fs.readFileSync, parses with js-yaml, returns plain object |
saveConfig(data) |
1. Backs up current config to .backup 2. Writes new config atomically via atomically |
migrateConfig(data) |
Detects Terminus (old project name) config directory and migrates on first launch |
Atomic write sequence (via atomically library):
- Write data to a temporary file in the same directory
fsyncthe temporary file to ensure data reaches disk- Atomically rename temporary file to
config.yaml - If rename fails (e.g., cross-device), falls back to write-in-place
Error handling: If config.yaml is corrupted (invalid YAML), the loader falls back to the .backup file. If both are corrupted, a fresh default config is generated.
pty.ts — PTY Manager
Manages pseudo-terminal processes via node-pty. This is performance-critical code.
| Class | Purpose |
|---|---|
PTYManager |
Spawns and tracks all PTY instances; handles IPC channels from renderer |
PTY |
Individual PTY wrapper with flow control, UTF-8 splitting, and lifecycle management |
Flow control implementation (detailed):
class PTY {
private buffer: Buffer[] = []
private bufferSize = 0
private paused = false
private static MAX_CHUNK = 100 * 1024 // 100 KB per IPC message
private static MAX_DELTA = 500 * 1024 // 500 KB buffer before pause
// Called when node-pty emits data
onData(data: Buffer) {
this.buffer.push(data)
this.bufferSize += data.length
if (this.bufferSize > PTY.MAX_DELTA) {
this.pty.pause() // Back-pressure: stop reading from PTY fd
this.paused = true
}
this.flush() // Send up to MAX_CHUNK to renderer
}
private flush() {
while (this.buffer.length > 0) {
const chunk = this.extractChunk(PTY.MAX_CHUNK)
this.sendToRenderer(chunk)
}
}
// Called when renderer acknowledges receipt
onAck(bytes: number) {
this.bufferSize -= bytes
if (this.paused && this.bufferSize < PTY.MAX_DELTA) {
this.pty.resume()
this.paused = false
}
}
}
UTF-8 splitting: Before sending data to the renderer, utfSplitter.ts ensures the chunk does not end in the middle of a multi-byte UTF-8 sequence. If it does, the trailing bytes are held back and prepended to the next chunk.
cli.ts — CLI Parser
Uses yargs to parse command-line arguments. Defines the top-level commands:
| Command | Options | Purpose |
|---|---|---|
open |
--profile, --directory |
Opens a new tab with the specified profile or directory |
run |
-- <command> |
Runs a command in a new tab |
connect |
<url> |
Connects using a URL (ssh://, telnet://, serial://) |
profiles |
(none) | Lists available profiles |
settings |
(none) | Opens the settings UI |
Plugins extend the CLI through CLIHandler providers in the renderer process. The main process forwards unrecognized commands to the renderer for plugin handling.
pluginManager.ts — Plugin Manager (Main Process)
Manages npm-based plugin installation in the main process.
| Method | Purpose |
|---|---|
install(packageName) |
Runs bundled npm binary to install a plugin to {userData}/plugins/ |
uninstall(packageName) |
Removes a plugin package from the plugins directory |
list() |
Reads {userData}/plugins/node_modules/ to list installed plugins |
The plugin manager bundles its own npm binary (not the system npm) to ensure consistent behavior regardless of the user's Node.js/npm installation. IPC is used to communicate between the renderer (UI) and main process (npm operations).
portable.ts — Portable Mode Detection
Detection logic:
- Check for
PORTABLE_EXECUTABLE_FILEenvironment variable - Check for a
data/directory adjacent to the executable (process.execPath) - If either is found, redirect
app.getPath('userData')andapp.getPath('appData')todata/
sentry.ts — Error Reporting
Sets up the Sentry SDK for the main process:
- Captures unhandled exceptions via
process.on('uncaughtException') - Captures unhandled promise rejections via
process.on('unhandledRejection') - Tags events with app version, platform, and architecture
- Respects
enableAnalytics: false— Sentry is not initialized when disabled
lru.ts — LRU Cache
A simple least-recently-used cache with a fixed size. Used to memoize fs.realpathSync calls:
class LRUCache<K, V> {
private cache = new Map<K, V>()
private maxSize: number
get(key: K): V | undefined
set(key: K, value: V): void
// Evicts least recently accessed entry when maxSize is exceeded
}
utfSplitter.ts — UTF-8 Byte Stream Splitter
Analyzes the trailing bytes of a Buffer to determine if they form an incomplete UTF-8 sequence:
- 0xxxxxxx — single byte, always complete
- 110xxxxx — 2-byte sequence, needs 1 continuation
- 1110xxxx — 3-byte sequence, needs 2 continuations
- 11110xxx — 4-byte sequence, needs 3 continuations
- 10xxxxxx — continuation byte
If the buffer ends with an incomplete sequence, those bytes are held back and prepended to the next data event.
8.1.2 tabby-core/src/ — Core Framework
The foundation that all other plugins depend on. Contains no platform-specific code — everything here works on Electron and web platforms.
services/config.service.ts — ConfigService
The most critical service in the entire application. Manages all configuration state.
| Member | Type | Purpose |
|---|---|---|
store |
ConfigProxy |
Reactive config object with default merging (read/write) |
changed$ |
ReplaySubject<void> |
Emits on every config mutation |
ready$ |
AsyncSubject<void> |
Completes once all config providers have contributed defaults |
save() |
async method |
Serializes diff-only config to YAML and writes to disk |
requestRestart() |
method | Prompts user to restart the application |
enabledServices |
Set<string> |
Services currently enabled (not in providerBlacklist) |
restoreDefaults() |
method | Resets all config to defaults |
ConfigProxy — reactive config access:
The ConfigProxy class wraps the raw config object with a JavaScript Proxy. This is the mechanism that makes config access "just work" — you read/write properties normally, and the proxy handles default merging, change notification, and diff-only storage:
// Reading a value:
const lines = config.store.terminal.scrollbackLines
// → If stored: returns stored value
// → If not stored: returns default from ConfigProvider contributions
// → If nested object: returns another ConfigProxy (recursive)
// Writing a value:
config.store.terminal.scrollbackLines = 50000
// → If value equals default: deletes from store (diff-only)
// → If value differs from default: stores in config
// → Triggers changed$.next()
// The deepmerge strategy for arrays is "replace" (not "append"):
// This prevents duplicate array entries when defaults change between versions
Config version migrations: The version field in config.yaml tracks the config schema version. When Tabby loads a config with an older version, migration functions update the structure to the current format.
services/app.service.ts — AppService
Manages the tab lifecycle, window state, and application-level operations.
| Member | Purpose |
|---|---|
tabs: BaseTabComponent[] |
All open tabs in the current window |
activeTab: BaseTabComponent |
Currently focused tab |
openNewTab(type, inputs) |
Opens a new tab of the specified Angular component type |
selectTab(tab) |
Focuses a tab, deselecting the previous one |
closeTab(tab) |
Closes a tab, destroys its component, triggers tabClosed$ |
splitTab(tab, direction) |
Wraps a tab in a SplitTabComponent and adds a new pane |
duplicateTab(tab) |
Creates a new tab with the same profile and options |
explodeTab(splitTab, child) |
Promotes a split pane child to a standalone tab |
combineTabsIntoSplit(tabs) |
Combines multiple standalone tabs into one split view |
tabsChanged$ |
Emits when tabs are added, removed, or reordered |
tabOpened$ |
Emits when a new tab opens (with the tab as payload) |
tabClosed$ |
Emits when a tab closes (with the tab as payload) |
activeTabChanged$ |
Emits on focus change (with the new active tab) |
ready$ |
Completes when the Angular UI is fully initialized |
hostWindow |
Reference to the platform window service |
SplitTabComponent architecture:
SplitTabComponent implements a recursive split container:
SplitTabComponent (root)
├── SplitContainer (horizontal)
│ ├── TerminalTabComponent (left, 50%)
│ └── SplitContainer (vertical)
│ ├── SSHTerminalTabComponent (top-right, 25%)
│ └── TerminalTabComponent (bottom-right, 25%)
Split operations:
- Split right: Creates a horizontal container with the current tab on the left and a new tab on the right
- Split bottom: Creates a vertical container with the current tab on top and a new tab on the bottom
- Resize: Drag the divider between panes to adjust ratios
- Focus navigation: Cycle between panes with keyboard shortcuts
services/profiles.service.ts — ProfilesService
| Member | Purpose |
|---|---|
profiles |
All configured profiles (user + built-in) |
profileGroups |
Profile group hierarchy (UUID-based) |
recentProfiles |
Recently used profiles (default: last 3) |
getProviders() |
Collects all ProfileProvider implementations from DI |
openProfile(profile) |
Opens a new tab for the given profile |
quickConnect(query) |
Parses quick-connect string and opens a connection |
importProfiles() |
Triggers profile import from all SSHProfileImporter providers |
getBuiltinProfiles() |
Gets auto-detected profiles (shells, imported SSH configs) |
getProfileDefaults(type) |
Gets default options for a profile type |
Quick-connect string parsing:
user@host→ SSH profile with host and useruser@host:port→ SSH profile with host, user, and portssh://user@host:port→ SSH profile (explicit protocol)host:port→ Telnet profiletelnet://host:port→ Telnet profile (explicit protocol)
services/hotkeys.service.ts — HotkeysService
Collects hotkey registrations from all HotkeyProvider implementations and dispatches key events:
| Member | Purpose |
|---|---|
hotkey$ |
Observable<string> — emits the hotkey ID when a registered key combination is pressed |
getHotkeyDescription(id) |
Returns human-readable description for a hotkey |
isEnabled(id) |
Whether a hotkey has at least one binding |
globalHotkey$ |
Emits for global (system-wide) hotkeys |
The service listens for keydown events on the document, matches against registered accelerators, and emits on hotkey$.
services/vault.service.ts — VaultService
| Member | Purpose |
|---|---|
isEnabled: boolean |
Whether vault is configured (has a master passphrase) |
isLocked: boolean |
Whether vault contents are currently encrypted in memory |
lock() |
Zeroes the decryption key and encrypted-state cache |
unlock(passphrase: string) |
Derives key from passphrase, decrypts vault blob |
setSecret(key, value) |
Stores a key-value pair in the encrypted vault |
getSecret(key): string |
Retrieves a decrypted secret by key |
removeSecret(key) |
Deletes a secret |
setEnabled(enabled, passphrase) |
Enables/disables vault, migrating secrets |
changePassphrase(oldPass, newPass) |
Re-encrypts vault with new passphrase |
services/commands.service.ts — CommandService
The command palette (Ctrl+Shift+P) system:
| Member | Purpose |
|---|---|
commands |
Flattened list of all commands from all CommandProvider implementations |
run(command) |
Executes a command's action function |
getCommands() |
Collects, deduplicates, and filters commands (respects commandBlacklist) |
lastCommand |
Most recently executed command (for "repeat last command") |
services/themes.service.ts — ThemesService
Manages application-level themes (distinct from terminal color schemes):
| Member | Purpose |
|---|---|
theme |
Current theme name |
themes |
Available themes |
setTheme(name) |
Applies a theme (CSS class changes) |
services/locale.service.ts — LocaleService
Manages i18n translations:
| Member | Purpose |
|---|---|
currentLocale |
Currently active locale code |
availableLocales |
All available locale codes |
setLocale(code) |
Switches the active locale, triggers retranslation |
services/selector.service.ts — SelectorService
Provides reusable UI selector/picker dialogs:
- Profile picker (with group hierarchy)
- Color scheme picker (with preview)
- Generic option selector (used throughout the codebase for any selection UI)
services/tabRecovery.service.ts — TabRecoveryService
| Member | Purpose |
|---|---|
saveTabs(tabs) |
Serializes tab state using TabRecoveryProvider implementations |
recoverTabs() |
Deserializes saved state, recreates tabs |
saveInterval |
30 seconds |
debounceTime |
1 second after changes |
closedTabHistory |
Last 5 closed tabs (for "reopen closed tab") |
services/fileProviders.service.ts — FileProvidersService
Aggregates FileProvider implementations:
| Member | Purpose |
|---|---|
readFile(path) |
Reads a file via the platform-appropriate provider |
writeFile(path, data) |
Writes a file atomically |
pickFile(options) |
Shows a file picker dialog |
pickDirectory() |
Shows a directory picker dialog |
services/homeBase.service.ts — HomeBaseService
Handles communication with Tabby's optional home base service:
- Sends anonymous usage statistics (if
enableAnalytics: true) - Checks for available updates
- Retrieves plugin recommendations
api/ — Public API Types
The tabby-core/src/api/ directory exports the TypeScript interfaces, abstract classes, and tokens that define the extension point contracts. This is the stable public API for plugin development:
| Export | Type | Purpose |
|---|---|---|
ConfigProvider |
Abstract class | Contribute default config and platform-specific defaults |
HotkeyProvider |
Abstract class | Register hotkey bindings |
ProfileProvider |
Abstract class | Define profile types with create/edit/connect logic |
QuickConnectProfileProvider |
Abstract class | Parse quick-connect strings |
TabRecoveryProvider |
Abstract class | Serialize/deserialize tab state |
TabContextMenuItemProvider |
Abstract class | Right-click menu items for tabs |
ToolbarButtonProvider |
Abstract class | Main toolbar buttons |
CommandProvider |
Abstract class | Command palette entries |
SettingsTabProvider |
Abstract class | Settings UI tabs |
CLIHandler |
Abstract class | CLI subcommand handlers |
ShellProvider |
Abstract class | Local shell detection |
FileProvider |
Abstract class | File I/O abstraction |
BaseTabComponent |
Abstract class | Base class for all tab types |
SplitTabComponent |
Component | Split pane container |
Platform |
Enum | Windows, macOS, Linux, Web |
8.1.3 tabby-electron/src/ — Electron Platform Bindings
Provides platform-specific implementations that require Electron/Node.js APIs. This plugin is loaded only on the desktop platform (not in the web build).
services/docking.service.ts — ElectronDockingService
| Member | Purpose |
|---|---|
dock(position) |
Calculates screen bounds and repositions window to edge |
undock() |
Restores window to previous floating position and size |
toggle() |
Shows/hides the docked window with animation |
handleDisplayChange() |
Re-calculates docking position when monitors are added/removed/resized |
Position calculation: Given dockPosition, dockFill, and dockSpace:
dockFillcontrols how much of the screen edge the window fills (0-1, default 0.5)dockSpacecontrols the usable screen ratio (0-1, default 1 — full screen width/height)- The window is centered along the docked edge
shells/ — Shell Providers (13 implementations)
Each provider implements the ShellProvider interface:
interface ShellProvider {
provide(): Promise<Shell[]>
}
interface Shell {
id: string
name: string
command: string
args: string[]
env: Record<string, string>
icon?: string
}
| Provider | Platform | Detection Method |
|---|---|---|
WindowsDefaultShellProvider |
Windows | Registry HKCU\...\ComSpec |
WindowsStockShellsProvider |
Windows | Known paths: System32\cmd.exe, PowerShell 5.1 |
PowerShellCoreShellProvider |
All | Searches PATH for pwsh / pwsh.exe |
CmderShellProvider |
Windows | Checks CMDER_ROOT environment variable |
Cygwin32ShellProvider |
Windows | Checks C:\cygwin\bin\bash.exe |
Cygwin64ShellProvider |
Windows | Checks C:\cygwin64\bin\bash.exe |
GitBashShellProvider |
Windows | Checks Program Files\Git\bin\bash.exe and Program Files (x86)\Git\bin\bash.exe |
MSYS2ShellProvider |
Windows | Checks C:\msys64\usr\bin\bash.exe and related paths |
WSLShellProvider |
Windows | Runs wsl.exe --list --verbose, parses output for distributions |
VSDevToolsProvider |
Windows | Scans VS 2017/2019/2022 install dirs via vswhere or known paths |
MacOSDefaultShellProvider |
macOS | Runs dscl . -read /Users/$USER UserShell |
LinuxDefaultShellProvider |
Linux | Reads $SHELL env var, falls back to /etc/passwd |
POSIXShellsProvider |
macOS/Linux | Reads /etc/shells and returns all listed shells |
Other Electron-specific services:
| Service | Purpose |
|---|---|
ElectronPlatformService |
Platform detection (OS, arch), app paths, native file dialogs |
ElectronHostAppService |
App lifecycle bridge: quit, restart, minimize, maximize |
ElectronFileProvider |
File read/write using Node.js fs module |
TouchbarService |
macOS Touch Bar: profile buttons, active tab info |
ElectronUpdaterService |
electron-updater integration: check, download, apply updates |
ElectronLogService |
Winston logger setup: file transport + console transport |
8.1.4 tabby-local/src/ — Local Terminal Plugin
services/terminal.service.ts: Creates local terminal tab instances. Resolves the shell executable from the profile, prepares environment variables, and requests PTY spawn from the main process.
services/sessions.service.ts: Tracks active local sessions. Provides session lookup by tab and cleanup on tab close.
Session lifecycle (detailed):
TerminalService.openTab(profile)creates aLocalTerminalTabComponent- The component's
ngOnInitrequests PTY spawn via IPC PTYManagerin the main process spawnsnode-ptywith:- Shell executable from profile or detected default
- Arguments from profile
- Working directory from profile or
$HOME - Environment: merge of
process.env+ profile overrides - Dimensions: current terminal size (cols x rows)
- The
SessionMiddlewareStackis created and attached - PTY output flows: node-pty → IPC → middleware pipeline → xterm.js
- User input flows: xterm.js → middleware pipeline (reverse) → IPC → node-pty
cli.ts: Handles tabby open --directory (opens local terminal in specified directory) and tabby run -- <command> (opens terminal running a specific command).
8.1.5 tabby-terminal/src/ — Terminal Emulation Plugin
Terminal Frontend Architecture:
abstract class Frontend {
// xterm.js Terminal instance wrapper
abstract attach(container: HTMLElement): void
abstract detach(): void
abstract write(data: string): void
abstract clear(): void
abstract resize(cols: number, rows: number): void
abstract getSelection(): string
abstract selectAll(): void
abstract scrollToBottom(): void
// Events
input$: Observable<Buffer> // User keystrokes
resize$: Observable<{cols, rows}>
bell$: Observable<void>
title$: Observable<string> // OSC title changes
}
xterm.js addon loading:
| Addon | Class | Purpose |
|---|---|---|
@xterm/addon-webgl |
WebglAddon |
GPU-accelerated rendering (default) |
@xterm/addon-canvas |
CanvasAddon |
Canvas 2D fallback rendering |
@xterm/addon-search |
SearchAddon |
In-terminal text search (Ctrl+Shift+F) |
@xterm/addon-serialize |
SerializeAddon |
Terminal state serialization (for recovery/clipboard) |
@xterm/addon-unicode11 |
Unicode11Addon |
Correct width calculation for Unicode 11 characters |
@xterm/addon-ligatures |
LigaturesAddon |
Font ligature rendering (if ligatures: true) |
@xterm/addon-image |
ImageAddon |
Sixel, iTerm2 image protocol, Kitty image protocol |
@xterm/addon-clipboard |
ClipboardAddon |
OSC 52 clipboard sequence handling |
middleware/ — Terminal Middleware Pipeline (detailed)
The middleware pipeline is a chain of processors. Each middleware can:
- Transform data flowing to the terminal (output direction)
- Transform data flowing from the terminal (input direction)
- Intercept and suppress data (e.g., login script matching)
| Middleware | Class | Input Transform | Output Transform |
|---|---|---|---|
InputProcessor |
InputProcessor |
Maps backspace key to correct escape code (\x7f or \x08 depending on platform/config) |
None |
LoginScriptProcessor |
LoginScriptProcessor |
None | Watches output for expect patterns; when matched, sends the corresponding response string. Supports regex and literal matching. |
OSCProcessor |
OSCProcessor |
None | Intercepts OSC (Operating System Command) escape sequences for processing (title changes, clipboard operations, custom OSC codes) |
TerminalStreamProcessor |
TerminalStreamProcessor |
Handles input modes (readline vs. raw) | Translates newline formats (CR, LF, CRLF) based on config |
UTF8SplitterMiddleware |
UTF8SplitterMiddleware |
None | Ensures output data is split at valid UTF-8 code point boundaries |
SessionMiddlewareStack: Chains middleware in order. Data flows through each middleware sequentially:
Output direction (PTY → terminal):
Session.data$ → UTF8Splitter → StreamProcessor → OSCProcessor
→ LoginScriptProcessor → InputProcessor → xterm.js.write()
Input direction (user → PTY):
xterm.js.input$ → InputProcessor → LoginScriptProcessor
→ OSCProcessor → StreamProcessor → UTF8Splitter → Session.write()
Third-party plugins inject middleware via the SessionMiddleware multi-provider token.
Login script example:
options:
scripts:
- expect: "login:"
send: "admin"
isRegex: false
- expect: "Password:"
send: "s3cret"
isRegex: false
- expect: "\\$\\s*$" # Shell prompt
send: "cd /app && ls"
isRegex: true
The LoginScriptProcessor processes scripts sequentially — once a pattern matches and the response is sent, it moves to the next script entry. After all scripts are processed, the middleware becomes a passthrough.
8.1.6 tabby-ssh/src/ — SSH Plugin
The SSH plugin is the most complex plugin in Tabby, implementing a full SSH client via the russh Rust-based native library (v0.1.37).
Core services:
| Service | File | Purpose |
|---|---|---|
SSHService |
services/ssh.service.ts |
Connection orchestration: handshake, auth, channel management |
SSHMultiplexerService |
services/sshMultiplexer.service.ts |
Connection pooling: reuses transports for same host |
SSHKnownHostsService |
services/sshKnownHosts.service.ts |
Host key verification (TOFU + OpenSSH known_hosts) |
PasswordStorageService |
services/passwordStorage.service.ts |
Credential storage bridge (vault/keytar/plaintext) |
SSH session types:
| Class | Purpose | Channel Type |
|---|---|---|
SSHSession |
Interactive shell | session channel with PTY request |
SFTPSession |
File transfer | subsystem channel (sftp) |
ForwardedPort |
Port forwarding | direct-tcpip or forwarded-tcpip channel |
X11Socket |
X11 forwarding | x11 channel |
SSH connection flow (detailed):
1. ProfilesService.openProfile(sshProfile)
│
2. SSHService.createSession(profile)
│
3. Resolve connection target
├── Direct: host:port from profile
├── Jump host: recursively connect to jumpHost profile first
├── SOCKS proxy: connect via SOCKS5 tunnel
├── HTTP proxy: connect via HTTP CONNECT
└── ProxyCommand: spawn command, use stdin/stdout as transport
│
4. Check SSHMultiplexerService for existing transport
├── Found (reuseSession: true): skip to step 7
└── Not found: proceed to step 5
│
5. TCP connection establishment
│
6. russh handshake
├── Version exchange
├── KEX (key exchange) — negotiates shared secret
├── Host key verification
│ ├── SSHKnownHostsService.check(host, port, key)
│ ├── If new: prompt user, store on accept
│ ├── If match: continue
│ └── If mismatch: show warning, user must explicitly override
│
7. Authentication (tries in order until success)
├── Public key (from profile.privateKeys)
│ ├── Try each key in order
│ ├── Decrypt passphrase-protected keys (vault/keytar/prompt)
│ └── AutoPrivateKeyLocator: scan ~/.ssh/id_* if no keys in profile
├── SSH agent (if agent: auto or explicitly configured)
│ └── Forward agent request to local SSH agent
├── Password (from vault/keytar/prompt)
└── Keyboard-interactive (server-driven challenge-response)
│
8. Channel open
├── Shell session: request PTY, request shell
├── SFTP: request subsystem "sftp"
├── Port forward: request direct-tcpip or tcpip-forward
└── X11: accept x11 channel requests
│
9. Attach to TerminalTabComponent
│
10. SessionMiddlewareStack processes I/O
SFTP implementation:
- Full SFTP subsystem client built on
russh - Operations: list, upload, download, rename, delete, mkdir, rmdir, chmod, stat
- Progress tracking for file transfers (bytes transferred, percentage)
- Resumable uploads (not built-in, but possible by seeking to offset)
- Context menu extensibility via
SFTPContextMenuItemProvider
WinSCP integration: When ssh.winSCPPath is configured, an "Open in WinSCP" action is available. SSHService constructs WinSCP command-line arguments:
WinSCP.exe sftp://user:password@host:port/
Password is passed via command line if available (security concern — visible in process list). For better security, use WinSCP's session storage.
Profile import:
SSHProfileImporterimplementations scan for SSH profiles from external sources- OpenSSH config parser reads
~/.ssh/configand maps Host entries to Tabby profiles - Handles
Includedirectives,Matchblocks, and wildcard hosts
SSH algorithm negotiation:
- Default algorithm lists (§7.3) are compiled into the russh native module
- Per-profile overrides are passed to russh during handshake setup
- russh handles the actual cryptographic operations in Rust (not JavaScript)
8.1.7 tabby-serial/src/ — Serial Plugin
Uses serialport npm package v11.0.1 for hardware serial communication.
services/serial.service.ts:
| Member | Purpose |
|---|---|
listPorts() |
Calls SerialPort.list() to enumerate available serial ports |
createSession(profile) |
Opens a serial connection with the specified parameters |
Port enumeration returns:
- Port path (
COM3,/dev/ttyUSB0,/dev/tty.usbserial-*) - Manufacturer (if available via USB descriptors)
- Serial number (if available)
- PnP ID (Windows)
- Location ID (macOS)
Slow-send mode (slowSend: true):
- Sends data one character at a time with a configurable delay between each character
- Useful for devices with small input buffers (microcontrollers, network equipment with limited UART buffers)
- The delay prevents buffer overrun on the receiving device
8.1.8 tabby-telnet/src/ — Telnet Plugin
Provides Telnet protocol and raw TCP socket connections:
| Mode | Default Port | Implementation |
|---|---|---|
| Telnet | 23 | Telnet option negotiation (WILL/WONT/DO/DONT) |
| Raw socket | 1234 | Plain TCP net.Socket, no protocol |
The Telnet implementation handles standard Telnet option negotiation for terminal type, window size, and echo mode.
8.1.9 tabby-settings/src/ — Settings UI Plugin
Provides the settings interface. The settings window is a tab with sub-tabs contributed by plugins via SettingsTabProvider.
Built-in settings tabs:
- General (appearance, behavior)
- Terminal (rendering, scrollback, keyboard)
- Profiles (CRUD for all profile types)
- Hotkeys (visual hotkey editor)
- Vault (enable, lock, change passphrase)
- Config file (raw YAML editor with syntax highlighting)
services/ within tabby-settings:
| Service | Purpose |
|---|---|
HotkeySettingsService |
Hotkey binding capture and validation |
ProfileSettingsService |
Profile creation, editing, deletion, import |
VaultSettingsService |
Vault enable/disable, lock/unlock, passphrase change |
ConfigSyncService |
Config export (download as file) and import (upload file) |
8.1.10 tabby-plugin-manager/src/ — Plugin Manager Plugin
services/pluginManager.service.ts:
| Member | Purpose |
|---|---|
installedPlugins |
Array of installed third-party plugins with metadata |
availablePlugins |
Array of plugins from npm registry search |
install(name, version?) |
Installs a plugin via bundled npm (IPC to main process) |
uninstall(name) |
Removes a plugin package |
update(name) |
Updates a plugin to the latest version |
isInstalled(name) |
Checks if a plugin is installed |
checkForUpdates() |
Compares installed versions against npm registry |
Plugin search: Queries the npm registry for packages with the tabby-plugin keyword. Results include package name, description, version, download count, and author.
Plugin compatibility: Plugins declare their compatible Tabby version via peerDependencies in package.json. The plugin manager warns (but does not prevent) installation of incompatible plugins.
8.1.11 tabby-linkifier/src/ — Link Detection Plugin
Detects clickable patterns in terminal output using TerminalDecorator:
- URLs: Matches
http://,https://,ftp://followed by valid URL characters - IP addresses: IPv4 dotted-decimal notation, IPv6 with brackets
- File paths: Platform-specific path patterns (absolute paths)
When a pattern is detected, xterm.js renders it as a clickable link. Clicking opens the URL in the default browser or the file in the default application.
8.1.12 tabby-community-color-schemes/ — Color Schemes
Contains 150+ terminal color schemes in the schemes/ directory. Each scheme is defined as a TypeScript object with:
{
name: 'Dracula',
foreground: '#f8f8f2',
background: '#282a36',
cursor: '#f8f8f2',
selection: '#44475a',
colors: [
'#21222c', // black
'#ff5555', // red
'#50fa7b', // green
'#f1fa8c', // yellow
'#bd93f9', // blue
'#ff79c6', // magenta
'#8be9fd', // cyan
'#f8f8f2', // white
'#6272a4', // bright black
'#ff6e6e', // bright red
'#69ff94', // bright green
'#ffffa5', // bright yellow
'#d6acff', // bright blue
'#ff92df', // bright magenta
'#a4ffff', // bright cyan
'#ffffff', // bright white
],
}
Registered via TerminalColorSchemeProvider. Users can also define custom color schemes in the settings.
8.1.13 tabby-auto-sudo-password/src/ — Auto-Sudo Plugin
Detects sudo password prompts in terminal output via regex pattern matching. When a match is found:
- Retrieves the stored password from vault/keytar
- Sends the password followed by a newline to the terminal session
- Clears the password from memory
The detection pattern matches common sudo prompt formats across Linux distributions.
8.1.14 tabby-uac/ — Windows UAC Helper
A C++ native module (UAC/) that provides UAC (User Account Control) elevation on Windows. The module uses Windows API calls (ShellExecuteExW with runas verb) to request elevation. Used when Tabby needs to perform operations requiring administrator privileges.
8.1.15 tabby-web/src/ — Web Platform Plugin
Provides platform service implementations for running Tabby in a web browser without Electron. Replaces:
ElectronPlatformService→WebPlatformServiceElectronHostAppService→WebHostAppServiceElectronFileProvider→WebFileProvider(using File API)
Limitations in web mode: no local terminal (no PTY), no native SSH (no russh), no serial ports, no system tray, no global hotkeys, no auto-updates.
8.1.16 tabby-web-demo/src/ — Demo Plugin
Provides a fully browser-based demo using the v86 JavaScript x86 emulator. Ships with a minimal Linux image in data/ that boots inside the browser. This allows users to try Tabby's terminal emulation without installing anything.
8.2 Class Hierarchy
The codebase contains 158 classes (per AST extraction). The key inheritance hierarchies:
BaseTabComponent (abstract — tabby-core)
│ Base class for all tab types. Provides lifecycle hooks,
│ activity tracking, title management, and destruction.
│
├── TerminalTabComponent (tabby-terminal)
│ │ Base for terminal-based tabs. Manages xterm.js instance,
│ │ session attachment, middleware stack, and resize handling.
│ │
│ ├── LocalTerminalTabComponent (tabby-local)
│ │ Local PTY terminal. Spawns shell process via node-pty.
│ │
│ ├── SSHTerminalTabComponent (tabby-ssh)
│ │ SSH terminal. Manages SSHSession lifecycle and SFTP access.
│ │
│ ├── SerialTerminalTabComponent (tabby-serial)
│ │ Serial terminal. Manages serialport connection.
│ │
│ └── TelnetTerminalTabComponent (tabby-telnet)
│ Telnet/raw socket terminal.
│
└── SplitTabComponent (tabby-core)
Container for split pane layouts. Recursively contains
other BaseTabComponent instances in horizontal/vertical splits.
BaseSession (abstract — tabby-terminal)
│ Base for session types. Provides data$, closed$, write(), resize().
│
├── LocalSession (tabby-local)
│ Wraps node-pty PTY. Handles spawn, write, resize, kill.
│
├── SSHSession (tabby-ssh)
│ Wraps russh SSH channel. Handles auth, shell, exec, env.
│
├── SerialSession (tabby-serial)
│ Wraps serialport SerialPort. Handles open, write, close.
│
└── TelnetSession (tabby-telnet)
Wraps net.Socket. Handles connect, write, close.
SessionMiddleware (abstract — tabby-terminal)
│ Base for I/O middleware. Provides feedFromSession(), feedFromTerminal().
│
├── InputProcessor — Backspace/delete mapping
├── LoginScriptProcessor — Expect/send automation
├── OSCProcessor — OSC escape sequence interception
├── TerminalStreamProcessor — I/O mode and newline translation
└── UTF8SplitterMiddleware — UTF-8 boundary alignment
ConfigProvider (abstract — tabby-core)
│ Contributes config defaults and platform-specific defaults.
│ One implementation per plugin.
│
├── CoreConfigProvider (tabby-core)
├── ElectronConfigProvider (tabby-electron)
├── TerminalConfigProvider (tabby-terminal)
├── SSHConfigProvider (tabby-ssh)
├── SerialConfigProvider (tabby-serial)
├── TelnetConfigProvider (tabby-telnet)
└── ... (one per plugin)
ProfileProvider (abstract — tabby-core)
│ Defines a connection profile type.
│
├── LocalProfileProvider (tabby-local)
├── SSHProfileProvider (tabby-ssh)
├── SerialProfileProvider (tabby-serial)
└── TelnetProfileProvider (tabby-telnet)
ShellProvider (abstract — tabby-core)
│ Detects available shells on the platform.
│
├── WindowsDefaultShellProvider
├── WindowsStockShellsProvider
├── PowerShellCoreShellProvider
├── CmderShellProvider
├── Cygwin32ShellProvider / Cygwin64ShellProvider
├── GitBashShellProvider
├── MSYS2ShellProvider
├── WSLShellProvider
├── VSDevToolsProvider
├── MacOSDefaultShellProvider
├── LinuxDefaultShellProvider
└── POSIXShellsProvider
8.3 Code Patterns
Angular Dependency Injection
All services use Angular's @Injectable() decorator. Multi-providers use multi: true to allow multiple implementations of the same token:
// Registering extension points in a plugin module
@NgModule({
providers: [
// Single provider
{ provide: MyService, useClass: MyServiceImpl },
// Multi-provider (multiple plugins can contribute)
{ provide: ConfigProvider, useClass: MyConfigProvider, multi: true },
{ provide: HotkeyProvider, useClass: MyHotkeyProvider, multi: true },
{ provide: ProfileProvider, useClass: MyProfileProvider, multi: true },
{ provide: CommandProvider, useClass: MyCommandProvider, multi: true },
{ provide: TerminalColorSchemeProvider, useClass: MyColorSchemes, multi: true },
],
})
export default class MyPluginModule {}
Observable Patterns (RxJS)
The codebase uses RxJS extensively for reactive state management and event handling:
// Config change reaction
this.configService.changed$.pipe(
debounceTime(500)
).subscribe(() => {
this.applySettings()
})
// Tab lifecycle events
this.appService.tabOpened$.subscribe(tab => {
if (tab instanceof TerminalTabComponent) {
this.attachDecorator(tab)
}
})
// Session data streaming
this.session.data$.subscribe(data => {
this.frontend.write(data)
})
// Cleanup
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe())
}
Async Subjects for Readiness
Services that require asynchronous initialization use AsyncSubject to signal readiness:
class ConfigService {
ready$ = new AsyncSubject<void>()
async init() {
await this.loadProviders()
await this.loadConfig()
this.mergeDefaults()
this.ready$.next()
this.ready$.complete() // Signals readiness to all subscribers
}
}
// Consumer waits for readiness:
await firstValueFrom(this.configService.ready$)
// or
this.configService.ready$.subscribe(() => {
// Config is now available
})
Atomic File Operations
All config writes use the atomically library to prevent corruption:
import { writeFile } from 'atomically'
// This writes to a temp file, fsyncs, then atomically renames
await writeFile(configPath, yaml.dump(data))
Platform Abstraction Pattern
Platform-specific code is isolated behind abstract services. The Electron and Web plugins provide different implementations:
// Abstract service (tabby-core — no platform dependency)
@Injectable()
abstract class PlatformService {
abstract getConfigPath(): string
abstract showOpenDialog(options): Promise<string[]>
abstract showSaveDialog(options): Promise<string>
abstract getOSRelease(): string
}
// Electron implementation (tabby-electron)
@Injectable()
class ElectronPlatformService extends PlatformService {
getConfigPath() {
return path.join(electron.app.getPath('userData'), 'config.yaml')
}
showOpenDialog(options) {
return electron.dialog.showOpenDialog(options)
}
}
// Web implementation (tabby-web)
@Injectable()
class WebPlatformService extends PlatformService {
getConfigPath() {
return '/virtual/config.yaml' // IndexedDB-backed
}
showOpenDialog(options) {
// Uses File API
}
}
Module Export Convention
Every plugin exports its Angular module as the default export:
// tabby-my-plugin/src/index.ts
@NgModule({
imports: [CommonModule, FormsModule],
declarations: [MyComponent],
providers: [...],
})
export default class MyPluginModule {}
The plugin loader expects default exports and uses them to register the module with Angular's injector.
8.4 Error Handling Patterns
Key error conditions and their handling:
| Error Message | Source | Context | Handling |
|---|---|---|---|
"No private keys in profile" |
SSH auth | No keys configured and none auto-discovered | Falls back to password auth or prompts user |
"WinSCP not found" |
SFTP UI | ssh.winSCPPath not set or invalid |
Shows notification suggesting WinSCP download |
"Cannot save an empty store" |
ConfigService | Prevents accidental config wipe from bugs | Write is silently skipped |
"Vault not configured" |
VaultService | Operation requires vault but vault: null |
Prompts user to set up vault passphrase |
"Vault is locked" |
VaultService | Operation requires decrypted vault | Prompts for vault passphrase |
"Frontend not ready" |
TerminalTab | xterm.js not yet initialized when data arrives | Buffers data until frontend is ready |
"Did not find any bootstrap components" |
Angular bootstrap | Plugin loading failed catastrophically | Fatal error — app cannot start |
Error reporting flow:
- Unhandled errors in main process → captured by Sentry (if enabled)
- Unhandled errors in renderer → caught by Angular's
ErrorHandler, logged to console, optionally sent to Sentry - Session errors (SSH disconnect, serial port close) → caught by session layer, surfaced as notifications via
ngx-toastr - Config errors (invalid YAML) → fallback to backup, log warning
8.5 Testing Strategy
The project uses ESLint for static analysis and TypeScript's compiler for type checking:
# Run ESLint across all packages
yarn run lint
# TypeScript type checking (implicit in build)
yarn run build
No unit test framework (Jest, Mocha, etc.) or test files were identified in the source tree. The project's testing approach relies on:
- TypeScript strict mode: Catches type errors at compile time
- ESLint: Enforces code quality and consistency rules
- Angular compiler: Validates templates and dependency injection at build time
- Manual testing: Features are tested manually across platforms
- Plugin isolation: The plugin architecture provides natural boundaries — changes in one plugin typically do not affect others
- Community testing: As an open-source project with a large user base, issues are quickly reported
Recommended testing approach for contributors:
- Test the specific feature/plugin being modified on at least one platform
- Cross-platform testing for changes that touch platform-specific code
- Build all plugins to verify no compilation errors are introduced
- Run
yarn run lintto ensure code style compliance - Test with multiple xterm.js frontends (webgl + canvas) for rendering changes
- For SSH changes: test with various server implementations (OpenSSH, Dropbear) and key types
8.6 Contributing Guide
Setting Up the Development Environment
# 1. Prerequisites
# - Node.js 22.x
# - Yarn 1.x (npm install -g yarn)
# - Rust toolchain (rustup.rs)
# - Platform build tools (see §2.3)
# 2. Clone the repository
git clone https://github.com/Eugeny/tabby.git
cd tabby
# 3. Install all dependencies (workspace-aware)
yarn install
# 4. Rebuild native modules for Electron's Node.js ABI
npx @electron/rebuild
# 5. Build all plugins and the main app
yarn run build
# 6. Start in development mode
TABBY_DEV=1 yarn run start
# On Windows: set TABBY_DEV=1 && yarn run start
Development Workflow
# Terminal 1: Watch mode (rebuilds on file changes)
yarn run watch
# Terminal 2: Start Tabby (restart manually after watch rebuilds)
yarn run start
The watch command runs webpack in watch mode for all plugin packages simultaneously. When a TypeScript/Angular source file changes, the affected plugin is rebuilt incrementally (typically 1-5 seconds).
Hot reload is not supported — after a rebuild, you must restart the Electron app to load the new code. Press Ctrl+R in the app window to reload the renderer process (faster than full restart, but does not reload main process changes).
Creating a New Plugin
- Create directory and initialize:
mkdir tabby-my-feature
cd tabby-my-feature
- Create
package.json:
{
"name": "tabby-my-feature",
"version": "1.0.0",
"description": "My Tabby plugin",
"keywords": ["tabby-plugin"],
"main": "dist/index.js",
"peerDependencies": {
"tabby-core": "*",
"tabby-terminal": "*"
},
"devDependencies": {
"typescript": "^4.9.5"
}
}
-
Create
webpack.config.mjs(copy fromtabby-linkifier/webpack.config.mjsas a template — it is one of the simpler plugins) -
Create
src/index.ts:
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ConfigProvider, CommandProvider } from 'tabby-core'
@Injectable()
class MyConfigProvider extends ConfigProvider {
defaults = {
myFeature: {
enabled: true,
},
}
}
@Injectable()
class MyCommandProvider extends CommandProvider {
async getCommands() {
return [{
id: 'my-feature:do-something',
label: 'My Feature: Do Something',
run: async () => {
// Command implementation
},
}]
}
}
@NgModule({
imports: [CommonModule],
providers: [
{ provide: ConfigProvider, useClass: MyConfigProvider, multi: true },
{ provide: CommandProvider, useClass: MyCommandProvider, multi: true },
],
})
export default class MyFeatureModule {}
- Add to workspace in root
package.json:
{
"workspaces": {
"packages": [
"...",
"tabby-my-feature"
]
}
}
- Build and test:
yarn install
yarn run build
yarn run start
Code Style Requirements
- TypeScript strict mode (
"strict": trueintsconfig.json) - 4-space indentation (no tabs)
- Single quotes for strings
- Trailing commas in multi-line constructs
- Angular component naming:
*Componentfor components,*Servicefor services,*Providerfor DI providers - File naming:
kebab-case.tsfor files,PascalCasefor classes - ESLint enforced — run
yarn run lintbefore submitting PRs - No
anytypes unless absolutely necessary (and documented why)
Internationalization for Plugin Developers
Making plugin strings translatable:
import { TranslateService } from '@ngx-translate/core'
@Injectable()
export class MyService {
constructor(private translate: TranslateService) {}
getLabel(): string {
return this.translate.instant('My translatable string')
}
}
In Angular templates:
<span translate>My translatable string</span>
<!-- or -->
<span>{{ 'My translatable string' | translate }}</span>
After adding new strings, run yarn run i18n:extract to update the .po files.
9. Integration Points
9.1 OpenSSH Config Import
Tabby imports SSH profiles from OpenSSH's ~/.ssh/config file via SSHProfileImporter. Supported directives:
| Directive | Mapped To | Notes |
|---|---|---|
Host |
Profile name | Wildcard patterns are expanded |
HostName |
options.host |
|
Port |
options.port |
Default: 22 |
User |
options.user |
Default: root |
IdentityFile |
options.privateKeys |
~ is expanded to home directory |
ProxyJump |
options.jumpHost |
Resolved to Tabby profile UUID |
ForwardAgent |
options.agentForward |
|
LocalForward |
options.forwardedPorts (local) |
|
RemoteForward |
options.forwardedPorts (remote) |
|
DynamicForward |
options.forwardedPorts (dynamic) |
|
ServerAliveInterval |
options.keepaliveInterval |
Converted from seconds to milliseconds |
ServerAliveCountMax |
options.keepaliveCountMax |
|
Include |
Recursive file inclusion | Supports globs |
Unsupported directives are silently ignored. The import is one-way — changes in Tabby do not write back to ~/.ssh/config.
9.2 WinSCP Integration
For graphical SFTP file management on Windows:
ssh:
winSCPPath: "C:\\Program Files (x86)\\WinSCP\\WinSCP.exe"
When configured, an "Open in WinSCP" button appears in SSH terminal tabs. SSHService constructs WinSCP command-line arguments to pass host, port, username, and optionally password/key path.
9.3 Cmder Integration
When Cmder is detected (via CMDER_ROOT environment variable), CmderShellProvider:
- Registers Cmder as an available shell
- Points to Cmder's
vendor/init.batfor shell initialization - Supports both cmd and PowerShell Cmder profiles
- Clink integration is preserved through Cmder's init scripts
9.4 WSL Integration
WSLShellProvider discovers installed WSL distributions:
# Internal enumeration command
wsl.exe --list --verbose
Each distribution appears as a separate shell profile with:
- Name: distribution name (e.g., "Ubuntu-22.04")
- Command:
wsl.exe -d <distro-name> - Both WSL 1 and WSL 2 distributions are supported
9.5 VS Developer Tools Integration
VSDevToolsProvider detects Visual Studio installations (2017, 2019, 2022) and offers:
- Developer Command Prompt: cmd.exe with VS build tools in PATH
- Developer PowerShell: PowerShell with VS build tools in PATH
Detection uses vswhere.exe (bundled with VS Build Tools) or scans known installation directories.
9.6 Clink Integration
The extras/clink/ directory contains Clink scripts that enhance cmd.exe when launched through Tabby:
- Readline-style line editing (arrow keys, Ctrl+R history search)
- Tab completion improvements
- History persistence
- Prompt customization hooks
9.7 macOS Automator Workflows
extras/automator-workflows/ contains macOS Automator workflows:
- Open Tabby Here: Right-click a folder in Finder → "Open Tabby Here" opens a terminal in that directory
- Integration with Finder's Services menu
9.8 System Tray Integration
On Windows and macOS:
- Left-click: Toggle window visibility (useful with docking)
- Right-click context menu:
- Profile quick-launch (top N profiles as menu items)
- Show/hide window
- Quit Tabby
The tray icon is not available on Linux due to inconsistent tray support across desktop environments (particularly Wayland compositors).
9.9 Touch Bar (macOS)
TouchbarService provides macOS Touch Bar integration:
- Profile quick-launch buttons (configurable which profiles appear)
- Active tab indicator
- Connection status (connected/disconnected) for SSH/serial tabs
9.10 Third-Party Plugin Ecosystem
The plugin manager searches npm for packages with the tabby-plugin keyword. The ecosystem includes plugins for:
- Additional terminal color schemes and themes
- Enhanced search and filtering
- Workspace/session management
- Snippet managers and text expansion
- Custom shell integrations
- Notification integrations
Plugin API stability commitment: The extension points defined in tabby-core/src/api/ form the stable public API. The multi-provider tokens (ConfigProvider, ProfileProvider, CommandProvider, etc.) are maintained for backward compatibility across minor versions. Internal service APIs may change between versions without notice.
9.11 Troubleshooting
Application Will Not Start (Windows)
Symptom: Tabby fails to launch, or shows "Cannot create process error 267."
Resolution:
- Check the log file at
%APPDATA%\tabby\log.txtfor error details - Verify no other Tabby instance is running (Task Manager → look for
tabby.exe) - Try command-line launch to see console output:
tabby.exe --no-sandbox - Reset config: rename
%APPDATA%\tabby\config.yamltoconfig.yaml.bakand restart - If error 267 ("The directory name is invalid"): a profile's
cwdpoints to a directory that no longer exists. Editconfig.yamland remove or fix invalidcwdvalues.
Note
Error 267 occurs when Windows cannot create a process in the specified directory. This typically happens when a profile's working directory has been deleted or a network drive is disconnected. Editing
config.yamlto remove the offendingcwdvalue (or setting it to an empty string for the home directory) resolves the issue.
SSH Key Authentication Failures
Symptom: "No private keys in profile" error or authentication fails despite correct keys.
Resolution:
- Verify the key file path in the profile is absolute and the file exists at that path
- Check key file permissions:
chmod 600 ~/.ssh/id_*(Linux/macOS); on Windows, ensure only the user has read access - If using an SSH agent, verify it is running and the key is loaded:
ssh-add -l - On macOS, use the full path (
/Users/name/.ssh/id_ed25519), not~/.ssh/id_ed25519— tilde expansion may not work in all contexts - For CA-signed keys, specify both the certificate (
.pubwith certificate) and the private key - Try enabling verbose SSH logging in the developer console to see the exact auth failure
Tip
The
AutoPrivateKeyLocatorautomatically scans~/.ssh/for files namedid_*(excluding.pubfiles) and offers them during connection. If your key has a non-standard name, add its path explicitly to the profile'sprivateKeysarray.
Crashes on Ubuntu 24.04
Symptom: Tabby crashes on startup on Ubuntu 24.04 or later.
Resolution:
- Launch with GPU disabled:
tabby --disable-gpu - If running Wayland, try X11 mode:
tabby --ozone-platform=x11 - Install required libraries:
sudo apt install libsecret-1-0 libx11-dev - Check for conflicting Electron applications or GPU driver issues
- For Snap installs:
snap refresh tabbyto ensure latest version
High Memory Consumption
Symptom: Tabby uses excessive RAM with many tabs open.
Resolution:
- Reduce scrollback buffer: set
terminal.scrollbackLinesto 5000-10000 (default is 25000) - Disable ligatures if not used:
terminal.ligatures: false - Switch renderer:
terminal.frontend: xterm-canvasto reduce GPU memory - Disable image protocol:
terminal.sixel: false - Close tabs that are no longer needed — each tab maintains a full xterm.js instance with its own scrollback buffer
- Check for plugins that may be accumulating state
Stuck on "CMD (clink)" on Windows
Symptom: Terminal hangs when opening cmd.exe with Clink active.
Resolution:
- Update Clink to the latest version
- If issue persists, temporarily disable Clink by removing it from the Cmder/cmd profile
- Use PowerShell or Git Bash as an alternative
- Check Clink's log files for errors
SSH Auto-Disconnect Through Jump Server
Symptom: SSH connections through a jump server disconnect after periods of inactivity.
Caution
Setting
keepaliveIntervalbelow 1000ms can trigger rate limiting on some SSH servers. Values of 3000-5000ms are generally safe. If connections still drop, the issue is likely a NAT timeout between you and the jump server (common with corporate firewalls — typical timeout is 5-15 minutes).
Resolution:
- Reduce keepalive interval and increase count:
options: keepaliveInterval: 3000 # 3 seconds keepaliveCountMax: 20 # More retries before giving up - Configure keepalive on the jump server's sshd_config:
ClientAliveInterval 30 - If behind a NAT/firewall with short timeouts, set keepalive interval below the NAT timeout
- Consider using
tmuxorscreenon the remote side for session persistence
Hyperlink Detection Issues
Symptom: Links in terminal output include trailing semicolons or other punctuation, or are not detected at all.
Resolution: This is a known limitation of the URL regex pattern in the linkifier. URLs followed by certain punctuation characters may include the punctuation in the detected URL. Workarounds:
- Manually select and copy the URL
- Add a space after URLs in scripts to ensure clean detection
- Monitor the upstream issue tracker for regex improvements