Skip to content

Plugin Development Guide

Komari supports extending the server with JavaScript plugins. A plugin is a ZIP package containing a manifest (komari-plugin.json) and an entry script (default script.js). Plugins run inside the server process in their own sandboxed goja JS runtime, and can register HTTP routes, intercept HTTP requests/responses, call system RPC methods, register their own RPC methods, declare configuration items, and inject admin pages.

Security

Plugins run with admin privileges and may request sensitive capabilities such as filesystem access, child process execution, or port listening. Only install plugins you trust; review the declared permissions carefully before enabling third-party plugins.

What a plugin can do

CapabilityAPIRequired permissionNotes
Register RPC methodsserver.registerRPCalways grantedRegister plugin:xxx methods callable by the UI or other plugins
Schedule periodic tasksserver.cronalways grantedRun a handler on the plugin event loop on a cron schedule
Call system RPCserver.callallowSystemRPCInvoke any registered RPC method with admin authority
Register HTTP routesserver.routeallowRoutesRegister METHOD /path on the host engine; supports streaming
Mount a static folderserver.staticallowRoutesServe a folder from the plugin directory on the host engine; optional SPA fallback ({ spa: true })
Intercept HTTP requests/responsesserver.hookallowHooksModify every HTTP request/response entering or leaving the server
Intercept WebSocket connections/framesserver.hook (ws kinds)allowHookswsConnect (allow/deny a connection), wsMessage/wsSend (inspect/replace/drop frames), wsClose (connection ended)
Embed CSS/JS into every pageserver.injectHTMLallowHTMLInjectEmbed head/body fragments into every HTML response (incl. admin and terminal pages)
Read plugin configurationserver.getConfigalways grantedRead saved configuration merged with manifest defaults
Declare configuration itemsmanifest configurationno permissionAdmin UI generates a config form automatically
Inject admin pagesmanifest pagesno permissionShow iframe / redirect pages in the admin sidebar
File accessfs / requireinside plugin dir and storage dir: always grantedSandboxed to the plugin directory and __storageDir__ (data/plugin-data/<short>); escaping requires allowAllFileAccess
Node compatibility modulesnode modulesnodeevents/fs/path/os/process/net/http/crypto, etc.
Child processeschild_processallowExecExecute external commands
Port listeningnet/http ServerallowListenBind local ports (default 127.0.0.1)

Quick Start with the SDK

The recommended workflow uses the published SDK packages and create-komari-plugin. It provides TypeScript types, VS Code completion, manifest hover documentation, a local build, and a watch mode that uploads and reloads the plugin automatically.

Prerequisites

  • Node.js 20 or later
  • A reachable Komari development server
  • An API key with permission to install and manage plugins
  • VS Code with the generated project opened as the workspace root

Keep the development server and API key private. The initializer stores them in komari.local.json, which is added to .gitignore by default.

Create a project

Run the initializer from the directory where the project should be created:

sh
npm create komari-plugin hello

It prompts for the development server URL and API key. For scripted setup, pass the values explicitly:

sh
npm create komari-plugin hello -- --server http://127.0.0.1:25774 --api-key "$KOMARI_API_KEY" --lang en

Then install dependencies and start development mode:

sh
cd hello
npm install
npm run typecheck
npm run dev

npm run dev builds the TypeScript source, packages the plugin, uploads it to the configured server, enables it, prints the runtime plugin log, and watches the source and manifest for changes. A file change automatically repeats that cycle. Use Ctrl+C to stop watching.

Useful alternatives:

sh
# Build, upload, and enable once
npm run dev -- --once

# Poll runtime logs less frequently
npm run dev -- --log-interval 1000

# Disable runtime log forwarding
npm run dev -- --no-logs

The [dev:log] lines come from the same per-plugin runtime log buffer shown in the Komari admin UI. Build output and enabled/running status are local developer tool output, not plugin runtime logs.

Generated project layout

text
hello/
├── src/plugin.ts          # TypeScript plugin source
├── komari-plugin.json     # Plugin manifest
├── komari.local.json      # Local server URL and API key; never commit
├── package.json
└── tsconfig.json

The generated manifest references the SDK Schema. In VS Code, this enables field completion, validation, and English hover descriptions:

