Instrument-style timeline of SDK hook callback nodes on a navy console panel

HarborClient plugins stay alive for the whole session. When something happens in the app — a request finishes, the library changes, a theme switches — your plugin can run code at that moment. Those callbacks are hooks.

A hook is a function you register on the plugin context (hc) so the host can call it later. You pass the handler to an API such as hc.http.onAfterSend; HarborClient keeps the registration, invokes your handler when the event fires, and returns a Disposable so you can unregister early if you need to. Hooks are a core pattern in the plugin SDK — not a niche HTTP feature.

The Disposable contract

Every event hook and most contribution APIs return the same shape:

interface Disposable {
  dispose(): void;
}

When you call hc.http.onAfterSend(handler) (or hc.ui.registerSettingsSection, and similar), the host auto-tracks the returned disposable on hc.subscriptions. On disable or unload, HarborClient disposes those registrations for you, then calls exported deactivate() if present.

Keep the return value only when you need early cleanup — for example inside a React effect:

useEffect(() => {
  const stop = hc.host.onLibraryChanged(() => {
    void reload();
  });
  return () => stop.dispose();
}, [hc]);

Dispose custom resources yourself — timers, syncOnWindowFocus, and other helpers that are not host-tracked registrations — from deactivate() or effect cleanup. Do not rely on stuffing those onto hc.subscriptions by hand.

Main vs renderer

Plugins can ship a renderer entry, a main entry, or both. That split decides which hooks you get:

  • Renderer (PluginContext) — React UI, settings, host library/workflow/sidebar events, themes, live servers, filesystem watch, and hc.http.onAfterSend for completed sends in the UI.
  • Main (MainPluginContext from @harborclient/sdk/main) — SES utilityProcess: hc.http.onBeforeSend / onAfterSend, hc.server.onRequest, and hc.ipc.handle. No React, no library/theme/live-server hooks.

Rule of thumb: mutate outbound requests or run hardened background logic in main; refresh UI or call renderer APIs such as hc.fs from the renderer after-send hook.

HTTP hooks

Requires the http permission in manifest.json.

Main: onBeforeSend and onAfterSend

onBeforeSend runs before each outgoing request. You may mutate method, URL, headers, or body (for example inject a trace header or strip Authorization). Handlers across plugins run sequentially.

import type { MainPluginContext } from '@harborclient/sdk/main';

export function activate(hc: MainPluginContext): void {
  hc.http.onBeforeSend(async (request) => {
    request.headers['X-Plugin-Trace'] = '1';
    delete request.headers['Authorization'];
  });

  hc.http.onAfterSend((request, response) => {
    console.log(request.method, request.url, response.status);
  });
}

Main onAfterSend is for SES-side work or main-process state. Prefer renderer onAfterSend when you only need UI reactions (history panels, toasts, writing a log file through hc.fs).

Renderer: onAfterSend only

The renderer HTTP surface exposes after-send only — there is no renderer onBeforeSend. The handler receives serializable request and response snapshots (method, url, headers, body, status, and related fields).

import type { PluginContext } from '@harborclient/sdk';

export function activate(hc: PluginContext): void {
  hc.http.onAfterSend(async (request, response) => {
    // react to completed requests without a main entry
  });
}

For a full walkthrough that appends each exchange to a local file, see Logging Requests with a HarborClient Plugin (draft).

Host and UI lifecycle hooks

These live on the renderer PluginContext and require the ui permission. They are coarse invalidation signals — subscribe, then refetch the data you need.

  • hc.host.onLibraryChanged(listener) — collections, folders, requests, or documents changed. Event includes reason and optional collectionId.
  • hc.host.onWorkflowsChanged(listener) — workflow create / update / rename / delete / refresh.
  • hc.host.onSidebarSelectionChanged(listener) — host sidebar selection (tree, tab, or plugin-driven).
  • hc.themes.onDidChange(listener) — active theme changed (Settings or plugin teardown fallback).

Typical pattern for a custom collections tree:

async function refreshTree() {
  const tree = await hc.host.listLibraryTree();
  renderSidebar(tree.collections);
}

const stop = hc.host.onLibraryChanged((event) => {
  // event.reason: 'collections' | 'folders' | 'requests' | 'documents'
  void refreshTree();
});

await refreshTree();
// Later: stop.dispose();

Live servers and filesystem

hc.liveServers requires the live-server permission:

  • onRunningChanged(listener) — the list of running live servers changed (including from the HarborClient UI).
  • onRequestLog(listener) — Express access-log lines from running servers as they arrive.

hc.fs.watchFile(path, listener) requires filesystem:read. It notifies when an allowlisted file changes (debounced). Useful for dotenv-style plugins that reload when a linked file is edited on disk.

Main-only: server.onRequest

With the server permission, main plugins can run a local echo/mock HTTP server. Register hc.server.onRequest before hc.server.start so inbound traffic hits your handler. Return a body (legacy JSON, always HTTP 200), undefined for the default echo payload, or a structured response from createHttpResponse (status, headers, body, delay).

import type { MainPluginContext } from '@harborclient/sdk/main';
import { createHttpResponse } from '@harborclient/sdk/runtime-utils';

export function activate(hc: MainPluginContext): void {
  hc.server.onRequest(async (request) => {
    if (request.path === '/echo') {
      return { ...request.echo, custom: true };
    }
    return createHttpResponse({
      status: 404,
      headers: { 'X-Mock': '1' },
      body: { error: 'not found', path: request.path }
    });
  });
  void hc.server.start({ port: 0 });
}

UI contributions — hc.ui.registerSettingsSection, sidebar panels, request/response tabs, menus, and the rest — are not event streams, but they share the same Disposable lifecycle. Register in activate, let the host track cleanup, and dispose early only when a panel unmounts. Treat them as the sibling pattern to hooks: one contract for “tell the host about me” and “notify me when something happens.”

Hooks vs request scripts

Both use an hc name, but they solve different jobs:

  • Request scripts — one-shot per send in the collection/request editor (variables, tests, response assertions).
  • Plugin hooks — long-lived until the plugin deactivates; can contribute UI, persist storage, and react across every send or library change.

Plugins do not replace collection or request scripts for per-request logic. Main-process plugins can still run the script hc API programmatically via hc.scripts.createContext() when you need that surface from a hook. See Plugins vs scripts and the request scripts docs.

Permissions cheat sheet

  • httponBeforeSend / onAfterSend
  • ui — host library / workflows / sidebar selection, themes onDidChange, UI register*
  • live-server — live server running list and request log hooks
  • filesystem:readwatchFile (plus read APIs)
  • serverhc.server.onRequest / start / stop
  • ipchc.ipc.handle (main) paired with renderer hc.ipc.invoke

Next steps

Start from the plugin skeleton (it already shows a main-process onAfterSend), then pick the entry that matches your job. Full API reference: Plugin development guide. For a concrete after-send logger, follow Logging Requests with a HarborClient Plugin.

More resources:

Leave a Reply

Trending

Discover more from HarborClient Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading