GETSSH Logo
GETSSH

Plugin SDK Documentation

Comprehensive guide to developing secure, zero-trust extensions for GETSSH.

GETSSH Plugin Developer SDK Documentation

Welcome to the GETSSH Plugin SDK! This document is the complete reference manual for third-party developers, covering plugin type selection, Manifest specifications, API interfaces, security models, and the publishing process.


Table of Contents

  1. Plugin Types Overview
  2. Manifest Specification (package.json)
  3. Sandbox Plugin
  4. Backend Plugin (Node.js)
  5. Security Sandbox Model & Escape Defense
  6. RASP Lifecycle Integration (Mandatory)
  7. System Monitoring Data Stream (sysmon)
  8. UI Extension Points (Native Context Menus)
  9. Complete Example: Hello World Sandbox Plugin
  10. Complete Example: Backend Node.js Plugin
  11. Packaging and Installation
  12. FAQ and Errors

1. Plugin Types Overview

The GETSSH plugin system supports two entirely different plugin types. Please read their privilege boundaries carefully before choosing:

FeatureSandbox Plugin (sandbox)Backend Plugin (Node.js)
Main Entryindex.html (Pure Frontend)main.js (Runs in Main Process)
Node.js Privileges❌ Strictly Prohibited✅ Access limited by VM Sandbox
Access electronAPI❌ Strictly Prohibited✅ Injected via ctx context
File System Access (fs)❌ Strictly Prohibited⛔ Prohibited in Strict Mode, Available in Normal Mode
Network Access (net)❌ Strictly Prohibited⛔ Prohibited in Strict Mode
Mandatory Lifecycle Hooks✅ ExemptedMUST implement deactivate()
Use CasesData display, status monitoring, read-only UI panelsSSH auditing, automation scripts, encrypted storage integration

Strongly Recommended to Prioritize Sandbox Plugins. Sandbox plugins cannot be maliciously exploited and are unaffected by security sandbox mode switches. Users inherently trust them more.


2. Manifest Specification (package.json)

Every plugin must include a package.json file in its root directory.

Complete Field Description

{
  "name": "my-awesome-plugin",
  "version": "1.0.0",
  "displayName": "My Awesome Plugin",
  "description": "A one-sentence description of your plugin.",
  "author": "Your Name <email@example.com>",
  "main": "main.js",
  "getssh": {
    "pluginId": "com.example.my-awesome-plugin",
    "type": "sandbox",
    "capabilities": ["lifecycle"]
  }
}

Field Breakdown

FieldTypeRequiredDescription
namestringUnique identifier (lowercase + hyphens). Used as the installation directory name.
versionstringSemantic version number, e.g., 1.0.0.
displayNamestringFriendly name displayed in the GETSSH plugin marketplace and settings.
descriptionstringShort description shown in the plugin list.
authorstringRecommendedAuthor information.
mainstringThe main entry point. Use index.html for Sandbox plugins, and main.js for Backend plugins.
getssh.pluginIdstringGlobally unique reverse-domain ID, e.g., com.example.myplugin. Duplicates are absolutely prohibited.
getssh.type"sandbox"Sandbox onlyDeclares as a sandbox plugin. Backend plugins must NOT include this field.
getssh.capabilitiesstring[]Backend onlyBackend plugins must include "lifecycle". Installation will be rejected without this declaration.

Name Resolution Priority: When parsing the plugin name, GETSSH degrades gracefully in this order: getssh.namedisplayNamename.


3. Sandbox Plugin

How it Works

The HTML file of a sandbox plugin is loaded into a strictly restricted <iframe>. This iframe uses the sandbox="allow-scripts" attribute, which means:

  • Absolutely NO allow-same-origin: The iframe's origin is null, making it impossible to read any DOM, Cookies, or localStorage of the host app.
  • Absolutely NO Node.js Environment: require, process, and window.electronAPI do not exist.
  • The Only Communication Channel: Calling the GETSSH-injected window.GETSSH SDK via postMessage.

Injected window.GETSSH SDK

Before loading your plugin code, GETSSH automatically injects the following SDK object into the sandbox:

window.GETSSH = {
  /**
   * Registers a clickable icon button in the sidebar.
   * @param id      Unique ID for the button (unique within your plugin)
   * @param icon    SVG string (automatically sanitized; malicious scripts stripped)
   * @param label   Label text shown on hover
   */
  registerSidebarAction(id: string, icon: string, label: string): void;

  /**
   * Pops up a system notification (requires user permission).
   * @param title Notification title
   * @param body  Notification body
   */
  showNotification(title: string, body: string): void;

  /**
   * Synchronously reads the host app's current locale (e.g., 'zh-CN', 'en-US').
   * This is a snapshot and will not update automatically. To listen for changes, use onThemeChange.
   */
  getLocale(): string;

  /**
   * Subscribes to host app theme change events.
   * Triggered whenever the user switches between dark/light/system modes in settings.
   * @param callback Receives the new theme value: 'dark' | 'light' | 'system'
   */
  onThemeChange(callback: (theme: 'dark' | 'light' | 'system') => void): void;
}

/**
 * Handler dictionary for sidebar button click events.
 * The key MUST match the id provided in registerSidebarAction.
 */
window.__sidebarHandlers: Record<string, () => void>;

Theme & Locale Awareness Example

You can use these two APIs to perfectly synchronize your plugin UI with the host app:

// Synchronously read locale settings on startup
const locale = window.GETSSH.getLocale();
document.getElementById('greeting').textContent =
  locale.startsWith('zh') ? '你好,世界!' : 'Hello, World!';

// Respond to theme switches in real-time
window.GETSSH.onThemeChange((theme) => {
  document.body.setAttribute('data-theme', theme);
  // Example: Update CSS variables, chart color schemes, etc.
});

Receiving Messages from the Host App

The host application may push data to your plugin via postMessage. You simply need to listen for the message event:

window.addEventListener('message', (event) => {
  // ALWAYS verify the message type to avoid processing irrelevant messages
  if (event.data.type === 'sysmon:data') {
    // See Section 7: System Monitoring Data Stream
    const { cpus, mem, net } = event.data.payload;
  }
});

PluginBridge Message Interceptor (Whitelist Mechanism)

All postMessage requests sent from the sandbox to the host must pass through the PluginBridge whitelist verification. Only the following actions are permitted:

ActionDescription
registerSidebarActionRegisters an icon button in the sidebar
registerPanelRegisters a panel page
showNotificationTriggers a desktop system notification
getActiveSessionIdFetches the current active SSH session ID (Always returns null. Plugins cannot access real IDs for security reasons)

The following actions are PERMANENTLY intercepted and will trigger a security warning log:

Intercepted ActionReason
sshWritePlugins are forbidden from directly writing to SSH terminals
sshConnect / sshDisconnectPlugins are forbidden from controlling connection lifecycles
saveProfiles / unlockProfilesPlugins are forbidden from accessing encrypted connection profiles
sftpWriteFile / sftpDeletePlugins are forbidden from modifying or deleting files via SFTP

Security Warning: Any malicious backend plugin that attempts to bypass the backend lifecycle validation by declaring "type": "sandbox" will be completely stripped of execution rights. When the GETSSH main process detects the sandbox declaration, it immediately skips loading any backend JS code. Any malicious code hidden inside main.js has absolutely no chance of execution.


4. Backend Plugin (Node.js)

How it Works

The main.js of a backend plugin executes within an isolated sandbox environment created by the Node.js vm module inside the Electron Main Process.

The activate(ctx) Context API

When the plugin is activated, the activate function receives a ctx object. This is your ONLY legitimate API entry point:

interface MainContextAPI {
  /**
   * @deprecated Please use ctx.host.notify() for new plugins.
   * Pops up a native desktop notification.
   */
  showNotification(title: string, body: string): void;

  /**
   * Encrypts a string using Electron's OS-level encryption capabilities.
   * The key is managed by the OS Keychain and is bound to the current user account.
   */
  safeStorageEncrypt(text: string): string;

  /**
   * Listens to SSH session connection events (Read-only).
   * Triggered whenever the user successfully establishes a new SSH connection.
   * @param callback sessionId is the internal GETSSH session ID, host is the target hostname
   */
  onSSHSessionConnect?(callback: (sessionId: string, host: string) => void): void;

  /**
   * Persistent Key-Value storage, providing isolated namespaces for each plugin.
   */
  storage: {
    get(key: string): Promise<any>;
    set(key: string, value: any): Promise<void>;
    delete(key: string): Promise<void>;
    clear(): Promise<void>;
  };

  /**
   * Two-way RPC communication bridge between the Backend VM and Frontend iframe plugins.
   */
  rpc: {
    /** Registers a method that the frontend can call via pluginRpcInvoke(). */
    registerMethod(method: string, handler: (payload: any) => Promise<any>): void;
    /** Proactively pushes data to the frontend, which listens via onPluginRpcMessage(). */
    sendToFrontend(payload: any): void;
  };

  /**
   * Native Host Integration APIs.
   * ♥️ All dialog invocations leave a [Plugin Host API] audit trail in the main process logs.
   */
  host: {
    /**
     * Sends an OS-native desktop notification directly from the backend plugin.
     * The notification pops up regardless of what interface the user is looking at. Ideal for server monitoring/alerting plugins.
     * @param title  Notification title
     * @param body   Notification body
     * @param type   Visual intent: 'info' (default) | 'warning' | 'error'
     */
    notify(title: string, body: string, type?: 'info' | 'warning' | 'error'): void;

    /**
     * Pops up an OS-native message/confirmation dialog.
     * Returns a Promise containing the index of the clicked button.
     * @param options.type        Dialog icon type: 'none' | 'info' | 'warning' | 'error' | 'question'
     * @param options.buttons     Array of button texts, e.g., ['OK', 'Cancel']
     * @param options.message     Main message text (displayed in bold)
     * @param options.detail      Secondary text (smaller font, optional)
     * @param options.defaultId   Index of the button to focus by default
     * @param options.cancelId    Index of the button treated as clicked when Escape is pressed
     * @param options.checkboxLabel Bottom checkbox text (optional)
     * @returns { response: number (clicked button index), checkboxChecked: boolean }
     */
    showMessageBox(options: {
      type?: 'none' | 'info' | 'warning' | 'error' | 'question';
      buttons?: string[];
      defaultId?: number;
      cancelId?: number;
      title?: string;
      message: string;
      detail?: string;
      checkboxLabel?: string;
    }): Promise<{ response: number; checkboxChecked: boolean }>;

    /**
     * Pops up an OS-native file/directory picker.
     * Security Guarantee: The plugin ONLY receives the file path strings and CANNOT read the file contents directly.
     * @param options.properties  Selection mode: 'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles'
     * @param options.filters     File type filters, e.g., [{ name: 'Images', extensions: ['png', 'jpg'] }]
     * @returns { canceled: boolean, filePaths: string[] }
     */
    showOpenDialog(options: {
      title?: string;
      defaultPath?: string;
      filters?: { name: string; extensions: string[] }[];
      properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles'>;
    }): Promise<{ canceled: boolean; filePaths: string[] }>;

    /**
     * Pops up an OS-native file save path picker.
     * @param options.filters File type filters
     * @returns { canceled: boolean, filePath?: string }
     */
    showSaveDialog(options: {
      title?: string;
      defaultPath?: string;
      filters?: { name: string; extensions: string[] }[];
    }): Promise<{ canceled: boolean; filePath?: string }>;
  };

  /**
   * SSH I/O Communication Bridge (Requires 'ssh:read' and 'ssh:write' in capabilities).
   */
  ssh?: {
    onData(sessionId: string, callback: (chunk: string) => void): void;
    write(sessionId: string, command: string): void;
  };

  /**
   * UI Extension Points — Inject custom menu items into native context menus, or register plugin settings.
   * See Section 8 for details.
   */
  ui: {
    registerTerminalContextMenu(actionId: string, label: string, handler: (context: { sessionId: string, selectionText: string }) => void): void;
    registerSFTPContextMenu(actionId: string, label: string, handler: (context: { sessionId: string, currentPath: string, selectedFiles: string[] }) => void): void;
    
    /**
     * [MANDATORY REQUIREMENT] Register the plugin's parameter configuration Schema.
     * All backend plugins MUST call this method exactly once during activate().
     * Your plugin MUST provide at least one configuration parameter. Passing an empty array `[]` is strictly prohibited, and will be intercepted on the spot, causing the kernel to reject the plugin.
     */
    registerSettings(schema: PluginSettingsSchema[]): void;
  };
}

VM Sandbox Security Levels

The execution privileges of backend plugins are governed by the security mode chosen by the user in GETSSH settings:

Security Moderequire() PrivilegesUse Case
Strict Mode (strict)Only path and os allowedMaximum security, limited distribution
Normal Mode (normal)Blocks dangerous modules like fs, child_process, netStandard plugin development
Developer Mode (developer)Full native require, completely unrestrictedFOR DEVELOPMENT ONLY. DO NOT distribute for production.

ctx.host Usage Examples

// ① Show a confirmation dialog and wait for user response
const result = await ctx.host.showMessageBox({
  type: 'warning',
  title: 'Action Confirmation',
  message: 'Are you sure you want to delete this configuration file?',
  detail: 'This action cannot be undone.',
  buttons: ['Delete', 'Cancel'],
  defaultId: 1,    // Focus "Cancel" by default
  cancelId: 1,
});
if (result.response === 0) {
  // User clicked "Delete" (Index 0)
}

// ② Pop up a file picker to let the user select a configuration file
const open = await ctx.host.showOpenDialog({
  title: 'Select Configuration File',
  filters: [{ name: 'JSON Config', extensions: ['json'] }],
  properties: ['openFile'],
});
if (!open.canceled) {
  const configPath = open.filePaths[0];
  // Process the path using ctx.storage or other controlled APIs...
}

// ③ Pop up a file save dialog to let the user choose an export path
const save = await ctx.host.showSaveDialog({
  title: 'Export Audit Report',
  defaultPath: 'audit-report.csv',
  filters: [{ name: 'CSV', extensions: ['csv'] }],
});
if (!save.canceled && save.filePath) {
  // save.filePath is the full local path chosen by the user
}

Security Notice: showOpenDialog only returns path strings. It never actively reads file contents. The plugin must use existing controlled channels (such as ctx.storage or dedicated streaming APIs) to access the file data. There is no implicit file read privilege escalation.


5. Security Sandbox Model & Escape Defense

Why can't I use the sandbox type to bypass hooks?

This is a very common question: Since sandbox type plugins are exempt from lifecycle validation, couldn't I write a malicious Node.js plugin, falsely declare "type": "sandbox" in package.json, and bypass the checks?

The Answer is: Absolutely Not. The GETSSH security architecture is specifically designed with multi-layered defenses to combat this exact type of deception:

Declares type: "sandbox"
         │
         ▼
[PluginManager] sees the sandbox flag
         │
         ▼
The Node.js loader in the main process executes an immediate 'return',
Reading NO code and executing NO main.js code
         │
         ▼
PluginBridge traps it inside an iframe prison
(sandbox="allow-scripts", NO allow-same-origin)
         │
         ▼
It can ONLY communicate with GETSSH via postMessage
         │
         ▼
PluginBridge Whitelist Interceptor:
Any operation outside the whitelist → Dropped immediately + Security Log Alert

Conclusion: Falsely declaring a sandbox plugin equates to voluntarily forfeiting all Node.js backend privileges. Its main.js will never be executed, and inside the iframe, it can only perform limited, read-only UI display. It is a dead end, not a bypass method.

SVG Icon Sanitization

When you register a button with a custom SVG icon via registerSidebarAction, GETSSH automatically sanitizes the SVG code:

  • Strips all dangerous tags like <script>, <iframe>, <foreignObject>, etc.
  • Removes all javascript: URI attributes.
  • The sanitizer uses Set collections for $O(1)$ ultra-fast lookups, completely avoiding UI rendering performance penalties.

6. RASP Lifecycle Integration (Mandatory)

This is the most critical security contract for backend plugins.

GETSSH is undergirded by a Rust-written Watchdog daemon responsible for real-time monitoring of the main process's security state. If the Watchdog detects anomalous behavior (such as malicious API hook injections), it triggers the RASP (Runtime Application Self-Protection) protocol and may forcibly terminate the Electron main process.

Before a forceful kill occurs, GETSSH attempts to execute the deactivate() hook of all plugins to prevent data corruption or resource leaks. Therefore, this hook is NOT optional—it is an integral part of system security.

Dual Mandatory Validation Points

Validation PointTrigger TimingConsequence of Failure
Install Time (Static Scan)When the user installs the .zipInstallation is rejected instantly; the file is never written to disk. The error is shown in the UI.
Load Time (Runtime Check)When the app scans plugin directories on startupThe plugin is skipped and will not run. A warning is written to the console logs.

What MUST be done in deactivate()

let pollingInterval = null;
let openFileHandle = null;

module.exports = {
  activate(ctx) {
    openFileHandle = fs.openSync('/tmp/plugin.log', 'w');
    pollingInterval = setInterval(() => {
      // Periodic operations...
    }, 1000);
  },

  deactivate() {
    // ✅ MANDATORY: Clear all timers
    if (pollingInterval) {
      clearInterval(pollingInterval);
      pollingInterval = null;
    }

    // ✅ MANDATORY: Close all file handles
    if (openFileHandle !== null) {
      fs.closeSync(openFileHandle);
      openFileHandle = null;
    }

    // ✅ MANDATORY: Disconnect all network connections
    // socket.destroy(); socket = null;

    // ✅ MANDATORY: Remove all event listeners
    // emitter.removeAllListeners();
  }
};

The Complete Teardown Flow During RASP Triggers

User selects "Restart into Safe Mode immediately" in the RASP modal
          │
          ▼
SecureCenter.handleAction('restart-safe')
          │
          ▼
① Invokes pluginTeardownFn()
          │
          ▼
② PluginManager.deactivateAll()
   ─ Iterates over all runningPlugins
   ─ Invokes deactivate() in a try/catch block for each plugin
          │
          ▼
③ Dispatches ACTION:RESTART-SAFE to the Watchdog
          │
          ▼
④ app.exit(0)

Manifest Declaration Requirements

Without any of the following, a backend plugin will fail installation:

{
  "name": "my-plugin",
  "version": "1.0.0",
  "displayName": "My Backend Plugin",
  "description": "A backend plugin example.",
  "main": "main.js",
  "getssh": {
    "pluginId": "com.example.my-backend-plugin",
    "capabilities": ["lifecycle"]
  }
}

Note: Backend plugins MUST NOT populate "type": "sandbox".

Runtime Mandatory Contract: Settings Registration

In addition to deactivate, when executing the activate hook, the plugin MUST register its parameter schema with the system to demonstrate full compatibility with GETSSH's parameter delivery pipeline:

module.exports = {
  activate(ctx) {
    // ✅ MANDATORY: Provide at least one actual configuration parameter!
    ctx.ui.registerSettings([
      { id: 'debugMode', type: 'boolean', label: 'Enable Debug', default: false }
    ]);
    // If this is omitted or an empty array is passed, GETSSH will intercept the plugin startup, throw an exception, and destroy it immediately.
  },
  deactivate() {
    // Actual resource release logic
  }
}

7. System Monitoring Data Stream (sysmon)

If your sandbox plugin needs to display real-time system status (CPU, memory, network), GETSSH automatically pushes system data to every active plugin iframe via postMessage.

Data Source: Driven by the getssh-sysprobe N-API extension written in Rust, leveraging the sysinfo library under the hood. CPU utilization is pre-calculated on the Rust side, meaning you do NOT need to perform any delta calculations in JS.

Data Structure

// Listen for this message in your sandbox plugin HTML/JS:
window.addEventListener('message', (event) => {
  if (event.data.type !== 'sysmon:data') return;

  const payload: SysmonPayload = event.data.payload;
});