json
{
  "$schema": "./node_modules/@komari-monitor/plugin-sdk/schema/komari-plugin.schema.json"
}

The generated project uses package version 1.4.1. Package versions and Komari server versions are not required to match patch-for-patch; this SDK release tracks the Komari 1.4.x compatibility line. The manifest komari field is a server version constraint and currently uses supported forms such as >=1.4.0.

SDK example

ts
import { definePlugin, jsonResponse, server } from "@komari-monitor/plugin-sdk";

definePlugin({
  load() {
    server.route("GET", "/hello", (_req, res) => {
      jsonResponse(res, { ok: true });
    });
  },
});

The SDK provides typed server helpers and a typed RPC catalog. Use rpc.call() for cataloged methods and server.call() for dynamic or plugin-owned methods. See server Module and RPC Methods for the complete API.

Manual ZIP Workflow

1. Create the plugin directory

hello/
├── komari-plugin.json
└── script.js

2. Write the manifest komari-plugin.json

json
{
  "name": "Hello World",
  "short": "hello",
  "description": "An example plugin",
  "author": "Your Name",
  "version": "1.0.0",
  "komari": ">=1.0.0",
  "entry": "script.js",
  "permissions": {
    "node": true,
    "allowSystemRPC": true,
    "allowRoutes": true
  }
}

See Manifest Reference for all fields.

3. Write the entry script script.js

js
const server = require("server");

function load() {
  console.log("hello plugin loaded");

  // Register an HTTP route: GET /hello
  server.route("GET", "/hello", async (req, res) => {
    const nodes = await server.call("common:getNodes");
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify({
      greeting: "Hello, Komari!",
      nodeCount: Object.keys(nodes).length
    }));
  });
}

function unload() {
  console.log("hello plugin unloaded");
}
  • The top-level script runs immediately when the plugin loads, but it is recommended to put logic in the global load() function.
  • load() runs every time the plugin is enabled/started; unload() runs on disable, uninstall, or server shutdown.

4. Package and install

Put komari-plugin.json and script.js directly at the ZIP's root (no wrapping folder), then upload it on the admin "Plugins" page, or install via the admin API:

powershell
curl -X POST -H "Cookie: session_token=<your session>" --data-binary "@hello.zip" http://localhost:25774/api/admin/plugin/install

5. Enable the plugin

After installation the plugin is disabled by default and must be enabled manually.

Because hello declares allowSystemRPC and allowRoutes, enabling it triggers the permission approval flow: the admin must confirm the permissions in a dialog before the plugin can be enabled (see "Permissions & Approval" below).

Once enabled, visit GET /hello to see the plugin's JSON response.

Installation limits

ZIP packages: up to 10,000 files, each file ≤ 128 MiB, total extracted ≤ 512 MiB, manifest ≤ 1 MiB. Any path-traversal entry (../, absolute paths) rejects the entire package.

Lifecycle

Load load()

  1. The server reads and validates komari-plugin.json and checks the komari version constraint.
  2. A dedicated JS runtime is created (sandbox root: data/plugin/<short>, plus the long-term storage directory data/plugin-data/<short> injected as __storageDir__), and the entry script is executed immediately.
  3. If the script defines a global load() function, it is invoked.
  4. A top-level error or a load() error → the plugin is auto-disabled and the error is persisted in last_error.

Unload unload()

Triggered on disable, delete, or server shutdown:

  1. The plugin is removed from the instance registry first (so server.call rejects with "not loaded" from inside unload()).
  2. The global unload() is called if defined (errors are recorded, not blocking).
  3. Registered RPC methods are unregistered, handlers and hooks cleared, and the runtime closed.

Route slots persist

Gin route slots registered by a plugin remain after unload: requests to those routes receive 404 until the plugin is loaded again. Reinstalling a running plugin unloads it first and restores its persisted enabled state.

Startup recovery

On server startup (LoadAll), every enabled and approved plugin is loaded automatically; plugins that fail to load are auto-disabled with last_error persisted (this does not stop the server from starting).

Long-term storage directory __storageDir__

Every plugin gets a dedicated long-term storage directory when enabled:

text
data/plugin-data/<short>/   # long-term storage (untouched by updates)
  • Fully separated from the code directory data/plugin/<short> (the ZIP contents); the fs sandbox covers both directories.
  • Scripts access it via the global __storageDir__ (injected in NodeJS mode, absolute path):
    js
    const fs = require("fs");
    const path = require("path");
    fs.writeFileSync(path.join(__storageDir__, "cache.json"), "{}");
  • Updates (reinstall) replace only the code directory; the long-term storage survives — suitable for caches, user data, and similar state.
  • Deleting a plugin removes both directories (delete means full removal).
  • Same sandbox rules as the plugin directory: nothing escapes __storageDir__, other plugins' storage directories are unreachable, and cross-directory fs.renameSync (plugin dir ↔ storage dir) is rejected.

Permissions & Approval

Permission model

  • Always granted (no declaration needed, no approval): server.registerRPC, server.cron, server.getConfig, file access inside the plugin directory and __storageDir__.
  • Granted by declaration (runtime settings, no approval): permissions.node, maxHTTPBodyBytes, maxChildOutputBytes, timeout.
  • Require admin approval (7 sensitive capabilities; any of them being true triggers the flow): allowSystemRPC, allowRoutes, allowHooks, allowHTMLInject, allowExec, allowListen, allowAllFileAccess.

Approval flow

When admin:setPluginEnabled enables a plugin, the server compares the declared capability set with the hash saved at the last approval:

  • Matches → enabled directly.
  • Differs and the plugin is not yet approved → returns { requires_approval: true }; the admin UI shows a permission dialog, and retries with approved: true after confirmation.
  • Changing sensitive capabilities after approval → requires re-approval (changes to node/timeout/size limits do not re-trigger approval).

Capability hash

The approval hash covers only the 7 sensitive capabilities (a sha256:-prefixed JSON hash); node, maxHTTPBodyBytes, maxChildOutputBytes, and timeout are excluded.

Behavior when a permission is missing

APIBehavior without permission
server.route / server.static / server.hook / server.injectHTMLThrows TypeError at load time; plugin load fails (auto-disabled)
server.callThe returned Promise is rejected (load not blocked)
require("child_process")Throws (no allowExec)
net/http Server listen()Throws (no allowListen)
fs / require outside plugin dirRejected by the sandbox (no allowAllFileAccess)

Debugging

Each plugin has a dedicated 64 KiB ring log buffer; console.* output and lifecycle/hook errors are written to it (reset on every load). Read it via the RPC2 method admin:getPluginLogs (param {short}):

POST /api/rpc2
{"jsonrpc":"2.0","id":1,"method":"admin:getPluginLogs","params":{"short":"hello"}}

TIP

When a plugin fails to load, the admin "Plugins" list shows last_error. Combined with the plugin logs, this diagnoses most issues (e.g. missing permissions, unsupported runtime APIs).

Security & Limitations

  • The plugin sandbox roots are data/plugin/<short> and data/plugin-data/<short> (__storageDir__): fs and require are confined to them, and path traversal / symlink escapes are rejected at the OS level (os.Root).
  • server.call runs with admin authority — a plugin calling admin:* methods is equivalent to the admin doing it themselves.
  • The JS runtime is not a browser and not full Node.js: no DOM, WebSocket client API, ESM, for await...of, or complete fs. Read the JS Runtime Reference before relying on any API.
  • Each JS turn is bounded by timeout (default 30 s): script init, load(), route handlers, hooks, RPC handlers, and fetch all obey it. A route handler that never calls end() produces 504.
  • There are no per-plugin CPU/memory/network quotas; process.memoryUsage() etc. report the whole Komari process.

Continue Reading

DocumentContents
Manifest ReferenceAll komari-plugin.json fields, permissions, configuration, pages
server Moduleserver.route / server.static / server.hook / server.injectHTML / server.call / server.registerRPC / server.cron / server.getConfig and lifecycle hooks
JS RuntimeEvery JavaScript API available in the sandbox and its compatibility
RPC MethodsAll system RPC methods callable via server.call
Publishing to the Plugin MarketPublish your plugin to the official plugin market

Released under the MIT license.