Plugin system overview
Declare behavior once at load — Go runs the hot path
Sulfur is a from-scratch Minecraft Java Edition 26.2 server (protocol 776) written in Go. Its plugin system is built around one core idea:
Plugins DECLARE behavior once, at load. Go runs the hot path.
A plugin’s script body runs exactly once when the plugin loads. During that single run it
captures things into the server: event hooks (register), mob declarations (declare_mob),
skill data (skill/mechanic/targeter/condition), and recipe matchers (set_recipe_matcher).
After load, the Go server owns the tick loop and calls back into the captured hooks only at the
exact moments they apply. An idle plugin costs the server essentially nothing.
Dual runtime
Sulfur ships two plugin runtimes, selected per plugin by the runtime field in plugin.toml:
The default Sulfur binary is a pure-Go static build (CGO_ENABLED=0). The Python lane lives
entirely behind the python build tag; without it, a runtime = "python" plugin is skipped
gracefully at load with a log line. See Python runtime.
The Starlark sandbox
Starlark plugins are hard-sandboxed by construction:
- No I/O surface. The Starlark universe has no filesystem, network, or
evalbuiltin —openis simply undefined. The only app-specific globals a plugin sees are the ones the host injects (log,chat,register,math, the recipe builtins, and — for mob plugins — the declaration builtins). - Step budget. Every callback runs on a fresh thread with a 10,000,000-step execution budget. A runaway loop returns an error (“too many steps”) instead of hanging the server.
- Safe dialect. Recursion is checked,
whileloops are off, and module globals are auto-frozen after the module body runs — captured hooks are immutable values that are safe to call from the tick goroutine. - Per-hook isolation. Each hook invocation is wrapped in a panic recover. One erroring or panicking hook is logged and skipped — it never aborts other hooks or kills the tick.
- Capabilities. Anything that touches live game state (entities, world, navigation, skill
mechanics) is gated by the manifest
capabilitieslist, with least-privilege defaults and loud failure. See Handles & capabilities.
Dogfooding: vanilla is a plugin
The plugin API is not a side feature — Sulfur’s own vanilla content runs through it:
- Every vanilla mob (pig, cow, sheep, chicken, wolf, cat, zombie, skeleton, creeper, spider,
enderman, villager, and more — 28 mob plugins under
plugins/mobs/) is declared withdeclare_mob, as a literal method-for-method port of the unobfuscated 26.2 jar’s AI goals. - Vanilla crafting is a bundled Starlark plugin (
plugins/crafting/) that re-expresses the jar’s recipe-matching algorithm through theset_recipe_matcherseam.
If the API can express all of vanilla faithfully, it can express your custom content too.
What a plugin can do today
Hot reload
The server watches the plugins/ directory (pure-Go fsnotify). Saving a .star or .toml file
triggers a debounced (150 ms) rebuild: a brand-new plugin manager is constructed off the tick
goroutine and swapped in between ticks. A save with a syntax error keeps the last-good plugin set
running — the broken build is logged and never swapped in.

