Permissions
Verified against Minecraft 26.3 · Part XIII · You are a level-four operator, you type
/msg, and the server’s own answer to may this player send a chat command is no — because an operator’s permission set grants exactly one thing that is not a command level.
Op yourself to four, the highest rung there is, and ask the server whether
you hold Permissions.CHAT_SEND_COMMANDS — one of five named capabilities
the class Permissions holds, each of them a Permission.Atom. It says no.
Not because you are restricted — because the set you were given is a
LevelBasedPermissionSet, and its answer to any permission that is not a
command level is false, with one hard-coded exception. The chat atoms
live only in the client’s set, which the server never sees and never
sends. There are two permission universes in this game, and they overlap only where the client runs the server’s checks, its rungs and its entity-selector atom, against its copy of your op level.
That is the shape of the model, and the thing to unlearn: a permission is not an integer. The five levels are there, numbered nought to four and stored as integers in ops.json, but a command node does not ask for a number. It asks a
PermissionSet a question, and a set is free to answer however it likes.
The cast
| class | what it decides | where it lives |
|---|---|---|
Permission | what is being asked for: a Permission.Atom (a named capability with an Identifier) or a Permission.HasCommandLevel (a rung) | both jars |
Permissions | nine permissions as constants — four rungs and five atoms; the client mints one more of its own. The plural class is the catalogue; the singular one is the shape | both |
PermissionLevel | the five rungs — all, moderators, gamemasters, admins, owners — and PermissionLevel.isEqualOrHigherThan | both |
PermissionSet | the answer. An interface with one abstract method, PermissionSet.hasPermission | both |
LevelBasedPermissionSet | the ordinary answer: an interface with five constants, one rung each | both |
PermissionSetUnion | the OR of several sets, and the rule that a union may not contain a union | both |
PermissionCheck | what a node demands: PermissionCheck.Require or PermissionCheck.AlwaysPass | both |
PermissionProviderCheck | the Predicate Brigadier holds, over anything implementing PermissionSetSupplier | both |
ChatAbilities | holds the client’s own set, built by subtraction from local reasons | client only |
net/minecraft/server/permissions holds every row above but ChatAbilities, which is the client’s and sits with the client’s chat code: a small package that changes how everything above it is written.
A question, an answer, and a check
Three types do three jobs that a single integer used to do, and keeping them apart is what makes the rest legible.
classDiagram
class PermissionProviderCheck {
PermissionCheck test
}
class PermissionCheck {
<<interface>>
boolean check(PermissionSet)
}
class Require["PermissionCheck.Require"] {
Permission permission
}
class AlwaysPass["PermissionCheck.AlwaysPass"]
class PermissionSet {
<<interface>>
boolean hasPermission(Permission)
}
class Permission {
<<interface>>
}
class Atom["Permission.Atom"] {
Identifier id
}
class HasCommandLevel["Permission.HasCommandLevel"] {
PermissionLevel level
}
class LevelBasedPermissionSet {
<<interface>>
}
class ChatAbilities
PermissionProviderCheck --> PermissionCheck : the check
PermissionCheck <|.. Require
PermissionCheck <|.. AlwaysPass
Require ..> PermissionSet : asks the source's set
Require --> Permission : the question
Permission <|.. Atom
Permission <|.. HasCommandLevel
PermissionSet <|-- LevelBasedPermissionSet
PermissionSet <|.. PermissionSetUnion
ChatAbilities --> PermissionSet : holds one
The three roles as types — the check a node holds at the top, the question it asks, and the set that answers; a hollow arrowhead reads as “is a kind of”, a solid arrow as “holds”, and the one dotted arrow without a hollow head is the single call that joins them.
Permissions holds nine, and the split is four to five:
four Permission.HasCommandLevel constants — one per rung above zero — and
five Permission.Atoms, of which one is the entity-selector gate and the
other four are the chat abilities below.
The question is data: both shapes are records, both are codec-dispatched
over BuiltInRegistries.PERMISSION_TYPE, and Permission.CODEC accepts an
atom written as a bare identifier as well as the full dispatched form. The answer is behaviour: PermissionSet has one abstract method, so every set in the game above is a lambda or a small object, and there is no set-of-permissions data structure anywhere but Permissions.CHAT_PERMISSIONS and the client’s chat code that copies it into ChatAbilities. The check is what
a command node carries: Commands.hasPermission wraps a PermissionCheck
in a PermissionProviderCheck, and that predicate is what Brigadier
consults.
Ninety-seven call sites of Commands.hasPermission, which is every requirement predicate on every command node in the game. Ninety-six are server-side
command registrations; the ninety-seventh is on the client, in
ClientPacketListener’s node builder.
Two things about LevelBasedPermissionSet decide most of this page. It is
an interface with five constants, not a class carrying a level, so
LevelBasedPermissionSet.GAMEMASTER is a singleton and comparing two sets
is comparing two references. And its LevelBasedPermissionSet.hasPermission
tests a Permission.HasCommandLevel against its own rung, grants
Permissions.COMMANDS_ENTITY_SELECTORS from gamemaster upward as a special
case written into the method, and returns false to every other atom.
An operator does not have everything; an operator has a number and one
exception.
Union is where that bites, and the rule is what its name says:
PermissionSet.union falls through to PermissionSetUnion, which ORs its
operands, so a set holding an atom and a set holding a rung together satisfy
both. The exception is the case the server uses.
LevelBasedPermissionSet.union of two level-based sets never builds a
PermissionSetUnion at all, and it does not take the higher of the two: both
branches of the override return the lower-levelled set, so it is a
minimum. CommandSourceStack.withMaximumPermission is that union, which for these sets takes the minimum and so works as the ceiling its name promises — which is how a function body is held down to gamemaster however high the caller sits
(functions and
macros).
Where a set comes from
MinecraftServer.getProfilePermissions is the whole of it — in
singleplayer through its override, IntegratedServer.getProfilePermissions
— and it returns a LevelBasedPermissionSet, never a union, never an atom
set. It is consulted afresh every time ServerPlayer.permissions is called;
nothing is cached on the player.
Its cascade is short. Not on the operator list at all, and you get
LevelBasedPermissionSet.ALL — rung zero, and marked deprecated where it is declared. On the
list, and the entry’s own set wins — ServerOpListEntry holds a
LevelBasedPermissionSet, built when the file was read from the integer the file stores, which for an entry /op wrote on a dedicated server is the op-permission-level property as it stood then (the management API’s operator methods can write any level). Singleplayer’s override answers
first. The owner gets LevelBasedPermissionSet.OWNER when the world allows
cheats and rung zero when it does not; any other player gets
LevelBasedPermissionSet.GAMEMASTER when the world allows cheats and the
host has switched on Command Access for other players, rung zero when
cheats are on without it, and the cascade above when cheats are off.
The integer survives at every edge of that model, which is worth knowing
before you go looking for a permissions file. ops.json stores a number.
server.properties stores a number. The op level reaches the client as one
of five values on ClientboundEntityEventPacket. And
PermissionLevel.byId clamps rather than failing, so an ops.json
hand-edited to level 9 is an owner and level −1 is rung zero. What does not
exist anywhere is a data pack that grants a permission:
PermissionCheck.CODEC has exactly one consumer in the whole game,
ArgumentUtils.serializeNodeToJson writing the generated command report,
and both type registries are bootstrapped in code by PermissionTypes and
PermissionCheckTypes.
The requirement is consulted inside the parse
This is the design decision a server administrator feels most often, and it
is not a message-formatting choice. Brigadier evaluates a node’s requirement
predicate while walking the tree, so a node you may not use is a node that
is not there. Commands.getParseException then reports an empty parse range
as an unknown command and a non-empty one as an unknown argument.
An unopped player typing /give is told there is no such command. The
server cannot tell them otherwise without a second parse, and it does not
do one.
One permission is not settled by the parse alone.
Permissions.COMMANDS_ENTITY_SELECTORS is tested at parse time and again
when the selector is resolved against the world, because a parsed selector
outlives its parse — which is exactly what an /execute chain relies on, and
why entity selectors
owns the two checks and the route that reaches only the second.
Two more consequences worth naming. Commands.LEVEL_MODERATORS gates
nothing: the rung exists, is settable and is stored, and no vanilla
command asks for it. And the ninety-seven gates divide four ways. Ninety-three
name a level constant outright — sixty-eight gamemaster, sixteen admin, nine
owner. Two are the ternary in SeedCommand and VersionCommand, which drop
to Commands.LEVEL_ALL when the server is the integrated one, and those are
the only two appearances of that constant in the game. The last two name a
PermissionCheck rather than a rung: GameModeCommand.PERMISSION_CHECK,
which the end of this page is about, and the client’s own synthetic
restricted-command check.
What the client is allowed to believe
The client has permissions of its own, and they are not a copy of the
server’s — no packet carries a PermissionSet. It has four sources of
belief, and they behave differently enough to be worth separating. Three of
them the server sent or the machine decided; the fourth is the server’s own constants, which the two sides literally share.
The op level, which arrives on an entity event and is mapped by
LocalPlayer.handleEntityEvent onto one of the five sets — on an
integrated server too, where the host’s own LocalPlayer learns its level
the same way. Note the bottom
rung: level zero maps to PermissionSet.NO_PERMISSIONS, not to
LevelBasedPermissionSet.ALL. The two are indistinguishable in practice —
rung zero satisfies no level and no atom either — but the client’s copy is a
different object from the server’s.
The command tree, whose nodes were filtered for this player before being
sent (Brigadier and
commands for what the packet
carries). Two elisions ride that packet and they mean different things.
A node whose requirement the player failed is simply absent, and the
filter is recursive, so a gated literal takes its whole subtree with it. A
node that a no-permission source would fail is flagged
(ClientboundCommandsPacket.FLAG_RESTRICTED) — “this needs some
permission”, asserted independently of you. Commands computes that flag
against a source it builds once from PermissionSet.NO_PERMISSIONS, and
only for nodes that already survived your own filter.
Its own atoms. ClientPacketListener mints a single synthetic
Permission.Atom for restricted commands and keeps two sources over its one
dispatcher: the ordinary one, whose set is the player’s own OR-ed with
that atom, so restricted nodes still highlight and complete; and a
no-permission one. ChatAbilities holds the other client-only set, and it is built the opposite way round from everything else here — it starts from all
four chat atoms granted and lets each ChatRestriction remove some. The
four are what this client will do at all: send messages, send commands,
accept messages from other players, and accept system messages; a client with
the first taken away refuses before the server is ever asked (chat and
signing). Three of the four ChatRestrictions that can remove them are decisions the machine you are sitting at makes — two settings of the chat option and a launcher flag — and the fourth, ChatRestriction.DISABLED_BY_PROFILE, comes
from the account service: a user flag fetched with your profile. The
client’s chat permissions are never granted by the game server; they are
only ever taken away, and only ever from outside it.
The server’s own constants, read locally. The client runs server
permission checks against its own set in several places —
WorldOptionsScreen gates Allow Cheats in a hardcore world on Permissions.COMMANDS_OWNER and its game-rule, game-mode and difficulty buttons on Permissions.COMMANDS_GAMEMASTER, and KeyboardHandler gates two debug keys and reads a third — and almost all of those are the
client asking its own copy a question the server will ask again later. One
is shared outright. GameModeCommand.PERMISSION_CHECK, a
PermissionCheck.Require for gamemaster exported from the command class, is
read by its own command registration, twice by KeyboardHandler — the second is why F3+F4 refuses to open the switcher at all, with debug.gamemodes.error, rather than opening a greyed-out one — once by GameModeSwitcherScreen against LocalPlayer’s own set when it sends the switch, and once more by ServerGamePacketListenerImpl when the packet arrives. One constant, five references in four classes, two sides of the network: the one PermissionCheck both sides read; the other permissions they share are the rungs and the entity-selector atom.
Asking a question the client cannot answer
Put those two client sources together and you get the one thing the client
can diagnose: it can tell “you would need permission for this” apart from
“that is a typo”, for a command it was asked to send on your behalf. A
dialog button and a chat click event both route through
ClientPacketListener.sendUnattendedCommand, whose two callers are
Screen.clickCommandAction and an adapter ClientPacketListener builds for
itself. A sign does not. A sign’s click command reaches every client that loads the sign, as part of its text, but no client runs it: SignBlockEntity runs it on the server when the sign is used, and only when the sign’s allow_op_features flag is set,
through a CommandSourceStack it builds itself at a
hard-coded LevelBasedPermissionSet.GAMEMASTER. There is nothing for the client to vet, because the client never sends it. The
figure is the route the other two take.
flowchart TD
IN["an unattended command — a dialog button or a chat click"]
IN --> P1{"parses with your set, restricted atom added?"}
P1 -- no --> E1["PARSE_ERRORS"]
P1 -- yes --> P2{"any signable argument?"}
P2 -- yes --> E2["SIGNATURE_REQUIRED"]
P2 -- no --> P3{"parses with NO_PERMISSIONS too?"}
P3 -- no --> E3["PERMISSIONS_REQUIRED"]
P3 -- yes --> OK["NO_ISSUES — sent, no screen"]
E1 --> RUN["ConfirmScreen — its button sends it"]
E3 --> RUN
E2 --> SUG["ConfirmScreen — its button puts it in the chat box, or copies it"]
ClientPacketListener.verifyCommand’s three tests in their order, and where each answer leads — only the clean command leaves with no screen, and the signed one is never sent from here at all.
ClientPacketListener.verifyCommand parses the same string against both
sources and reads the difference. Succeeding with the ordinary source and failing with the no-permission one means some node on the path was gated, or an argument used a selector — which is as much as the
client can ever know, because it was never told which permission or whose.
Between the two parses sits a test that has nothing to do with permissions:
SignableCommand.hasSignableArguments. An unattended command cannot be
signed, so one that needs a signature is refused outright — its
ConfirmScreen offers to put the command in the chat box for you to send yourself,
or, when a screen is to follow or chat commands are off, to copy it.
Three of the four outcomes pop a confirmation screen; the fourth,
ClientPacketListener.CommandCheckResult NO_ISSUES, sends with no screen at all. So an unattended command that is
merely unusual is always shown to you first — but a clean one goes
straight out, and a sign’s command never came this way to begin with.
Which answers the operator at the top of this page. Their /msg goes
through, and it goes through because nothing on the server’s side of the
wire ever asks for Permissions.CHAT_SEND_COMMANDS: the /msg node carries
no requirement at all, the atom lives only in this client’s ChatAbilities,
and the one participant that consults it is the machine the player is
sitting at. The server’s no is real and it is never asked. That is what two permission universes buy — and where they overlap, at the rungs and the selector atom, is The server’s own constants, read locally above.
For a 1.21-era reader. PlayerList.setAllowCommandsForAllPlayers is gone: singleplayer answers through its own override,
IntegratedServer.getProfilePermissions, which gives a guest gamemaster behind the host’sIntegratedServer.setGuestCommandAccesstoggle. And the host’sLocalPlayeris no longer set directly byIntegratedServer.publishServer: it learns its level from the entity event like any other player.
Where to look
PermissionSet first — seventeen lines, and the whole model is in them.
Then LevelBasedPermissionSet for the two special cases that decide
everything, Permissions for its nine, and Commands.hasPermission for the one idiom every requirement in a command registration uses.
MinecraftServer.getProfilePermissions for where a player’s set is
decided, and ClientPacketListener.verifyCommand for the only place the client reasons about a permission it was never told about.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.