interface SysmonPayload {
  cpus: {
    overall: number;   // Global CPU utilization, range 0-100
    cores: number[];   // Array of independent core utilization, range 0-100
  };
  mem: {
    total: number;     // Total memory (bytes)
    used: number;      // Used memory (bytes)
    free: number;      // Free memory (bytes)
  };
  net: {
    rx: number;        // Bytes received since last tick
    tx: number;        // Bytes transmitted since last tick
  };
}

Usage Example

<!-- index.html -->
<div id="cpu">--</div>
<div id="mem">--</div>
<script>
window.addEventListener('message', (e) => {
  if (e.data.type !== 'sysmon:data') return;
  const { cpus, mem } = e.data.payload;
  document.getElementById('cpu').textContent =
    'CPU: ' + cpus.overall.toFixed(1) + '%';
  document.getElementById('mem').textContent =
    'MEM: ' + (mem.used / 1024 / 1024 / 1024).toFixed(1) + ' GB';
});
</script>

8. UI Extension Points (Native Context Menus)

Backend Node.js plugins can inject custom menu items into the OS-native context menus of the Terminal View and SFTP File Manager without writing a single line of frontend code.

How it Works

  1. The plugin calls ctx.ui.registerTerminalContextMenu or ctx.ui.registerSFTPContextMenu inside activate().
  2. The GETSSH Main Process broadcasts the sync-plugin-ui-extensions event to the React Frontend, which updates its state tree in real-time.
  3. When the user right-clicks the terminal or SFTP view, the host dynamically constructs a native OS menu incorporating your registered items.
  4. Upon clicking your menu item, the main process invokes your registered callback inside the VM Sandbox, passing precise context data (selected text, file paths, etc.).
  5. When a plugin is uninstalled or reloaded, all of its menu items are instantly garbage-collected, leaving zero phantom menus.

API Reference

// Call inside activate(ctx):

// Inject a menu item into the Terminal right-click context menu
ctx.ui.registerTerminalContextMenu(
  'my-action',       // Unique ID within your plugin
  'Translate Selection', // Text displayed to the user
  (context) => {
    console.log('Session ID:', context.sessionId);
    console.log('User Selected Text:', context.selectionText);
  }
);

// Inject a menu item into the SFTP file list right-click context menu
ctx.ui.registerSFTPContextMenu(
  'preview-file',
  'Preview File',
  (context) => {
    console.log('Current Directory:', context.currentPath);
    console.log('Right-clicked Files:', context.selectedFiles); // string[]
  }
);

Context Data Structures

Menu Typecontext Object Structure
registerTerminalContextMenu{ sessionId: string, selectionText: string }
registerSFTPContextMenu{ sessionId: string, currentPath: string, selectedFiles: string[] }

Note: Menu items registered during activate() remain valid throughout the plugin's lifecycle. You cannot dynamically add or remove individual menu items—any changes require a plugin reload.


9. Complete Example: Hello World Sandbox Plugin

This is the simplest sandbox plugin: it registers a button in the sidebar that pops up a notification when clicked.

Directory Structure

hello-world/
├── package.json
└── index.html

package.json

{
  "name": "hello-world-plugin",
  "version": "1.0.0",
  "displayName": "Hello World",
  "description": "A minimalist GETSSH Sandbox Plugin example.",
  "author": "Your Name",
  "main": "index.html",
  "getssh": {
    "pluginId": "com.example.hello-world",
    "type": "sandbox"
  }
}

index.html

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<script>
// After the sandbox boots, window.GETSSH and window.__sidebarHandlers are automatically injected by GETSSH

const actionId = 'hello-btn';

// 1. Register the button click handler
window.__sidebarHandlers[actionId] = () => {
  window.GETSSH.showNotification('Hello!', 'Greetings from the sandbox plugin.');
};

