Using HashiCorp Plugins with go-go-goja

User guide and surface reference for loading external HashiCorp go-plugin modules into go-go-goja runtimes.

Sections

Terminology & Glossary
📖 Documentation
Navigation
22 sectionsv0.1
📄 Using HashiCorp Plugins with go-go-goja — glaze help goja-plugin-user-guide
goja-plugin-user-guide

Using HashiCorp Plugins with go-go-goja

User guide and surface reference for loading external HashiCorp go-plugin modules into go-go-goja runtimes.

Topicgojapluginshashicorprepljavascriptmodulesgoja-repl--plugin-dir

This page explains how plugin-backed JavaScript modules work from a user point of view.

The short version is simple: a plugin is a separate Go binary that speaks the HashiCorp go-plugin protocol, publishes a manifest that describes one JavaScript module, and responds to function calls from the host runtime. The host goja runtime stays inside go-go-goja. The plugin does not own a goja.Runtime; it only exposes a process boundary and an RPC surface.

That distinction matters because it tells you what to expect operationally:

  • You still use require("plugin:...") inside JavaScript.
  • The host process still owns module loading, JavaScript execution, and runtime shutdown.
  • Plugin modules are loaded from directories you explicitly trust.
  • Closing the runtime shuts down the plugin subprocesses that were started for that runtime.

What a plugin looks like to a JavaScript user

From JavaScript, a plugin-backed module looks like a normal native module:

const echo = require("plugin:echo")

echo.ping("hello")
echo.math.add(2, 3)
echo.pid()

The module name is the manifest module name published by the plugin. In the current design, plugin modules are expected to live in the plugin: namespace, so plugin:echo is valid and echo is rejected.

The exports currently supported by the host are:

  • Top-level functions such as echo.ping(...)
  • Top-level objects containing methods such as echo.math.add(...)

The first implementation intentionally keeps the export surface small. That keeps plugin manifests easy to validate and makes it obvious how a plugin export maps onto a JavaScript call site.

Quick start

This section shows the smallest working flow for testing a plugin manually.

1. Build a plugin binary

From the repository root:

mkdir -p ~/.go-go-goja/plugins/examples
go build -o ~/.go-go-goja/plugins/examples/goja-plugin-examples-greeter ./plugins/examples/greeter

If you want to install the whole example catalog at once, run:

make install-modules

The binary name matters. Discovery uses the pattern goja-plugin-* by default, so goja-plugin-examples-greeter is picked up automatically.

2. Start the canonical REPL with plugin discovery enabled

go run ./cmd/goja-repl tui

At runtime, goja-repl tui scans ~/.go-go-goja/plugins/... by default, starts matching plugin binaries through go-plugin, validates their manifests, and registers them as runtime-scoped CommonJS modules.

If you want to use a different location for one run, pass one or more explicit flags:

go run ./cmd/goja-repl --plugin-dir /tmp/goja-plugins tui

3. Require the module in JavaScript

let greeter = require("plugin:examples:greeter")
greeter.greet("hello")
greeter.strings.upper("hello")
greeter.meta.pid()

Expected results:

  • greeter.greet("hello") returns "hello, hello"
  • greeter.strings.upper("hello") returns "HELLO"
  • greeter.meta.pid() returns the plugin subprocess PID

Example catalog

The example directory now contains several SDK-authored plugins, each meant to teach one part of the surface:

ExampleModule nameWhat to learn from it
plugins/examples/greeterplugin:examples:greeterBaseline module structure, metadata, function exports, object methods
plugins/examples/clockplugin:examples:clockZero-argument handlers and structured result objects
plugins/examples/validatorplugin:examples:validatorsdk.Call helpers, defaults, map/slice inputs, and validation failures
plugins/examples/kvplugin:examples:kvStateful object methods that keep process-local state
plugins/examples/system-infoplugin:examples:system-infoMixed export shapes and nested JSON-like responses
plugins/examples/failingplugin:examples:failingExplicit handler errors and how failures surface to JavaScript

If you want a single starting point, copy plugins/examples/greeter. If you want a plugin that looks more like input validation or process-local services, read validator and kv next.

4. Exit the runtime

When you leave the REPL or the runtime is closed, the plugin subprocesses created for that runtime are shut down by runtime cleanup hooks.

Current command-line entrypoints

The canonical interactive entrypoint is goja-repl tui.

goja-repl tui

goja-repl tui is the Bobatea TUI REPL with completion and help widgets.

It supports:

  • default scan under ~/.go-go-goja/plugins/...
  • optional --plugin-dir flags for explicit directories
  • optional --allow-plugin-module flags for module-name allowlisting
  • runtime profile selection through --profile

Example:

go run ./cmd/goja-repl tui

Plugin discovery rules

This section explains how the host decides which binaries count as plugins.

By default, the host-side plugin config uses:

  • discovery pattern: goja-plugin-*
  • namespace prefix: plugin:
  • gRPC transport only

The host scans the configured directories and filters for regular executable files that match the discovery pattern. For each candidate, it:

  1. starts a go-plugin client,
  2. dispenses the shared module service,
  3. asks for the module manifest,
  4. validates the manifest,
  5. reifies the described exports into the runtime's require.Registry.

If any plugin has an invalid manifest, runtime creation fails early instead of partially registering a broken module set.

Discovery visibility in the REPL

goja-repl tui exposes plugin visibility through runtime startup and its in-session JavaScript environment:

  • the TUI uses the same plugin directory and allowlist flags as the rest of goja-repl,
  • plugin discovery happens during app startup against the selected runtime profile.

Surface API reference

This section is the user-facing contract for what a plugin may expose to JavaScript.

Module name

  • Must be non-empty
  • Must begin with plugin:
  • Must be unique within the runtime

Examples:

  • valid: plugin:echo
  • valid: plugin:examples:greeter
  • valid: plugin:dbtools
  • invalid: echo
  • invalid: fs

Export kinds

Current export kinds:

  • FUNCTION
  • OBJECT

Rules:

  • A FUNCTION export must not declare methods.
  • An OBJECT export must declare one or more method names.
  • Export names must be unique within the module.
  • Object method names must be unique within the object export.

Invocation model

Every JavaScript call is forwarded over RPC as:

  • module export name,
  • optional object method name,
  • argument list converted into protobuf structpb.Value values.

The result comes back as one protobuf value and is converted back into a JavaScript value by the host runtime.

That means the most reliable data shapes are the ones that already round-trip cleanly through JSON-like values:

  • strings,
  • numbers,
  • booleans,
  • arrays,
  • objects,
  • null.

Authoring rules for plugin users

If you are building your own plugin binary, follow these rules first before you optimize anything else.

  • Use a binary name that matches goja-plugin-*.
  • Publish a module name in the plugin: namespace.
  • Prefer the higher-level pkg/hashiplugin/sdk package instead of implementing contract.JSModule manually.
  • Keep arguments and return values JSON-like.
  • Prefer small, explicit function signatures.
  • Use object exports only when you want a real namespace such as plugin:foo.math.add.

These rules are not arbitrary. They align with how the first host implementation validates manifests and how it currently marshals values across the process boundary.

The example catalog under plugins/examples/... now follows the richer SDK path:

  • sdk.MustModule(...) defines the module,
  • sdk.Function(...) defines top-level functions,
  • sdk.Object(...sdk.Method(...)) defines object-method exports,
  • sdk.MethodSummary(...), sdk.MethodDoc(...), and sdk.MethodTags(...) attach richer method metadata,
  • sdk.Serve(...) boots the shared transport.

The examples are intentionally varied. greeter is the small baseline, clock emphasizes structured return values, validator shows sdk.Call helper usage, kv shows plugin-local state, system-info shows nested responses, and failing shows explicit error returns.

Security and trust model

This feature is process isolation, not sandboxing.

Loading a plugin means executing a local Go binary that you selected via --plugin-dir. The plugin runs outside the host process, which is useful for isolation and lifecycle control, but it is still native code you are choosing to execute. Treat plugin directories as trusted execution inputs.

What the current system does provide:

  • explicit directory opt-in,
  • optional module-name allowlisting at the CLI/config layer,
  • manifest validation,
  • namespace validation,
  • runtime-scoped lifecycle,
  • gRPC-only transport.

What it does not currently provide by default:

  • checksums,
  • signatures,
  • automatic provenance verification,
  • an opinionated allowlist policy.

If you want to allow only a specific set of modules for one run, use the allowlist flag:

go run ./cmd/goja-repl --allow-plugin-module plugin:examples:greeter tui

You can repeat the flag to allow multiple module names.

Troubleshooting

ProblemCauseSolution
Cannot find module 'plugin:examples:greeter'The plugin binary was not discovered or the manifest was rejectedConfirm the binary is named goja-plugin-examples-greeter, placed in the configured directory, and that you passed --plugin-dir /path/to/dir
Runtime creation fails with must use namespaceThe plugin manifest published a module name outside plugin:Update the plugin manifest to use a name like plugin:echo
Runtime creation fails with not in the allowlistThe plugin loaded successfully but its module name was not on the requested allowlistAdd --allow-plugin-module plugin:your-module or remove the allowlist restriction
The plugin binary exists but still is not loadedThe file is not executable or does not match the discovery patternRun chmod +x if needed and keep the binary name under goja-plugin-*
Calls fail on argument conversionThe JS values do not cleanly round-trip through protobuf structpb.ValueUse JSON-like values and avoid host-specific Goja objects/functions as arguments
goja-repl tui does not see pluginsThe plugin was not built under the default tree and no explicit directory was passedBuild into ~/.go-go-goja/plugins/... or pass one or more --plugin-dir flags

See Also

  • goja-repl help plugin-tutorial-build-install — Step-by-step tutorial for building and installing a plugin
  • goja-repl help goja-plugin-developer-guide — Internal architecture and integration guide
  • goja-repl help repl-usage — General REPL usage
  • goja-repl help creating-modules — Native in-process module authoring guide