3.0 PREVIEW

Notice: GETSSH 3.0 PREVIEW is scheduled for release around October 10, 2026. Capabilities marked as "Coming soon" will be available for early access in this milestone.

SDK

GETSSH Plugin SDK Developer Guide

Coming soon

(This guide is adapted for PLUGIN 2.0; the legacy guide is deprecated and can be traced in the GitHub repository.)

(This guide is adapted for PLUGIN 2.0; the legacy guide is deprecated and can be traced in the GitHub repository.)

中文 | English

Applicable Version: GETSSH Plugin Runtime. This document reflects the current implementation and replaces legacy VM-sandbox SDK documentation.

The GETSSH Plugin SDK is not an npm package that needs to be installed. The host injects controlled APIs when plugins launch: backend code receives context via activate(context), and plugin web pages communicate with the host via window.GETSSH.

1. Choose Your Plugin Architecture

ArchitectureManifestExecution EnvironmentIdeal Use Cases
Pure UI Plugingetssh.type: "sandbox"iframe sandbox="allow-scripts"Panels, dashboards, read-only utilities
Pure Backend PluginOmit getssh.typeIsolated OS sandbox processStorage, SSH extensions, network requests, native dialogs
UI + Backend PluginOmit getssh.type, provide main, renderer, and HTMLUI iframe + isolated backend processVisual interfaces requiring controlled host capabilities

Do not specify "type": "hybrid". Version 3 only recognizes "sandbox"; plugins with backend logic should omit getssh.type.

Pure UI plugins remain fully operational in Safe Mode. Whether backend plugins launch depends on the user-configured backend execution policy:

ModeBackend Behavior
safeDoes not execute any plugin backend code
normalEach plugin executes in an isolated OS sandbox process
strictCurrently shares the same OS-level sandbox boundary as normal; reserved for policy evolution
developerPlugins enter the Electron main process directly, treated as fully trusted code

Isolated processes leverage macOS Seatbelt, Linux bubblewrap, and Windows AppContainer launcher. Plugins cannot directly read private host files, write to host directories, access raw networks, spawn child processes, or instantiate Workers; when host capabilities are required, call the context APIs documented below.

2. Manifest: package.json

Every plugin root directory must include a package.json. Below is a complete example of a UI + Backend plugin:

{
  "name": "hello-getssh",
  "version": "1.0.0",
  "displayName": "Hello GETSSH",
  "description": "GETSSH plugin example",
  "author": "Your Name",
  "main": "main.js",
  "renderer": "renderer.js",
  "getssh": {
    "pluginId": "com.example.hello-getssh",
    "capabilities": ["lifecycle", "storage:default"]
  }
}

Field Reference

FieldRequiredCurrent Semantics
nameYesUnique v3 runtime identity, installation directory name, backend RPC binding namespace, and getssh-plugin:// URL host
versionYesPlugin version; semantic versioning recommended
mainYesHTML path for pure UI plugins; CommonJS JavaScript entry point for backend plugins
displayNameRecommendedDisplay name in user interface
descriptionRecommendedShort plugin summary
authorRecommendedAuthor information
rendererRequired for UIBootstrapping script executed in hidden UI sandbox to register sidebar buttons and panels
getssh.typeRequired for UICurrently the only valid value is "sandbox"; when present, GETSSH completely skips backend loading
getssh.capabilitiesRequired for backendArray of declared backend capabilities; must include "lifecycle"
getssh.nameOptionalDisplay name override with higher precedence than displayName
getssh.pluginIdOptionalCompatibility and marketplace metadata; currently not used for runtime identity binding

The runtime strictly uses name as identity. Do not use getssh.pluginId for panel URL construction, RPC invocation, or storage access. For public plugins, restrict name to lowercase alphanumeric characters and hyphens (e.g., server-health) to avoid case sensitivity discrepancies across Windows, Linux, macOS, and URL hostnames.

Supported Capabilities

CapabilityEffect
lifecycleMandatory for all backend plugins, affirming implementation of deactivate()
storage:defaultExplicit declaration of default storage tier; identical to 5 MiB default quota
storage:extendedElevates plugin KV storage quota to 500 MiB
storage:unlimitedRemoves plugin KV storage quota limit
ssh:readSubscribes to terminal output of designated SSH sessions
ssh:writeWrites commands to designated SSH sessions; requires explicit user approval upon first use
host:clipboardReads and writes system clipboard; reading triggers a user notification
net:fetchAccesses public HTTP/HTTPS endpoints via GETSSH's SSRF-guarded gateway

