Skip to content

Plugins

Tony West edited this page Aug 28, 2026 · 7 revisions

Plugins

Joro supports Go plugins that extend the tool without modifying the core binary. Plugins are shared objects (.so on Linux, .dylib on macOS) loaded at startup from ~/.joro/plugins/.

Plugin management lives at SettingsPlugins. There is no top-level Plugins tab — a plugin can still add one, but managing them is a category of the Settings page.

Go plugins do not load on Windows or on a build without cgo. Where that rules them out, Scripting is the extension path that works everywhere.

Plugin types

Type Description
exec_provider Adds an execution backend to the Execute tab (its own connect, disconnect, and command flow).
tab Adds a top-level navigation tab with an embedded web UI.
feature Adds a sub-tab under SettingsPlugins, beside Manage, with an embedded web UI.
proxy_hook Runs inside the proxy request/response pipeline to inspect or modify traffic.
dashboard Replaces the default Dashboard page entirely. Only one dashboard plugin is active at a time. While one is installed, the widget layout editor on Settings has no effect and says so.
interact_provider Adds a callback source to the Interact tab alongside the native tokens and XSS probes. Manages its own instances (one per remote server or endpoint) and merges inbound interactions into the unified event feed.

Tab, feature, and dashboard plugins serve their UIs inside sandboxed iframes at /plugin/{name}/.

Installing a plugin

  1. Open SettingsPluginsManage in the web UI.
  2. Click upload and select the built plugin file (.so or .dylib).
  3. Click Restart Now, which gracefully re-executes Joro so the new plugin is picked up.

After the restart you should see the plugin listed with its name, type, and status. A feature plugin appears as a new sub-tab beside Manage; a tab plugin appears in the header navigation.

Managing plugins

From SettingsPluginsManage you can:

  • List all plugins and their status.
  • Delete a plugin, optionally taking its stored data with it.
  • Trigger a graceful restart at any time with Restart Now.

A file that fails to load is listed too, with a dash for its name and Error as its status; hover the status for the reason. This is where a plugin built against the wrong toolchain shows up, and you can delete it from the same table. A warning bar also appears above the top navigation on every page — naming the file and the reason for one failure, or counting them for several — with a link straight here. Dismiss it and it stays dismissed for the session.

When a plugin will not load

Joro checks a plugin file before opening it, and reports what is wrong in the terms you would fix it in:

  • not a Go plugin (no Go build information)
  • not built as a Go plugin (no -buildmode recorded)
  • built with -buildmode=<x>, want plugin
  • built with go1.X, this joro binary is go1.Y
  • built against github.com/BishopFox/joro/sdk vA, this joro binary embeds vB

Each ends with the command that fixes it: joro --build-plugin <dir> --install. A plugin that panics while reporting its own manifest is caught too, and shows as panic in Manifest: <value>.

This matters more than a tidy error message. Go opens a plugin with dlopen, before the API server binds — so a plugin the binary cannot load used to be a Joro that would not start, with no UI to delete the offender from. --no-plugins is the way back in: it starts without opening any plugin, still lists every installed file so you can delete one, and marks each row Disabled. Restart without the flag when you are done.

Building is stricter for the same reason. joro --build-plugin now refuses a Go toolchain that does not match the one this binary was compiled with, exits non-zero and writes no .so — previously it warned and built something that would never load.

After Joro updates itself it counts the installed plugins that must be rebuilt against the new version and tells you, in the terminal and in the in-app update banner. An update changes the embedded SDK version, so this is the normal case rather than an error.

Deleting a plugin

Delete removes the plugin file from ~/.joro/plugins/ after a confirmation naming the file. The confirmation offers to also delete the plugin's stored data at ~/.joro/plugin-data/<name>/, off by default — leave it unticked to keep the plugin's API keys and state for a later reinstall. Plugin state saved inside your user and project configs is kept either way, so a config still round-trips through a machine that does not have the plugin.

Deleting the file does not stop a loaded plugin. Its row stays, marked Removed, and its code, routes and proxy hooks keep working until you restart; the reminder to restart persists until you do. Deleting a file that never loaded takes effect immediately, and its row simply disappears.

Building a plugin

Plugins are ordinary Go modules that import the Joro SDK (github.com/BishopFox/joro/sdk) and export a var Plugin sdk.Plugin symbol. Working examples live in examples/plugins/ in the Joro repository, covering each plugin type.

Note: Plugins built against a release binary are not interchangeable with plugins built against a source build (make build). The two use different go build flags, and Go's plugin loader rejects shared objects whose flags don't match the host. Build plugins against the line you intend to run them on.

Build and install in one step using the Joro binary:

./joro --build-plugin examples/plugins/hello-feature --install

Or build without installing:

./joro --build-plugin examples/plugins/hello-feature

This produces a .so or .dylib (depending on your OS) next to the plugin source. Use --install to also copy it into ~/.joro/plugins/.

You can also build a plugin manually with standard Go tooling:

cd examples/plugins/hello-feature
go build -buildmode=plugin -o hello-feature.dylib .   # macOS
go build -buildmode=plugin -o hello-feature.so .      # Linux

Naming rules

Plugin names must match ^[a-z0-9][a-z0-9_-]*$ and cannot be any of the reserved words api, ws, ext, or system. The SDK rejects anything else at load time.

Isolation

Each plugin gets its own data directory under ~/.joro/plugin-data/{name}/ and its own WebSocket channel for broadcasting events back to the UI. Panics in plugin code are caught and logged so that a faulty plugin cannot bring Joro down.

Rendering captured data

Values in the capture store are arbitrary bytes from the wire — a URL may hold markup or quotes, and a URL captured in origin-form is stored relative, so new URL() throws on it rather than returning a path.

Build cells and labels with createElement and textContent rather than interpolating a captured value into a markup string. This applies to attribute values as well as text: a quote in a captured value closes an interpolated title="…". examples/plugins/hello-dashboard shows the pattern.

Nothing catches this for you. Joro serves its own UI under a Content-Security-Policy that forbids inline script, but /plugin/* paths are exempt — plugin UIs legitimately use inline <script>, and every shipped example does. So the policy that would turn a markup-interpolation bug into a blocked script on the main UI does not apply to your page. A plugin that renders captured bytes as HTML is script execution on Joro's own origin, with a captured value from a target you are testing as the source. Build the DOM.

State persistence

Plugins can opt into saving their own state inside your named User or Project Configs. User-scoped state (API keys, personal tokens) rides with User Configs; engagement-scoped state (active sessions, instance configs, correlation IDs) rides with Project Configs. Nothing is autosaved — state is only serialized when you explicitly save a config, and only applied when you load one. State blobs for plugins you do not have installed on the current machine are preserved on save/load round-trips, so swapping configs between machines with different plugin sets will not silently drop anything.

Clone this wiki locally