1.0.0
Compatibility
Required content
Changes
[1.0.0] — 2026-04-16
Initial release. Server-side Fabric 1.21.1 private chat channel for staff members, with zero hard mod dependencies beyond Fabric API.
Added
Core chat mechanic
/staffchat(or/sc) — toggles staff-chat mode for the caller. While toggled, every regular chat line the player sends is routed to staff only instead of global chat. Title pulse on toggle on/off + persistent actionbar● Staff Chat Modeso nobody forgets they're in stealth mode./staffchat <message>— one-off send without changing toggle state. Good for quick replies during normal play.@<message>quick prefix — typing@at the start of a regular chat line routes the rest to staff only. Works even with toggle mode off. The prefix character is configurable (chat.quick-prefix) — set to""to disable entirely, or change to#/~/ etc. to avoid clashes with mention syntax.- Permission node:
staffchat.true(default OP 4 fallback) gates sending AND receiving. Without a permissions plugin, OPs see the channel automatically; with LuckPerms you can grant it per-group.
Mention pings
@<playername>inside a staff message pings the mentioned player (must also holdstaffchat.true) with:- An actionbar flash (
★ Mentioned by <sender>) - A chime (default
entity.experience_orb.pickup, all configurable — sound id, volume, pitch, actionbar template)
- An actionbar flash (
- Self-mentions are skipped. Offline / non-staff names are skipped.
Discord webhook mirroring (opt-in)
- Built-in async webhook POST to a configured Discord webhook URL.
Every staff message is forwarded as the sender's name + avatar (via
https://mc-heads.net/avatar/<uuid>). Async executor so no tick lag; configurable retry budget on transient failures. - Off by default. Admins opt in by setting
discord.enabled = trueand fillingdiscord.webhook-url. - Privacy guarantee: staff messages never leak through other
chat-bridge mods. The mod suppresses the outgoing message at
ALLOW_CHAT_MESSAGE(so nothing in the normal chat pipeline sees it) and re-delivers it as a system message (not a chat message) to each staff recipient — chat-bridge mods that hook the chat broadcast pipeline therefore never observe it. The built-in webhook is the only intended Discord mirror for staff chat. allowed_mentions.parseis empty so a staff member typing@everyonedoes NOT actually ping the Discord server.- Misconfigured webhooks (4xx response) trigger a 5-minute log-spam suppression window so a stale token doesn't drown the console.
LuckPerms metadata prefix
- If LuckPerms is loaded, each sender's
[Prefix]metadata is resolved reflectively and prepended to their display name (soft dep — no LuckPerms = no crash). - Falls back to a config-based
prefixes { id = "..." }map keyed by the higheststaffchat.rank.<id>permission the player holds, in declaration order. Final fallback: blank.
Security
Lifted the defensive patterns from the PokeBuilder project:
- Permission re-checked on every message send — a staff member demoted between messages N and N+1 silently drops off the recipient list on N+1 (no error leaks the demotion).
- Input sanitizer strips C0 control characters (
\u0000–\u001F), strips the raw section-sign (\u00A7) to prevent bypassing the&code path, caps messages atchat.max-length(default 256), and rejects messages that are entirely formatting codes. - Rate limit: sliding 30-second window, configurable
security.rate-limit-per-30s(default 10). Exceeded → sender gets a "slow down" reply, message dropped server-side, audit log records the rate-limit event. staffchat.bypass.rate-limitbypass node useshasExplicit— OPs don't auto-bypass unless LuckPerms explicitly grants it.- Audit log (
logs/staffchat-audit.log): every staff send gets an 8-character base36 tx id + timestamp + sender + content + recipient count + outcome (DELIVERED / RATE_LIMITED / SANITIZED / DROPPED). Atomic append viaStandardOpenOption.APPENDso concurrent writes don't clobber. - Quick-prefix gated by the same
staffchat.truecheck — non-staff players typing@hellosee their message in global chat as normal, with no permission-leak signal.
Commands
/staffchat Toggle staff chat mode (staffchat.true)
/staffchat <message> One-off send (no toggle change) (staffchat.true)
/staffchat reload Reload config from disk (staffchat.admin.reload, OP 3)
/staffchat list List currently-online staff (staffchat.admin.list, OP 3)
/sc Alias for /staffchat toggle
/sc <message> Alias for /staffchat <message>
Configuration
config/staffchat/config.conf is a HOCON file. Notable knobs:
chat.format— placeholder-based message template ({prefix} {sender} {message} {time})chat.quick-prefix— the@character that triggers quick-send; set to""to disablechat.toggle-indicator+chat.toggle-on-title/chat.toggle-off-title— visual polish on mode flipmentions.*— ping behavior on@usernameinside staff messagesdiscord.*— the opt-in webhook mirrorprefixes { id = "..." }— fallback prefix map when LuckPerms is absentsecurity.rate-limit-per-30s/security.log-*— anti-spam + auditpersist-toggled-state— remember who had /staffchat on across restarts (stored indata/staffchat/toggled.json)message-locale—"auto" | "en_us" | "fr_fr" | ...
Message overrides go in a separate messages { ... } block. The mod
only persists your actual deltas there (not the entire locale base),
so the config file stays small.
i18n
English ships bundled. Drop any config/staffchat/lang/<locale>.json
to add a new language without rebuilding the jar. Resolution order:
config-dir → bundled resource → bundled en_us fallback.
Permissions (summary)
| Node | Default | Purpose |
|---|---|---|
staffchat.true |
OP 4 fallback | Send + receive in the staff channel |
staffchat.bypass.rate-limit |
false (explicit only) | Ignore the rate limit |
staffchat.admin.reload |
OP 3 | /staffchat reload |
staffchat.admin.list |
OP 3 | /staffchat list |
staffchat.rank.<id> |
— | Fallback prefix tier when LuckPerms absent |
Requirements
- Minecraft 1.21.1
- Fabric Loader ≥ 0.16.0
- Fabric API
- Java 21
Everything else is bundled in the jar:
- Fabric Permissions API (v0.3.1)
- SpongePowered Configurate HOCON (v4.1.2)
- Typesafe Config (v1.4.1)
LuckPerms is optional — prefix metadata is honored if present, but the mod works without it.
No Cobblemon / Impactor / Mega Showdown dependency — StaffChat runs on any vanilla Fabric 1.21.1 server. Happy to coexist with PokeBuilder if both are installed.
Notes for server operators
- If you already run a Discord chat-bridge mod (Styled Chat,
ServerSideUtils, simple-discord-bridge, etc.), staff messages
will NOT be mirrored by it because we suppress at
ALLOW_CHATphase. Use the built-indiscord.*webhook config to opt into exactly the Discord channel you want staff mirrored to. /op <name>alone gives that player the staff channel (OP 4 fallback onstaffchat.true). To restrict to specific players or groups, install LuckPerms and managestaffchat.trueexplicitly.data/staffchat/toggled.jsonis intentional — toggled state survives routine server restarts. Setpersist-toggled-state = falsein config to opt out.
Optional dependencies
Projects on Modrinth are automatically available through a Maven repository for use with JVM build tools such as Gradle. To learn more about the Modrinth Maven API, click here.
Note: When available, you should use the creator's maven repo instead as it will have transitive dependency information that the Modrinth Maven API does not. You may also end up with duplicate dependencies if you use a mix of Modrinth and non-Modrinth Maven repositories for your dependencies, because the group identifier will be different when served through the Modrinth Maven API.
Maven coordinates:
Version ID:
build.gradle:
repositories {
exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
}
}
// forRepositories(fg.repository) // Uncomment when using ForgeGradle
filter {
includeGroup "maven.modrinth"
}
}
}
// Standard Gradle dependency
dependencies {
implementation "maven.modrinth:wYn2zpCf:fBSEgu5A"
}
// Legacy Loom dependency
dependencies {
modImplementation "maven.modrinth:wYn2zpCf:fBSEgu5A"
}

