Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

classwhat it decideswhere it lives
Permissionwhat is being asked for: a Permission.Atom (a named capability with an Identifier) or a Permission.HasCommandLevel (a rung)both jars
Permissionsnine 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 shapeboth
PermissionLevelthe five rungs — all, moderators, gamemasters, admins, owners — and PermissionLevel.isEqualOrHigherThanboth
PermissionSetthe answer. An interface with one abstract method, PermissionSet.hasPermissionboth
LevelBasedPermissionSetthe ordinary answer: an interface with five constants, one rung eachboth
PermissionSetUnionthe OR of several sets, and the rule that a union may not contain a unionboth
PermissionCheckwhat a node demands: PermissionCheck.Require or PermissionCheck.AlwaysPassboth
PermissionProviderCheckthe Predicate Brigadier holds, over anything implementing PermissionSetSupplierboth
ChatAbilitiesholds the client’s own set, built by subtraction from local reasonsclient 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’s IntegratedServer.setGuestCommandAccess toggle. And the host’s LocalPlayer is no longer set directly by IntegratedServer.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.