HarborClient plugins can listen to every completed HTTP exchange. That makes it straightforward to build a local request logger: pick a file in Settings, then append method, URL, status, and a few other details after each send.

This tutorial starts from the official plugin skeleton, replaces the example settings toggle with a log file path, and wires hc.http.onAfterSend to write newline-delimited JSON. For the full API reference, see the Plugin development guide.

What you will build

A small unpacked plugin that:

  • Contributes a Settings section where you choose the log file
  • Stores the chosen path in plugin storage
  • Listens after each request with hc.http.onAfterSend
  • Appends one JSON line per exchange to that file

Prerequisites: HarborClient with plugin support, Node.js, and pnpm.

Start from the plugin skeleton

Clone the skeleton, then detach it from the upstream Git history so your project can become its own repository:

git clone https://github.com/harborclient/plugin-skeleton.git request-logger
cd request-logger
rm -rf .git
git init
pnpm install

Deleting .git (or running rm -rf .git && git init) is important. If you leave the skeleton remote in place, your first push will still point at the template repository.

Open package.json and set the dependency @harborclient/sdk to version “^1.5.1”.

Open the request-logger directory using your favorite IDE, like VS Code or Cursor.

Cursor IDE open on the request-logger plugin skeleton README.md and project tree
The cloned skeleton opened in an IDE.

Update manifest.json so the plugin has a unique id, a clear name, and the permissions this logger needs:

{
  "id": "com.example.request-logger",
  "name": "Request Logger",
  "version": "1.0.0",
  "author": "Acme Inc.",
  "summary": "Append completed requests to a local log file.",
  "description": "README.md",
  "engines": { "harborclient": ">=2.0.0" },
  "renderer": "dist/renderer.js",
  "main": "dist/main.js",
  "permissions": [
    "ui",
    "storage",
    "http",
    "filesystem:pick",
    "filesystem:read",
    "filesystem:write"
  ],
  "contributes": {
    "settingsSections": [{ "id": "settings", "title": "Request Logger" }]
  }
}
manifest.json for Request Logger showing permissions and settings contribution
Updated manifest.json with permissions and the settings section.

The skeleton already declares ui, storage, and http. The filesystem permissions let the settings panel create or choose a log file, then let the after-send handler read and rewrite that allowlisted path. You can drop the example footer panel contribution for this tutorial; a Settings section is enough.

Add a log file setting

Open src/components/SettingsPanel.tsx and replace the skeleton’s enabled checkbox with a log file path UI.

hc is the plugin context HarborClient passes into your plugin APIs. In the skeleton, activate(hc) in src/renderer.tsx receives it and forwards it into the settings panel as <SettingsPanel hc={hc} />. That hc prop is typed as PluginContext from @harborclient/sdk. Call hc.fs.saveFile so the user picks a destination — HarborClient allowlists that path and keeps the grant across restarts — then persist the absolute path with hc.storage.set('logFilePath', path). Replace the whole file contents with:

import { useCallback, useEffect, useState } from '@harborclient/sdk/react';
import type { PluginContext } from '@harborclient/sdk';

const STORAGE_KEY = 'logFilePath';

export function SettingsPanel({ hc }: { hc: PluginContext }) {
  const [logFilePath, setLogFilePath] = useState('');
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let active = true;
    void hc.storage
      .get<string>(STORAGE_KEY)
      .then((value) => {
        if (active) setLogFilePath(value ?? '');
      })
      .catch(() => {
        if (active) setError('Failed to load settings.');
      });
    return () => {
      active = false;
    };
  }, [hc.storage]);

  const chooseLogFile = useCallback(async () => {
    setError(null);
    try {
      const path = await hc.fs.saveFile('', {
        defaultPath: 'harborclient-requests.jsonl',
        filters: [{ name: 'JSON Lines', extensions: ['jsonl', 'log', 'txt'] }]
      });
      if (!path) return;
      await hc.storage.set(STORAGE_KEY, path);
      setLogFilePath(path);
      hc.ui.showToast('Log file updated');
    } catch {
      setError('Failed to save log file path.');
    }
  }, [hc]);

  return (
    <div className="flex flex-col gap-3 p-4">
      <p className="text-muted">
        Choose where HarborClient should append request logs.
      </p>
      <p className="break-all">{logFilePath || 'No log file selected.'}</p>
      <button type="button" onClick={() => void chooseLogFile()}>
        Choose log file…
      </button>
      {error ? <p className="text-danger">{error}</p> : null}
    </div>
  );
}

Leave src/renderer.tsx alone for a moment — the next section replaces that file with the full activation code that registers this settings panel and the after-send logger.

Listen after each request

The skeleton’s src/main.ts already shows the HTTP hook pattern with hc.http.onAfterSend.

In the HarborClient plugin SDK, a hook is a Disposable-returning callback the host runs at a specific lifecycle moment. For the full inventory — HTTP, host, themes, live servers, filesystem, and cleanup — see SDK Hooks.

The skeleton’s main-process hook is ideal for terminal logging. File writes, though, go through hc.fs, which is available on the renderer plugin context. For this logger, open src/renderer.tsx again and register the after-send handler there so you can read the path from storage and append to disk in one place. Replace the contents of src/renderer.tsx with:

import type { PluginContext, PluginHttpRequest, PluginHttpResponse } from '@harborclient/sdk';
import { SettingsPanel } from './components/SettingsPanel';

const STORAGE_KEY = 'logFilePath';

async function appendLogLine(hc: PluginContext, line: string): Promise<void> {
  const path = await hc.storage.get<string>(STORAGE_KEY);
  if (!path) return;

  let existing = '';
  try {
    existing = await hc.fs.readFile(path);
  } catch {
    existing = '';
  }

  const next = existing.length === 0 ? `${line}\n` : `${existing}${line}\n`;
  await hc.fs.writeFile(path, next);
}

export function activate(hc: PluginContext): void {
  function SettingsPanelHost() {
    return <SettingsPanel hc={hc} />;
  }

  hc.ui.registerSettingsSection({
    id: 'settings',
    title: 'Request Logger',
    Component: SettingsPanelHost
  });

  hc.http.onAfterSend(async (request: PluginHttpRequest, response: PluginHttpResponse) => {
    const entry = {
      at: new Date().toISOString(),
      method: request.method,
      url: request.url,
      requestHeaders: request.headers,
      bodyLength: request.body?.length ?? 0,
      status: response.status,
      statusText: response.statusText,
      responseHeaders: response.headers,
      responseBodyLength: response.body?.length ?? 0,
      sourceRequestName: request.sourceRequestName ?? null
    };

    try {
      await appendLogLine(hc, JSON.stringify(entry));
    } catch (error) {
      console.error('Failed to write request log', error);
    }
  });
}
src/renderer.tsx showing hc.http.onAfterSend and appendLogLine
renderer.tsx with the settings panel and after-send logger.

A few practical notes:

  • hc.fs.writeFile overwrites the whole file, so the helper reads the current contents, appends one JSON line, and writes the result back.
  • User-selected paths from saveFile stay on the plugin allowlist after restart.
  • Prefer lengths or redacted summaries over full bodies when logs might contain tokens or personal data.
  • If you still want terminal output, keep the skeleton’s src/main.ts onAfterSend logger alongside this renderer handler.

Build and load the plugin from a directory

Build at least once so dist/ exists, then point HarborClient at the project folder:

pnpm build
# or, while iterating:
pnpm dev
  1. Open File → Plugins (Alt+Shift+P).
  2. Choose Install → Load unpacked….
  3. Select the request-logger directory — the folder that contains manifest.json.
  4. Confirm the permissions dialog and enable the plugin.
Enable Request Logger permission dialog in HarborClient
Review and enable the plugin permissions.

Unpacked plugins stay registered across launches. While pnpm dev is watching, HarborClient reloads the plugin when manifest.json or the built entry files change. Leave the settings panel open if you want to see UI updates immediately after each rebuild.

One thing to note, the README.md included in the plugin directory is displayed to users of the plugin. You will need to replace the skeleton markdown with your own.

Request Logger plugin details page with Development source and README
Plugin details for an unpacked Development install.

Enable logging

Open Settings → Request Logger, choose a log file.

Settings Request Logger panel with chosen log file path
Choose the log file under Settings → Request Logger.

Verify the log

Now send several requests, and open the file you chose. You should see one JSON object per line, for example:

{"at":"2026-08-02T15:01:22.410Z","method":"GET","url":"https://httpbin.org/get","requestHeaders":{"Accept":"*/*"},"bodyLength":0,"status":200,"statusText":"OK","responseHeaders":{"content-type":"application/json"},"responseBodyLength":312,"sourceRequestName":"Get httpbin"}
harborclient-requests.jsonl open in a text editor with a GET log entry
A formatted request log entry written by the plugin.

If nothing appears, confirm the plugin is enabled, a log path is saved in Settings, and the send completed (after-send runs when a response is received).

Next steps

Want those JSONL lines in the HarborClient UI? Continue in Show Request Logs in a Footer Panel — add a Request Logs footer toggle and a slide-up panel that lists recent exchanges as they complete. From there you can also filter by status, rotate files by date, or package the plugin for others. When you are ready to share it, commit a built dist/, push to GitHub, and follow the marketplace steps in How to Write 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