# Tabby SSH Login Scripts System: Architecture and Implementation Guide

> Explore Tabby's four-layer SSH login scripts system architecture and implementation. Learn how LoginScriptProcessor middleware automates remote prompts and command execution for efficient SSH sessions.

- Repository: [Eugene/tabby](https://github.com/Eugeny/tabby)
- Tags: architecture
- Published: 2026-03-03

---

**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:

1. **Data Model** – Defines the `LoginScript` interface and storage structure in [`tabby-terminal/src/middleware/loginScriptProcessing.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-terminal/src/middleware/loginScriptProcessing.ts)
2. **UI Settings** – Provides the profile editing interface via `LoginScriptsSettingsComponent` in [`tabby-terminal/src/components/loginScriptsSettings.component.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-terminal/src/components/loginScriptsSettings.component.ts)
3. **Profile Plumbing** – Persists scripts in `SSHProfileOptions` within [`tabby-ssh/src/api/interfaces.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/api/interfaces.ts) and manages save operations in [`tabby-ssh/src/components/sshProfileSettings.component.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-ssh/src/components/sshProfileSettings.component.ts)
4. **Session Integration** – Executes scripts during live connections through `BaseSession` in [`tabby-terminal/src/session.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-terminal/src/session.ts) and `SSHShellSession` in [`tabby-ssh/src/session/shell.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/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:

```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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/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:

```ts
// 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:

```ts
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()`:

```ts
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-terminal` and `tabby-ssh` modules
- **Profile-based storage** stores `LoginScript` arrays in `SSHProfileOptions`, persisting automation rules as JSON within each connection profile
- **Middleware integration** attaches a `LoginScriptProcessor` to every `SSHShellSession` via `BaseSession.setLoginScriptsOptions()`, monitoring `feedFromSession()` 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 `BaseSession` to 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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/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.