// 2. Register the button in the sidebar (SVG icons are automatically sanitized)
window.GETSSH.registerSidebarAction(
  actionId,
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
    <circle cx="12" cy="12" r="10" fill="currentColor"/>
  </svg>`,
  'Say Hello'
);
</script>
</body>
</html>

10. Complete Example: Backend Node.js Plugin

This example demonstrates a backend plugin that listens to SSH connection events and writes them to an audit log.

Directory Structure

ssh-auditor/
├── package.json
└── main.js

package.json

{
  "name": "ssh-auditor",
  "version": "1.0.0",
  "displayName": "SSH Audit Logger",
  "description": "Logs all SSH connection events to a local log file.",
  "author": "Your Name",
  "main": "main.js",
  "getssh": {
    "pluginId": "com.example.ssh-auditor",
    "capabilities": ["lifecycle"]
  }
}

main.js

// NOTE: Under Strict Mode (strict), the `fs` module is unavailable.
// This example requires the user to set the security mode to "Normal" or "Developer" to run.
const fs = require('fs');
const os = require('os');
const path = require('path');

const logPath = path.join(os.tmpdir(), 'getssh-audit.log');
let fileStream = null;

module.exports = {
  activate(ctx) {
    fileStream = fs.createWriteStream(logPath, { flags: 'a' });
    fileStream.write(`[${new Date().toISOString()}] SSH Audit Plugin Started\n`);

    ctx.onSSHSessionConnect?.((sessionId, host) => {
      const line = `[${new Date().toISOString()}] Connected to: ${host} (session: ${sessionId})\n`;
      fileStream?.write(line);
    });

    // ⛔ THIS HOOK IS MANDATORY: Declare the configuration parameters for this plugin
    ctx.ui.registerSettings([
      { id: 'logLevel', type: 'string', label: 'Log Level', default: 'info' }
    ]);

    ctx.showNotification('SSH Auditor', `Audit logs are now being recorded to ${logPath}`);
  },

  // ⛔ THIS HOOK IS MANDATORY; the plugin will fail to install without it
  deactivate() {
    if (fileStream) {
      fileStream.write(`[${new Date().toISOString()}] SSH Audit Plugin Stopped\n`);
      fileStream.end();    // ✅ MANDATORY: Close the file stream
      fileStream = null;
    }
  }
};

11. Packaging and Installation

Packaging Rules

Package the plugin directory into a .zip file. Plugin files can reside directly at the .zip root, or wrapped inside a single subdirectory:

# Format A (Recommended): Directly at the root
my-plugin.zip
├── package.json
├── main.js
└── index.html

# Format B (Supported): Wrapped in a subdirectory
my-plugin.zip
└── my-plugin/
    ├── package.json
    ├── main.js
    └── index.html

Warning: GETSSH performs Zip Slip (Directory Traversal) vulnerability checks against all extraction paths. Any .zip archive attempting to extract files outside the designated plugin directory will be rejected immediately.

Installation Method

Inside the GETSSH application: Go to Settings → Plugins → Install Plugin, and select your .zip file.


12. FAQ and Errors

Error during installation: [Security] Plugin installation rejected: ...capabilities...

Cause: The "getssh": { "capabilities": ["lifecycle"] } declaration is missing in the backend plugin's package.json. Solution: Follow the requirements in Section 6 and add the complete getssh field to package.json.

Error during installation: [Security] ... does not export a 'deactivate' lifecycle hook

Cause: GETSSH failed to find the deactivate keyword during static scanning or runtime source analysis of your main.js, or discovered that your deactivate is an empty shell function (e.g., () => {}). Solution: Ensure your main.js exports a deactivate function containing actual resource cleanup logic (disconnecting networks, clearing timers, etc.). Using empty functions to bypass scrutiny is strictly forbidden.

Error during installation: Invalid Architecture: Missing package.json manifest.

Cause: The package.json file cannot be found within the .zip archive, or there are multiple sibling subdirectories inside the .zip. Solution: Ensure package.json is located directly at the .zip root, or within a single solitary subdirectory inside the .zip.

Plugin unresponsive on load, window.GETSSH is undefined

Cause: Your sandbox plugin code executed before the GETSSH SDK injection was complete. Solution: Do not wait for any DOMContentLoaded events; the SDK is injected before scripts are executed. Ensure your scripts are placed in an inline <script> tag inside the <body>, rather than imported via an external src link (external scripts cannot be loaded within the sandbox).

Notifications are not appearing

Cause: Operating system notification permissions have not been granted. Solution: This is governed by the user's OS permissions. showNotification failing silently when permissions are not granted is expected behavior.