Tabby SSH Login Scripts System: Architecture and Implementation Guide
Tabby implements a four-layer SSH login scripts system that uses a lightweight LoginScriptProcessor middleware to automatically respond to remote prompts and execute commands during SSH sessions.
Tabby's SSH login scripts system provides an extensible automation framework for handling authentication prompts and post-login commands in terminal sessions. Implemented across the Eugeny/tabby repository, this feature combines TypeScript data models, Angular UI components, and session middleware to enable automatic interaction with remote servers through configurable expect/send pairs.
Architecture Overview
The system is organized into four distinct layers that handle everything from data definition to runtime execution:
- Data Model – Defines the
LoginScriptinterface and storage structure intabby-terminal/src/middleware/loginScriptProcessing.ts - UI Settings – Provides the profile editing interface via
LoginScriptsSettingsComponentintabby-terminal/src/components/loginScriptsSettings.component.ts - Profile Plumbing – Persists scripts in
SSHProfileOptionswithintabby-ssh/src/api/interfaces.tsand manages save operations intabby-ssh/src/components/sshProfileSettings.component.ts - Session Integration – Executes scripts during live connections through
BaseSessionintabby-terminal/src/session.tsandSSHShellSessionintabby-ssh/src/session/shell.ts
Data Model and Profile Storage
The foundation of the system rests on two core interfaces defined in tabby-terminal/src/middleware/loginScriptProcessing.ts. The LoginScript interface describes a single automation rule containing an expect string (or regex), a send payload, and optional flags like isRegex and optional. The LoginScriptsOptions interface groups these into a scripts array.
In tabby-ssh/src/api/interfaces.ts, the SSHProfileOptions interface extends LoginScriptsOptions, ensuring every SSH profile can store its own automation scripts directly in the profile JSON:
{
"type": "ssh",
"name": "production-server",
"options": {
"host": "example.com",
"user": "admin",
"scripts": [
{ "expect": "password:", "send": "s3cr3t", "isRegex": false },
{ "expect": "Two-factor code:", "send": "123456", "isRegex": false, "optional": true },
{ "expect": "", "send": "cd /var/log", "isRegex": false }
]
}
}
Unconditional scripts—those with an empty expect field—execute immediately after the shell channel opens, while conditional scripts wait for their trigger patterns in the incoming data stream.
User Interface Components
Profile editing happens through LoginScriptsSettingsComponent in tabby-terminal/src/components/loginScriptsSettings.component.ts, which renders a table interface for adding, deleting, and reordering scripts. This component embeds into the SSH profile editor via a ViewChild reference.
When users click Save, the SSHProfileSettingsComponent.save() method in tabby-ssh/src/components/sshProfileSettings.component.ts forwards the script data to the child component, ensuring the scripts array persists within the profile's options object.
Session Integration and Middleware Execution
The runtime execution relies on the abstract BaseSession class in tabby-terminal/src/session.ts, which provides the setLoginScriptsOptions() method. This method instantiates a LoginScriptProcessor middleware and injects it into the session's middleware stack.
For SSH connections, SSHShellSession in tabby-ssh/src/session/shell.ts invokes this.setLoginScriptsOptions(this.profile.options) within its constructor, automatically attaching the processor to every new SSH session. The processor operates through two primary mechanisms:
Conditional Script Matching
The feedFromSession() method intercepts incoming terminal data and scans for expect patterns. When matched, it transmits the send string via this.outputToSession.next() and removes the script from the queue:
// Simplified logic from LoginScriptProcessor
for (const script of this.remainingScripts) {
const match = script.isRegex
? new RegExp(script.expect, 'g').test(dataString)
: dataString.includes(script.expect);
if (match) {
this.outputToSession.next(Buffer.from(script.send + '\n'));
this.remainingScripts = this.remainingScripts.filter(x => x !== script);
} else if (script.optional) {
this.remainingScripts = this.remainingScripts.filter(x => x !== script);
} else {
break; // Halt processing until required script matches
}
}
Unconditional Script Execution
After the shell channel opens, SSHShellSession.start() calls loginScriptProcessor?.executeUnconditionalScripts(), immediately transmitting any scripts with empty expect fields—ideal for running initialization commands like export LANG=en_US.UTF-8 or cd /project.
Implementation Examples
Adding Scripts Programmatically
You can manipulate login scripts through the ProfileService API:
import { ProfileService } from 'tabby-core';
import { SSHProfile } from 'tabby-ssh';
async function addOTPLoginScript(profileId: string) {
const profile = await ProfileService.getProfileById<SSHProfile>(profileId);
profile.options.scripts.push({
expect: 'Enter OTP:',
send: '987654',
optional: true,
isRegex: false
});
await ProfileService.saveProfile(profile);
}
Extending to Custom Protocols
Any terminal type extending BaseSession inherits login script support by calling setLoginScriptsOptions():
class CustomShellSession extends BaseSession {
constructor(profile: CustomProfile) {
super();
this.setLoginScriptsOptions(profile.options);
}
}
This pattern works for SSH, Telnet, Serial, or custom protocols, as the processor operates on the abstract session level without blocking the main terminal I/O.
Summary
- Four-layer architecture separates data definitions, UI components, profile persistence, and runtime execution across
tabby-terminalandtabby-sshmodules - Profile-based storage stores
LoginScriptarrays inSSHProfileOptions, persisting automation rules as JSON within each connection profile - Middleware integration attaches a
LoginScriptProcessorto everySSHShellSessionviaBaseSession.setLoginScriptsOptions(), monitoringfeedFromSession()for trigger patterns - Dual execution modes handle conditional prompts (passwords, 2FA codes) and unconditional post-login commands through the same API
- Protocol agnostic design allows any session type extending
BaseSessionto leverage the same automation engine
Frequently Asked Questions
What is the difference between conditional and unconditional login scripts?
Conditional scripts specify an expect string that the processor watches for in incoming data—used for passwords or two-factor prompts. Unconditional scripts leave expect empty and execute immediately when the shell channel opens via executeUnconditionalScripts(), making them suitable for initialization commands like directory changes or environment variable exports.
How does Tabby handle regex patterns in login scripts?
When isRegex: true is set on a LoginScript object, the processor in tabby-terminal/src/middleware/loginScriptProcessing.ts instantiates a RegExp object with the global flag and tests incoming data against it. Plain string matches use String.prototype.includes() instead. Regex patterns are useful for handling dynamic prompts that vary by server or session state.
Can login scripts be used for terminal protocols other than SSH?
Yes. The LoginScriptProcessor middleware is implemented in the abstract BaseSession class (tabby-terminal/src/session.ts), making it available to any session type. As long as the concrete session class calls setLoginScriptsOptions() during initialization—like SSHShellSession does for SSH connections—the script engine will function for Telnet, Serial, or custom protocol implementations.
What happens if an expected prompt never appears?
If a script is marked as optional: true, the processor removes it from the queue after processing subsequent data without a match, allowing the session to continue. Non-optional scripts block further script execution until matched, but they do not block the terminal session itself—the user can still interact manually while the processor waits for the pattern.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →