
PlugMaster
Keeps your plugins up to date from Modrinth and Hangar, and controls them from in game. Updates are verified and parked for the next start, never swapped into a running server.
Tells you which of your plugins have a newer version, fetches it safely, and lets you switch plugins on and off without a restart.
Forty plugins in a folder, and no way to know which three of them shipped a fix last week. So you check by hand, or you don't check at all and find out from a player. Neither is a plan.
PlugMaster reads the folder for you. It works out which jars are published on Modrinth or Hangar, compares what you have against what's out there, and tells you what moved. Fetching the new file is a separate step you ask for, and nothing is ever swapped into a running server.
The second half is the part you reach for when something's already gone wrong at 2am: /pm list, /pm info, /pm disable, /pm reload. Turn the misbehaving plugin off, look at what it depends on, put it back, without kicking anyone.
Drop the jar in and restart. It reports and does nothing else until you tell it to.
Requirements
- Bukkit, Spigot, Paper, Purpur or Folia, 1.16.5 and up
- BungeeCord, Waterfall or Velocity for the update half
- Java 11 or newer
- Outbound HTTPS to
api.modrinth.comandhangar.papermc.io
No dependencies, nothing shaded. The JSON and YAML parsers are written into the project, which is why one jar carries a plugin.yml, a bungee.yml and a velocity-plugin.json and runs on any of them without relocation.
How it recognises your plugins
Two sources, asked in that order.
Modrinth goes by the SHA-1 of the file itself. Nothing to configure and nothing to guess at: the exact bytes you have either exist on Modrinth or they don't. Every installed plugin is looked up in one bulk request rather than one request per plugin, so a folder of sixty costs a single round trip.
That endpoint doesn't filter by release channel, though, so it can answer with a beta on a server that only accepts releases. When that happens the one plugin affected is asked about individually, where the channel does get honoured. The rest of the folder is unaffected.
Hangar has no hash to go on, so matching happens in two stages. First the website entry of the plugin.yml, which is dependable when it points at a Hangar project. Failing that, a name search, and the result is only accepted if exactly one project matches. Ambiguous means no match, not a guess. By default the Hangar project owner also has to line up with an author from the plugin.yml, which you can relax if it costs you matches.
If Modrinth is down, Hangar is skipped for that pass. Not because it wouldn't work, but because every plugin would count as unmatched, and Hangar matching is one search request each. Sixty requests to another service because a first one had a bad minute is not a trade worth making. The pass says so and tries again later.
Whatever nothing matches is listed by /pm update list, so it's clear what still needs doing by hand rather than quietly looking up to date.
Why updates are parked instead of installed
No plugin can safely replace a jar underneath a running server. The classloader is still holding the old classes, listeners are registered against objects from it, scheduler tasks are mid-flight, and other plugins may be holding references of their own. Anything that claims otherwise is gambling with your uptime.
So a download goes to Bukkit's update folder, the one configured as update-folder in bukkit.yml, and the server installs it on its next start. Downloaded now, active after your next restart. BungeeCord and Velocity have no such folder, so there PlugMaster keeps its own and applies the parked files as the proxy shuts down, which comes to the same thing.
If your update-folder is set to an empty value, the server hands back the plugins folder itself, and writing there would overwrite the jar that's currently running. PlugMaster notices that and falls back to plugins/update/.
What gets checked before anything is parked
A download is four things away from being trusted, and it has to pass all four.
- Size, during the transfer. The limit is enforced while bytes are arriving, not measured once the file is already on disk. A broken or hostile endpoint gets cut off at
max-download-megabytesinstead of filling the disk first. TheContent-Lengthheader is checked up front too, but headers are allowed to lie, so both. - It has to be a zip. Anything under 1 KB or without the
PKmagic bytes is an error page, not a jar. A Cloudflare interstitial returned with HTTP 200 is a real thing that happens. - The checksum. Modrinth publishes SHA-1 per file, Hangar SHA-256. Whichever the source gave is verified against the bytes that arrived, and a mismatch means discarded.
- The plugin inside is the right plugin. The
plugin.ymlin the downloaded jar has to name the plugin it's meant to replace. This is what catches a name search that went somewhere it shouldn't have, and it's why a source without a hash still can't get a foreign jar parked under your plugin's file name.
Anything that fails is deleted, not parked, and you're told which check it failed.
Already-parked updates are recognised as such, by hash first and by the version in their own plugin.yml otherwise. So 1.1 sitting in the update folder won't block 1.2 when it appears, and a newer jar you dropped in by hand won't be overwritten by an older one.
Runtime control
/pm enable and /pm disable use nothing but public Bukkit API. Disabling also tells you which loaded plugins list the target as a dependency, so you find out before they start throwing, not after.
/pm unload and /pm reload are the fragile pair, and they're honest about it. There is no public API for removing a plugin from the server, so this reaches into private SimplePluginManager and SimpleCommandMap fields. What gets cleaned up: event handlers, scheduler tasks, registered services, the plugin's commands and their aliases, its entries in the manager's own registries, and finally its classloader, which is closed so the jar is actually released and the classes can be collected.
Order matters there. Everything reaching outward is unregistered first and only then is the plugin removed from the registries, so a failure partway through can't leave a plugin that's gone from the manager but still answering events.
Which internals those are depends on the server. Bukkit, Spigot and Paper up to 1.20.4 keep their plugins in SimplePluginManager. Paper 1.20.5 and newer run their own plugin system and leave that class as a shell that forwards everything, so the real registers sit in PaperPluginInstanceManager instead, with a dependency graph and a classloader registry of their own that both have to be cleaned out. PlugMaster carries both paths and picks whichever actually holds the plugin.
If it finds neither, it refuses and says so, before the plugin has been touched. Nothing gets disabled on the way to finding out it cannot be unloaded.
Even where it works, a reload is best effort. A plugin that handed references to itself to something else can leave traces behind whatever gets cleaned up. For a plugin you've just updated, a restart is still the clean route. This is for the 2am case where a restart is the expensive option.
Installing a plugin you do not have yet
Off by default. downloader.enabled: true turns it on, and plugmaster.download gates it separately from everything else.
/pm search worldedit list matching plugins with their ids
/pm download worldedit fetch the newest fitting version into plugins/
/pm load worldedit.jar start it, or leave it for the next restart
The search only offers plugins that fit: Modrinth is asked for plugins tagged with a loader this server runs and, while strict-game-version is on, for the server's own version. The download goes through the same four checks an update does, and refuses on top of that if the jar turns out to hold a plugin you already have.
It stops at downloading. The new jar sits in plugins/ and does nothing until you load it or restart, because a plugin that starts things you did not ask for is not what you want at 2am.
Modrinth only. Hangar has no comparable search-and-fetch path, and guessing at one would mean guessing at which project a name refers to.
Metrics
PlugMaster reports anonymous counts to bStats: server software and version, Java version, player count, and which of PlugMaster's own switches are on. No names, no addresses, nothing about which plugins you run.
Switch it off in plugins/bStats/config.yml with enabled: false - that covers every bStats plugin on the server at once. Bukkit family only; the proxy side reports nothing.
Commands
Everything is /pm, with /plugmaster as the full name.
| Command | What it does |
|---|---|
/pm list | Every loaded plugin, green for enabled and red for disabled |
/pm info <plugin> | Version, authors, description, website, dependencies, soft dependencies and registered commands |
/pm enable <plugin> | Enable a loaded but disabled plugin |
/pm disable <plugin> | Disable it, and name anything that depends on it |
/pm load <file.jar> | Load a jar from the plugins folder |
/pm unload <plugin> | Remove a plugin from memory, classloader included |
/pm reload <plugin> | Unload and load the same file again |
/pm update check | Ask Modrinth and Hangar what has moved |
/pm update install [plugin] | Download and park. No argument, or all, does everything pending |
/pm update list | The plugins nothing could be matched to |
/pm update reload-config | Re-read config.yml, rebuild the HTTP client and reschedule the timer |
Plugin names with spaces work; everything after the subcommand is joined back together.
Update checks run on a daemon thread of their own, never on the server scheduler, so a slow API can't hold up a tick. That's also what makes the plugin work on Folia without touching a scheduler API.
BungeeCord, Waterfall and Velocity get the /pm update ... half only. Their plugin managers are built differently - Velocity wires its plugins up once at startup through Guice and has no runtime unload at all - and switching individual plugins on and off at runtime on a proxy is unusual enough that the extra reflection wasn't worth carrying.
Permissions
Everything defaults to OP.
| Node | Grants |
|---|---|
plugmaster.* | All of the below |
plugmaster.update.admin | Every /pm update ... subcommand, plus the login notice |
plugmaster.control.info | /pm list and /pm info |
plugmaster.control.enable | /pm enable |
plugmaster.control.disable | /pm disable |
plugmaster.control.load | /pm load |
plugmaster.control.unload | /pm unload |
plugmaster.control.reload | /pm reload |
Admins holding plugmaster.update.admin are shown pending updates when they log in, and straight away if they're already online when a check finds something. Console log lines get lost; a message on join doesn't.
Configuration
The listing below shows the layout only. The same file with every option documented in place ships inside the jar and can be read at config.yml on Codeberg.
Click to expand the config.yml layout (29 lines)
check-on-startup: true
check-interval-hours: 12
auto-download: false
channel: release
strict-game-version: true
notify-admins-on-join: true
ignore:
- PlugMaster
modrinth:
enabled: true
bulk-check: true
hangar:
enabled: true
require-author-match: true
timeout-seconds: 20
max-download-megabytes: 200
The first check runs 30 seconds after start, so it isn't competing with everything else a server does while booting.
auto-download is off on purpose. Left off, PlugMaster only ever reports, and every download is something you asked for. Turn it on once you trust what it's matching.
What has been tested
The platform-independent core has 72 tests: downloads against a real HTTP server, the size limit enforced mid-stream, and rejection of error pages, wrong checksums and foreign jars.
The commands were then run against real servers - Paper 1.21.11 and Paper 1.20.6, headless, driving list, info, unload, load, reload twice over and update check from the console. Both came back with no exceptions, and the reloaded plugin re-ran its own startup each time.
The downloader was run the same way: refused while switched off, enabled through /pm update reload-config, then searched, downloaded a real plugin into plugins/ and started it with /pm load.
Velocity 3.5.0 went through the same routine on a live proxy: a real Velocity plugin recognised by its velocity-plugin.json, matched on Modrinth, the update downloaded, parked and genuinely swapped in as the proxy shut down, plus a search and a download of a plugin that was not installed at all.
Known limitations
Worth reading before you rely on it.
- BungeeCord and Waterfall have not been through a live proxy. The update half is shared code that the Bukkit and Velocity sides exercise, but the BungeeCord entry point itself is unproven.
- The Hangar field names are reconstructed, not verified against a live response.
downloads,fileInfo.sha256Hashandnamespace.ownercame from secondary sources. The code is deliberately defensive, so a field that isn't there costs the verification rather than the run, but treat Hangar matching as the less proven of the two sources. - Unload and reload reach into server internals, not API, on both kinds of server. A future version can move them; PlugMaster then refuses rather than half unloading something.
- On Paper the command map may keep an entry. Paper backs it with its own command system, and that view refuses removal in some builds. The unload still goes through - the old command just answers until the next restart, and PlugMaster tells you.
- Plugins that aren't published on either service can't be matched. That's not a bug, it's the whole reason
/pm update listexists.
Bug reports are genuinely useful. /pm update check output and your server version are usually enough.
Support
Made and maintained by LucasTHCR.
Questions, bug reports and feature ideas go to dc.gg/paperstream. Source is on Codeberg.
License
Copyright (C) 2026 LucasTHCR
PlugMaster is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
Short version: use it, study it, change it, pass it on. Anything you distribute that's built on it has to be GPLv3 too and has to ship its source.
Сервер для плагина PlugMaster - как у профи
Плагин PlugMaster создан для серверов: на своём сервере вы настраиваете его под себя и решаете, кому играть. Создать сервер с плагином PlugMaster для друзей можно за пару минут - BungeeHost всё уже подготовил.