Declaring capabilities unlocks corresponding host APIs without granting arbitrary Electron, Node.js, or OS-level privileges.

3. Three Supported Directory Structures

3.1 Pure UI Plugin

hello-ui/
├── package.json
├── renderer.js
├── index.html
└── ui.js
{
  "name": "hello-ui",
  "version": "1.0.0",
  "displayName": "Hello UI",
  "description": "Pure UI plugin",
  "main": "index.html",
  "renderer": "renderer.js",
  "getssh": {
    "type": "sandbox"
  }
}

main points to HTML, but is not executed as a Node.js file. Third-party UI plugins must supply renderer.js to register panel entries on startup.

3.2 Pure Backend Plugin

hello-backend/
├── package.json
└── main.js
{
  "name": "hello-backend",
  "version": "1.0.0",
  "displayName": "Hello Backend",
  "description": "Pure backend plugin",
  "main": "main.js",
  "getssh": {
    "capabilities": ["lifecycle", "storage:default"]
  }
}

3.3 UI + Backend Plugin

hello-getssh/
├── package.json
├── main.js
├── renderer.js
├── index.html
└── ui.js

This configuration uses the complete Manifest from Section 2. renderer.js handles UI registration, index.html serves as the visible web view, and main.js exposes backend RPC handlers.

4. Build a UI + Backend Plugin in 5 Minutes

renderer.js: UI Registration Entry

renderer.js executes inside a hidden sandboxed iframe strictly to register UI endpoints. Do not make backend RPC calls here.

const panelId = 'hello-getssh.main';
const actionId = 'open-hello-panel';

window.GETSSH.registerPanel(
  panelId,
  'Hello GETSSH',
  'getssh-plugin://hello-getssh/index.html'
);

window.__sidebarHandlers[actionId] = () => {
  window.GETSSH.openPanel(panelId);
};

window.GETSSH.registerSidebarAction(
  actionId,
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>',
  'Open Hello Plugin'
);

The hostname in getssh-plugin://hello-getssh/ must match name in package.json exactly. Bundled HTML, JavaScript, CSS, JSON, and standard image assets load via the same protocol:

<link rel="stylesheet" href="./style.css">
<script src="./ui.js"></script>

main.js: Registering Backend Capabilities

Backend entry points must be CommonJS modules exporting both activate and deactivate.

let unsubscribe = null;

module.exports = {
  async activate(context) {
    // All backend plugins currently must register 1-256 settings fields.
    context.ui.registerSettings([
      {
        id: 'greeting',
        type: 'string',
        label: 'Greeting Message',
        description: 'Text displayed inside the plugin panel',
        default: 'Hello from GETSSH'
      }
    ]);

    context.rpc.registerMethod('greet', async (payload) => {
      const configured = await context.storage.get('greeting');
      const greeting = configured ?? 'Hello from GETSSH';
      return { message: `${greeting}, ${payload?.name || 'developer'}!` };
    });

    context.ui.registerTerminalContextMenu(
      'remember-selection',
      'Save Selected Text',
      async ({ selectionText }) => {
        await context.storage.set('lastSelection', selectionText);
        context.host.notify('Hello GETSSH', 'Saved terminal selection');
      }
    );
  },

  async deactivate() {
    unsubscribe?.();
    unsubscribe = null;
  }
};

index.html & ui.js: Calling the Backend

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hello GETSSH</title>
</head>
<body>
  <input id="name" value="GETSSH">
  <button id="run">Call Backend</button>
  <pre id="output"></pre>
  <script src="./ui.js"></script>
</body>
</html>
const output = document.querySelector('#output');

document.querySelector('#run').addEventListener('click', async () => {
  try {
    const result = await window.GETSSH.invokeBackend('greet', {
      name: document.querySelector('#name').value
    });
    output.textContent = result.message;
  } catch (error) {
    output.textContent = error instanceof Error ? error.message : String(error);
  }
});

window.GETSSH.onBackendMessage((payload) => {
  console.log('backend event:', payload);
});

window.GETSSH.onThemeChange((theme) => {
  document.documentElement.dataset.theme = theme;
});

If the backend is in safe mode, failed to launch, or did not register the requested method, invokeBackend() rejects. The frontend must always catch exceptions.

5. Backend Lifecycle Rules

interface PluginModule {
  activate(context: MainContextAPI): void | Promise<void>;
  deactivate(): void | Promise<void>;
}
  • activate() timeout: maximum 8 seconds.
  • The backend must register RPCs, terminal context menus, SFTP menus, and settings schemas prior to resolving activate(). The isolation runtime takes a single snapshot upon activation completion; subsequent registrations will not sync to the host.
  • The backend must call context.ui.registerSettings() with at least one field. If the plugin requires no configurations, supply a simple boolean toggle such as enabled.
  • deactivate() should terminate timers, streams, event listeners, and subscriptions. The host allows approximately 2 seconds for shutdown before forcefully killing the plugin process.
  • Forceful termination by the Security Center can kill processes immediately; do not rely solely on deactivate() for data integrity.
  • Use console.log/info/warn/error for logging. Standard output of isolated processes is an internal IPC channel; do not override process.stdout or write raw protocol data to it.

The backend entry point is loaded via require(). Ensure output is CommonJS; if writing in TypeScript, ESM, or utilizing bundlers, compile to CommonJS target.

6. Backend API Reference: context

Boundary data types referenced in signatures:

type StructuredData =
  | null
  | string
  | boolean
  | number
  | StructuredData[]
  | { [key: string]: StructuredData };

6.1 Notifications & System Encryption

interface MainContextAPI {
  showNotification(title: string, body: string): void;
  safeStorageEncrypt(text: string): Promise<string>;
  host: {
    notify(
      title: string,
      body: string,
      type?: 'info' | 'warning' | 'error'
    ): void;
  };
}

showNotification() is a legacy alias; modern plugins should invoke host.notify(). type expresses semantic intent; actual display styling is governed by the host operating system.

safeStorageEncrypt() encrypts strings using Electron's OS-backed safe storage, returning a base64 ciphertext. It is asynchronous and must be awaited. The SDK does not currently expose a decryption API to plugins; do not use it as a general-purpose plugin credential vault.

6.2 Plugin KV Storage

interface PluginStorageAPI {
  get(key: string): Promise<StructuredData | undefined>;
  set(key: string, value: StructuredData): Promise<void>;
  delete(key: string): Promise<void>;
  clear(): Promise<void>;
}

Storage keys are partitioned by Manifest name. Keys can be up to 256 characters. Default quota is 5 MiB, storage:extended allows 500 MiB, and storage:unlimited is unmetered.

Store JSON-serializable structures only: null, string, boolean, finite numbers, arrays, and plain objects. Avoid passing functions, Symbols, BigInts, Dates, Maps, Sets, Buffers, class instances, circular structures, or undefined.

await context.storage.set('cache', { updatedAt: Date.now(), hosts: 3 });
const cache = await context.storage.get('cache');
await context.storage.delete('cache');

6.3 Bidirectional RPC

interface PluginRpcAPI {
  registerMethod(
    method: string,
    handler: (payload: StructuredData | undefined) =>
      StructuredData | undefined | Promise<StructuredData | undefined>
  ): void;
  sendToFrontend(payload: StructuredData): void;
}

RPC method names permit alphanumeric characters, ., _, :, -, length 1-128, and forbid __proto__, prototype, or constructor.

context.rpc.registerMethod('server:list', async ({ group }) => {
  return { group, items: await context.storage.get(`group:${group}`) ?? [] };
});

context.rpc.sendToFrontend({ type: 'sync-complete', count: 12 });

Frontend caller:

const result = await window.GETSSH.invokeBackend('server:list', { group: 'prod' });

window.GETSSH.onBackendMessage((event) => {
  if (event.type === 'sync-complete') console.log(event.count);
});

RPC payload and return values must be structured data under 2 MiB with a maximum nesting depth of 24 levels. Timeout is 15 seconds. Paginate large datasets rather than embedding raw file buffers into single RPC calls.

6.4 Settings Schema

interface PluginSettingsField {
  id: string;
  type: 'string' | 'number' | 'boolean' | 'password';
  label: string;
  description?: string;
  default?: StructuredData;
}

interface PluginSettingsAPI {
  registerSettings(fields: PluginSettingsField[]): void;
}

Rules:

  • Each backend plugin must register between 1 and 256 fields.
  • id follows safe identifier rules and must be unique.
  • label length: 1-256 characters; description max: 4096 characters.
  • When saved by the user, GETSSH stores values under field id in the plugin KV store and reloads the plugin.
  • Retrieve settings via context.storage.get(field.id).
  • password only controls UI input masking; it does not provide separate cryptographic encryption guarantees.
context.ui.registerSettings([
  { id: 'enabled', type: 'boolean', label: 'Enable Plugin', default: true },
  { id: 'endpoint', type: 'string', label: 'API Endpoint', default: 'https://api.example.com' },
  { id: 'interval', type: 'number', label: 'Refresh Interval (s)', default: 30 },
  { id: 'token', type: 'password', label: 'Access Token' }
]);

const enabled = (await context.storage.get('enabled')) ?? true;

6.5 Terminal & SFTP Context Menus

interface PluginContextMenuAPI {
  registerTerminalContextMenu(
    actionId: string,
    label: string,
    handler: (data: {
      sessionId: string;
      selectionText: string;
    }) => unknown | Promise<unknown>
  ): void;

  registerSFTPContextMenu(
    actionId: string,
    label: string,
    handler: (data: {
      sessionId: string;
      currentPath: string;
      selectedFiles: string[];
    }) => unknown | Promise<unknown>
  ): void;
}

actionId adheres to safe identifier conventions with label length 1-256. Maximum 128 registrations per menu category.

context.ui.registerSFTPContextMenu(
  'copy-remote-path',
  'Copy Remote Path',
  async ({ currentPath }) => {
    await context.host.clipboard.writeText(currentPath);
  }
);

The snippet above also requires the host:clipboard capability.

6.6 SSH Data Streams & Command Execution

interface PluginSshAPI {
  onData(
    sessionId: string,
    callback: (chunk: string) => void
  ): () => void;
  write(sessionId: string, command: string): Promise<void>;
}
  • ssh:read unlocks onData().
  • ssh:write unlocks write().
  • UI panels cannot access raw session IDs. The backend extracts sessionId from terminal or SFTP menu invocation payloads.
  • onData() returns an unsubscribe function that must be invoked when done and cleaned up during deactivate().
  • write() accepts up to ~1 MiB per call. First write prompts user confirmation (deny, allow once, allow for this session).
let stopReading = null;

context.ui.registerTerminalContextMenu(
  'watch-output',
  'Monitor Session Output',
  ({ sessionId }) => {
    stopReading?.();
    stopReading = context.ssh.onData(sessionId, (chunk) => {
      console.log('SSH output:', chunk);
    });
  }
);

Only call these methods when capabilities are declared in the Manifest.

6.7 Clipboard & Native Dialogs

interface PluginHostAPI {
  clipboard: {
    writeText(text: string): Promise<void>;
    readText(): Promise<string>;
  };
  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 }>;
  showOpenDialog(options: {
    title?: string;
    defaultPath?: string;
    filters?: Array<{ name: string; extensions: string[] }>;
    properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles'>;
  }): Promise<{ canceled: boolean; filePaths: string[] }>;
  showSaveDialog(options: {
    title?: string;
    defaultPath?: string;
    filters?: Array<{ name: string; extensions: string[] }>;
  }): Promise<{ canceled: boolean; filePath?: string }>;
}

Clipboard methods require host:clipboard. Native dialogs do not require additional capabilities.

File dialogs return selected paths only. In normal/strict modes, this path does not automatically confer filesystem read/write privileges; isolated plugins cannot open files via raw fs. Use controlled file APIs when available.

6.8 Outbound Network Requests: context.net.fetch

Manifest must declare net:fetch:

const response = await context.net.fetch('https://api.example.com/v1/status', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ source: 'getssh' }),
  redirect: 'follow'
});

if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();

Gateway constraints:

  • Only http: and https: schemes allowed (URL max: 8192 characters).
  • Embedded credentials in URLs are blocked.
  • Requests resolving to loopback, private RFC1918, link-local, reserved, or multicast IP addresses are rejected.
  • Maximum 5 redirects followed; cross-origin redirects strip authorization and cookie headers.
  • Request timeout: 30 seconds; maximum response body: 1 MiB.
  • Maximum 128 headers; connection-level headers (host, content-length, connection, transfer-encoding) are host-managed.
  • Supported options: method, headers, body, redirect. Transmit strings for widest cross-mode compatibility.
  • Direct raw sockets, AbortSignal, FormData, Blob, or streaming request bodies are not supported.

Violating network security rules throws SecurityError, triggering the Security Center and terminating the offending plugin. Standard network errors reject with NetworkError.

7. Frontend API Reference: window.GETSSH

Plugin web pages must be loaded by GETSSH via getssh-plugin://<name>/<file>. When opened directly in browsers or from local disk, window.GETSSH is undefined.

API Availability

APIrenderer.js Startup SandboxVisible HTML Panel
registerSidebarActionYesYes
registerPanel / openPanelYesYes
showNotificationYesYes
getLocale / onThemeChangeYesYes
invokeBackend / onBackendMessageNoYes

registerSidebarAction

interface RendererContextAPI {
  registerSidebarAction(id: string, svgIcon: string, label: string): void;
}

SVG strings are sanitized. Pass handler functions by attaching to window.__sidebarHandlers:

window.__sidebarHandlers.open = () => window.GETSSH.openPanel('server-dashboard.main');
window.GETSSH.registerSidebarAction('open', svg, 'Open Dashboard');

registerPanel & openPanel

interface RendererContextAPI {
  registerPanel(panelId: string, title: string, renderUrl: string): void;
  openPanel(panelId: string): void;
}

Always use internal plugin protocol URLs:

window.GETSSH.registerPanel(
  'server-dashboard.main',
  'Server Dashboard',
  'getssh-plugin://server-dashboard/index.html'
);

Register before opening. Format IDs as <plugin-name>.<panel-name> to maintain global uniqueness.

showNotification

interface RendererContextAPI {
  showNotification(title: string, body: string): void;
}

Renders via browser Notification API; requires system permission.

getLocale & onThemeChange

interface RendererContextAPI {
  getLocale(): string;
  onThemeChange(
    callback: (theme: 'dark' | 'light' | 'system') => void
  ): void;
}

getLocale() provides current language snapshot. onThemeChange() notifies of theme transitions.

invokeBackend & onBackendMessage

interface RendererContextAPI {
  invokeBackend(
    method: string,
    payload?: StructuredData
  ): Promise<StructuredData | undefined>;
  onBackendMessage(callback: (payload: StructuredData) => void): void;
}

invokeBackend() automatically routes to the backend sharing the same name as the panel URL. Cross-plugin invocation is barred.

onBackendMessage() receives payloads dispatched via backend context.rpc.sendToFrontend().

Host System Telemetry Events

Plugin panels receive regular periodic telemetry messages:

window.addEventListener('message', (event) => {
  if (event.source !== window.parent) return;
  if (event.data?.type !== 'sysmon:data') return;

  const { cpus, mem, net } = event.data.payload;
  // cpus.overall: number; cpus.cores: number[]
  // mem.total / mem.used / mem.free: bytes
  // net.rx / net.tx: bytes since the latest refresh
});

Verify event.source and type before processing host telemetry.

8. Data, Rate, and Registration Limits

MetricCurrent Limit
Single IPC protocol message2 MiB
Structured data depth24 levels
RPC / host call timeout15 seconds
Activation timeout8 seconds
Registered RPC methods128
Terminal context menu items128
SFTP context menu items128
Settings schema fields1-256
Isolated plugin calls to host250 calls/sec
IPC protocol message throughput500 msgs/sec, 16 MiB/sec
Backend logging throughput64 KiB per execution

These thresholds protect client stability. Throttle high-frequency polling and batch events accordingly.

9. Node.js Execution Rules in Isolated Mode

While backend plugins run as Node.js processes loading local CommonJS modules, OS sandboxing defines strict privilege boundaries:

  • Plugin installation directory is read-only.
  • A randomized private temporary HOME is allocated per run and wiped upon termination.
  • Host user directories, GETSSH userData, system temp directories, and external volumes cannot be directly read or written.
  • Raw network sockets are disabled; use context.net.fetch().
  • Spawning child processes, Workers, POSIX signals, debuggers, and native addons are disallowed.
  • Environment variables inherit a sanitized whitelist only.

Bundle business logic into pure JavaScript and channel all host operations through context. Do not rely on developer mode loopholes (fs, child_process, raw Electron APIs).

Linux Note: Normal and strict modes require bwrap (bubblewrap) installed on the system. If absent, GETSSH refuses to launch backend plugins (pure UI plugins remain operational).

10. Dependencies, Packaging, and Installation

GETSSH does not execute npm install upon installation. Bundles must include all required runtime JavaScript, CSS, and asset dependencies. We recommend compiling backend code into a single CommonJS bundle.

ZIP archives support flat or single top-level directory layouts:

hello-getssh.zip
├── package.json
├── main.js
├── renderer.js
├── index.html
└── ui.js

Or nested inside one directory:

hello-getssh.zip
└── hello-getssh/
    ├── package.json
    └── ...

macOS / Linux packaging command:

cd hello-getssh
zip -r ../hello-getssh.zip . -x '*.DS_Store'

Windows PowerShell packaging command:

Compress-Archive -Path .\hello-getssh\* -DestinationPath .\hello-getssh.zip -Force

Navigate to Settings → Plugins, drag the ZIP archive into the dropzone, inspect requested permissions, and confirm installation.

Internal TypeScript definitions are maintained at apps/getssh-client/src/types/plugin.d.ts. Third-party plugins should treat this guide's signatures as normative.

11. Migrating from Legacy SDK

Legacy Conventionv3 Convention
Backend executes in main process vm.ScriptNormal/strict modes execute in isolated OS sandbox processes
getssh.type: "hybrid"Omit getssh.type; pair renderer with backend main
getssh.pluginId acts as runtime identityname acts as runtime identity; pluginId is metadata
safeStorageEncrypt() returns synchronouslyAsynchronous: await context.safeStorageEncrypt()
ssh.onData() unmanagedReturns an unsubscribe function to be stored and invoked
ssh.write() synchronousAsynchronous: await context.ssh.write()
pluginRpcInvoke()Frontend calls window.GETSSH.invokeBackend()
onPluginRpcMessage()Frontend registers window.GETSSH.onBackendMessage()
registerSettingsSchema()Backend calls context.ui.registerSettings()
registerUIExtension()Use registerTerminalContextMenu() or registerSFTPContextMenu()
onSSHSessionConnectNot exposed; obtain session ID from context menu invocation
Normal mode accesses arbitrary files/networkControlled access via context.storage, context.net, context.ssh, context.host

12. Common Errors

Node.js plugins must declare ... lifecycle

Manifest missing "lifecycle" capability. Add it to capabilities array and export deactivate().

Backend plugins must export activate() and deactivate()

Backend is not CommonJS, exports invalid identifiers, or main points to wrong file. Verify bundled production artifact.

Backend plugins must call context.ui.registerSettings()

Call registerSettings() before activate() completes, providing at least one field.

Plugin '<name>' is not running

Common causes: Safe Mode disabled backend execution, activation failure, missing system isolation tools (bwrap), or URL hostname mismatching Manifest name.

Method '<method>' not found

Ensure backend calls context.rpc.registerMethod() before activate() finishes and verify method identifier casing.

window.GETSSH is undefined

Page must be loaded via getssh-plugin://<name>/.... Direct filesystem or standard browser tabs do not inject the SDK bridge.

Plugin Terminated Following SecurityError

Plugin crossed a zero-trust boundary (e.g. net.fetch resolving to intranet/loopback address). Check endpoint configuration and reload.

Data Rejected by RPC or Storage

Ensure values are JSON-serializable without circular references, functions, BigInts, or binary Buffers, and stay within the 2 MiB payload limit.

13. Pre-Release Checklist

  • name uses stable lowercase identifiers matching all getssh-plugin://<name>/... URLs.
  • Pure UI plugins declare getssh.type: "sandbox"; backend plugins omit type.
  • Backend compiles to CommonJS and exports both activate and deactivate.
  • Backend capabilities include lifecycle with remaining capabilities strictly minimized.
  • activate() completes within 8 seconds with all registrations finalized prior to resolution.
  • deactivate() safely clears timers, listeners, and subscriptions.
  • Frontend handles invokeBackend() rejections gracefully.
  • All cross-boundary IPC data is JSON-serializable within size and rate boundaries.
  • No reliance on developer mode loopholes (child_process, fs, raw Electron).
  • ZIP archive contains all required production dependencies.
  • Verified in normal/strict modes across macOS, Windows, and Linux.