RE:NODE

Minecraft ops.json example: whitelist, bans and usercache

By RE:NODE Team
17 min read


A Minecraft server keeps its people in five JSON files in the server root: ops.json (operators and their level), whitelist.json (who may join), banned-players.json and banned-ips.json (who may not), and usercache.json (a lookup table from names to UUIDs). Every entry is keyed by UUID, not by name. The server reads them into memory at startup and writes the whole list back to disk whenever a command changes it, which is why a file you edited by hand while the server was running quietly reverts. The safe rule is short: use /op, /whitelist and /ban while the server runs, and edit the files only with it stopped.

A working ops.json looks like this:

ops.json
[  {    "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5",    "name": "Notch",    "level": 4,    "bypassesPlayerLimit": true  },  {    "uuid": "853c80ef-3c37-49fd-aa49-938b674adae6",    "name": "jeb_",    "level": 2,    "bypassesPlayerLimit": false  }]

The rest of this post goes through each file field by field, what the commands write, what a hand edit can do that a command cannot, how offline mode breaks every one of these lists at once, and - because the same search often lands here - what the level-type values in server.properties mean. The policy side - who should be an operator at all, and how to build ranks with LuckPerms - is in Minecraft whitelist and permissions; this post is about the files themselves.

The five files at a glance#

FileChanged byHolds
ops.json/op, /deopOperators, their level, slot bypass
whitelist.json/whitelist add/removePlayers allowed in when the whitelist is on
banned-players.json/ban, /pardonBanned accounts, reason, expiry
banned-ips.json/ban-ip, /pardon-ipBanned addresses, reason, expiry
usercache.jsonThe server, on every loginRecently seen names and UUIDs

All five are JSON arrays of objects. All five live next to server.properties, not inside the world folder, so they are shared by every dimension and survive a change of level-name. Start a new world for a new season and your operators, whitelist and bans carry over untouched. Delete the world folder and they are still there; delete the server root and they are gone, which is worth remembering when you decide what your backups cover.

They replaced plain-text files in 1.7.6, when Minecraft moved from names to UUIDs. If you find ops.txt, white-list.txt, banned-players.txt or banned-ips.txt on an old server, they are leftovers. A modern server converts them once and ignores them after that, and editing them does nothing.

The same files exist on vanilla, Paper, Spigot, Purpur, Fabric and NeoForge servers, because they belong to the vanilla server code that all of those are built on. Plugins can add their own lists on top - EssentialsX keeps per-player data, LuckPerms has its own storage - but the five files here behave the same on every one of them.

ops.json, field by field#

Each operator is one object with four fields.

FieldTypeMeaning
uuidstringThe account. This is what the server matches
namestringThe last known name. Cosmetic
level1 to 4Operator permission level
bypassesPlayerLimittrue/falseMay join when max-players is reached

The uuid is the only field the server uses to recognise a player. The name is written for your benefit and updated when the player logs in under a new name; a stale name does no harm. The reverse is not true: an entry with a missing or malformed uuid is skipped when the file loads, with no error you are likely to notice, so a hand-written entry with only a name simply does not exist as far as the server is concerned.

level is where hand editing earns its keep. /op <player> always writes the value of op-permission-level from server.properties, which defaults to 4. There is no argument to /op that sets a level, so if you want one player at level 4 and another at level 2, you either change op-permission-level before each /op and restart in between, or you write the number into the file. The short version of the four levels:

  • 1 - bypasses spawn protection and nothing else.
  • 2 - cheat-style commands such as /gamemode, /give, /tp and command blocks.
  • 3 - player management: /kick, /ban, /op, /whitelist.
  • 4 - everything, including /stop.

The full table and the reason the levels mean far less on a plugin server than they appear to - plugins usually grant their permissions to every operator regardless of level - are covered in the permissions post. On Paper, treat level as controlling vanilla commands only.

bypassesPlayerLimit lets that player join when the server already holds max-players people. /op writes false for a new operator and keeps the existing value when you re-op somebody, and no command sets it to true, so the file is the only place to switch it on. It is the setting that lets the admin get into a full server to deal with whatever filled it.

Two more interactions worth knowing. Operators bypass the whitelist completely, so an old operator entry is a way in that whitelist remove will not close. And /deop removes the entry entirely rather than lowering the level - there is no level 0 to demote somebody to.

whitelist.json#

whitelist.json
[  { "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Notch" },  { "uuid": "853c80ef-3c37-49fd-aa49-938b674adae6", "name": "jeb_" }]

Two fields, the same rules: uuid is matched, name is decoration. The file is consulted only when white-list=true in server.properties, and it is kept when the whitelist is switched off, so turning it back on later restores exactly the same set of people.

Whitelist is the one list with a reload command. /whitelist reload re-reads whitelist.json from disk, which makes it the only one of the five you can safely hand edit with the server running - as long as you run the reload before anything else writes the file. With enforce-whitelist=true, the reload also disconnects anyone online who is no longer listed.

When you run /whitelist add <name>, the server needs a UUID for that name. It checks usercache.json first, and if the name is not there it asks Mojang's profile service. That is why the command works for a player who has never joined, and why it fails with "That player does not exist" when the name is misspelled, was changed, or belongs to a Bedrock gamertag rather than a Java account.

banned-players.json and banned-ips.json#

The ban lists carry more fields, because a ban records who did it, when and why.

banned-players.json
[  {    "uuid": "36532b5e-c442-3dbb-a24c-c7e55d0f979a",    "name": "Griefer123",    "created": "2026-10-15 14:03:22 +0000",    "source": "Server",    "expires": "forever",    "reason": "Lava-cast over the spawn town"  }]
banned-ips.json
[  {    "ip": "203.0.113.45",    "created": "2026-10-15 14:05:10 +0000",    "source": "Server",    "expires": "forever",    "reason": "Ban evasion"  }]
FieldExampleMeaning
created2026-10-15 14:03:22 +0000When the ban was made
sourceServer, Rcon, a player nameWho issued it
expiresforever or a dateWhen it lapses
reasonFree textShown to the player on the disconnect screen

source is the name of whatever ran the command: Server for the console, Rcon for a remote console, or the operator's name. With no reason given, vanilla writes Banned by an operator.

expires is the interesting one. Vanilla's /ban has no duration argument and always writes forever, but the server does honour a date in this field. Write a timestamp in the same format as created - "2026-10-22 14:00:00 +0000" - and the ban stops applying after that moment. That is how temporary bans from plugins such as EssentialsX end up stored: in this same file, with a real expiry date. Get the format wrong and the date may not parse, so copy the shape of created exactly, including the space before the timezone offset.

The commands, all at operator level 3:

CommandEffect
ban <player> [reason]Adds to banned-players.json, kicks if online
ban-ip <player or address> [reason]Adds the address to banned-ips.json
pardon <player>Removes a player ban
pardon-ip <address>Removes an address ban
banlist [players or ips]Prints the current lists

ban-ip given a player name bans that player's current address, which is the convenient form when they are online. Address bans are a blunt tool. Many home connections change address on reconnect, mobile networks put thousands of people behind one address, and a VPN defeats them in seconds. Use an account ban first and an address ban as a supplement for a determined alt-account problem, and expect to pardon an innocent player occasionally.

Neither ban list has a reload command in vanilla, so hand edits to them need the server stopped.

usercache.json#

usercache.json
[  {    "name": "Notch",    "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5",    "expiresOn": "2026-11-15 14:03:22 +0000"  }]

This is not a permission file. It is a cache of names and UUIDs the server has resolved, refreshed every time a player logs in. Commands use it to turn a name into a UUID without asking Mojang, and so do player heads, /whitelist add and the ban commands for players who are offline. expiresOn is set about a month ahead; an expired entry is looked up again the next time it is needed. The file is capped at the most recently used thousand or so entries, so it stays small on a busy server.

Because it is only a cache, usercache.json is safe to delete with the server stopped - it is rebuilt as players join. That is occasionally the fix when a renamed player's old name keeps resolving to the wrong account, or when a switch between online and offline mode has left it full of UUIDs that no longer match anything. Do not hand edit it to "fix" a player's identity; the server will overwrite your change from the next login anyway, and the identity that matters is the one in the other four files.

Why hand edits get overwritten while the server runs#

The behaviour that catches everyone has a simple cause. At startup the server reads each list into memory. From then on, the copy in memory is the real one. When any command changes a list - /op, /deop, /whitelist add, /ban, /pardon, or a plugin calling the same code - the server serialises its in-memory list and writes the whole file again. Nothing it has not loaded survives that write.

startupchangewhole file rewrittenoverwritten at next write/op, /ban, /whitelistor a pluginYour hand editmade while runningops.json on diskread once at startList in memorythe copy that counts
Where a hand edit is lost

So editing ops.json in the file manager while the server runs appears to work - the file on disk shows your change - and then disappears the next time anyone is opped, banned or whitelisted. Your edit was never in memory, and the next write replaces the file with what was. The same applies in reverse: an edit made with the server running has no effect until it is loaded, which for ops and bans means a restart.

There is a nastier version. If you save one of these files with a JSON syntax error - a trailing comma after the last entry is the usual one, a missing quote the other - the server logs a warning when it fails to load the list and carries on with an empty list in memory. Your operators are no longer operators, your whitelist admits nobody but ops, and your bans are lifted. The next command that touches that list then writes the empty list back to disk, replacing your broken file and every entry in it.

The procedure that always works:

  1. Stop the server and wait for it to finish shutting down.
  2. Take a copy of the file you are about to edit, or a backup of the server.
  3. Make the edit. Keep it a valid JSON array: commas between objects, none after the last one, every key and string in double quotes.
  4. Paste the file into any JSON validator if you are unsure.
  5. Start the server and read the start of the log for load warnings.
  6. Run /op or /whitelist list or /banlist from the console to confirm the list holds what you expect.

For the whitelist alone there is a shortcut: edit, save, then /whitelist reload immediately.

On RE:NODE the file manager edits these files in the browser with syntax highlighting, which makes a stray comma visible before you save, and SFTP works with credentials generated per server if you prefer your own editor - SFTP and the file manager has the details. The console keeps command history, so the verification step is a couple of keystrokes. And a backup slot is there to take a snapshot before you edit; restoring one is a button, covered in Minecraft world backup and restore.

Offline mode and the UUID trap#

Every list here is keyed by UUID, and online-mode decides where UUIDs come from. That makes it the setting most likely to break all five files at once.

With online-mode=true, a player's UUID is issued by Mojang. It belongs to the account, survives name changes, and is what every command writes. These are version 4 UUIDs: the third group of characters starts with a 4, as in 069a79f4-44e9-4726-....

With online-mode=false, the server does not ask Mojang anything. It derives a UUID from the name itself, by hashing the text OfflinePlayer: followed by the exact username. These are version 3 UUIDs, and the third group starts with a 3. The same account produces two completely different identities:

ModeNotch's UUID
Online069a79f4-44e9-4726-a5be-fca90e38aaf5
Offlineb50ad385-829d-3141-a216-7e7d7539ba7f

The consequences follow directly:

  • Switching modes orphans every entry. An operator opped in online mode is not an operator after you switch to offline mode, and the whitelist admits none of its members. Their inventories, advancements and plugin data are keyed by UUID too, so they also appear to vanish - the old player files are still in the world folder, under the old UUIDs.
  • Name case matters offline. The hash is of the exact string, so Notch and notch are two different offline players.
  • A rename is a new player offline. There is no account behind the name, so a new name means a new UUID and an empty inventory.
  • Offline whitelisting can write the wrong UUID. On many versions, /whitelist add and /op still try Mojang's lookup first when the name belongs to a real Java account, and only fall back to the offline UUID if that fails. The entry then holds the online UUID and never matches the offline player who connects. The reliable fixes are to let the player join once with the whitelist off, so the cache holds their offline UUID, or to write the offline UUID into the file yourself.

You can compute an offline UUID with a few lines of Python. Java builds it with UUID.nameUUIDFromBytes, and this reproduces it exactly:

offline_uuid.py
import hashlib, uuid, sysname = sys.argv[1]h = bytearray(hashlib.md5(("OfflinePlayer:" + name).encode("utf-8")).digest())h[6] = (h[6] & 0x0F) | 0x30  # version 3h[8] = (h[8] & 0x3F) | 0x80  # IETF variantprint(uuid.UUID(bytes=bytes(h)))
bash
$ python3 offline_uuid.py Notchb50ad385-829d-3141-a216-7e7d7539ba7f

The one legitimate place for online-mode=false is a backend server behind a Velocity or BungeeCord proxy. The proxy authenticates with Mojang and, with forwarding configured, passes the real online UUID through to the backend - so the backend's lists hold version 4 UUIDs even though its own online-mode is off. If you see version 3 UUIDs appearing in usercache.json on a proxied backend, forwarding is not working and every player is arriving with a name-derived identity. Fix the forwarding before you do anything else; the Velocity network guide covers it. Bedrock players through Geyser and Floodgate are a third case again, with their own generated UUIDs - see Geyser and Bedrock crossplay.

Moving a server from offline to online mode properly means rewriting each list with the new UUIDs and renaming each player's data files in world/playerdata/, plus whatever plugins store per UUID. It is a migration, not a setting change, and it needs a backup taken first.

level-type in server.properties#

The other half of the question that brings people here is the line level-type=minecraft\:normal. It picks the world preset used when a world is generated. On 1.19 and later the values are namespaced:

ValueWhat you get
minecraft:normalStandard terrain. The default
minecraft:flatSuperflat, shaped by generator-settings
minecraft:large_biomesNormal terrain with biomes several times larger
minecraft:amplifiedExtreme vertical terrain
minecraft:single_biome_surfaceOne biome everywhere

The backslash before the colon is the Java properties file format: the server's own writer escapes every colon, so the file it generates says minecraft\:normal. When the server reads the file back, minecraft\:normal and minecraft:normal in a value mean exactly the same thing, so either form is fine to type. What is not fine is an unrecognised value - a typo such as minecraft:largebiomes - which on current versions gives you a normal world rather than a clear error, and you find out only once players are walking around it.

Older versions used bare words: default, flat, largeBiomes, amplified, and in some versions buffet or customized. Modern servers translate the common legacy names to the namespaced ones, so an old file still works, but write the namespaced form in anything new.

Three practical points:

  • It applies only at world creation. Once level.dat exists, the generator is stored in the world and changing level-type does nothing. To try another preset, change level-name to a new folder so the server generates a fresh world beside the old one.
  • `amplified` costs real CPU. Generating amplified chunks is noticeably slower than normal ones, and on a shared core that shows as lag spikes while players explore. Pre-generate the playable area before opening the server - world border and pre-generation explains how.
  • `large_biomes` is for big player counts. Spreading biomes out gives everybody room, at the price of long walks to find a specific one.

Seeds, generator-settings JSON for flat worlds and worldgen datapacks such as Terralith are covered in custom world generation, and the rest of the file in every server.properties key.

Troubleshooting the lists#

I edited ops.json and nobody is an operator. Either the server was running and overwrote your edit, or the JSON did not parse and the server loaded an empty list. Stop the server, open the file and check it.

A player is in whitelist.json but cannot join. Compare the UUID in the file with the one in usercache.json after they try to connect. A version 3 UUID on one side and version 4 on the other means an online/offline mismatch. Also check white-list is what you think it is: /whitelist on writes the setting to disk, so a stale hand-edited server.properties can disagree with what the server is doing.

`/whitelist add` says the player does not exist. Spelling, a recent rename, a Bedrock gamertag, or the server cannot reach Mojang's profile service. Have the player join once with the whitelist off, or add the UUID by hand with the server stopped.

A pardoned player is still banned. Check banned-ips.json. A player banned with ban-ip stays out after pardon, because the address ban is a separate list and needs pardon-ip.

Bans came back after a restart. Someone pardoned in the file while the server ran. The in-memory list still held the ban and wrote it back. Use /pardon.

The server is full and the admin cannot get in. Set "bypassesPlayerLimit": true on the admin's entry with the server stopped, or raise max-players - a value that only limits slots, not performance by itself.

FAQ#

Can I add an operator without the server running?

Yes. Write the entry into ops.json with the player's UUID, a level from 1 to 4 and bypassesPlayerLimit, keep the file valid JSON, and start the server. The UUID has to be right; look it up from a previous login in usercache.json or from any Mojang UUID lookup for online-mode servers.

Is the name field in these files used for anything?

Barely. Matching is by UUID, and the server refreshes the name when the player next logs in. A wrong or outdated name in an entry causes no problem as long as the UUID is correct; a correct name with a wrong UUID does nothing at all.

How do I make a temporary ban without a plugin?

Ban the player normally, stop the server, change that entry's expires from forever to a timestamp in the same format as created, and start the server again. The ban stops applying after that time.

Is it safe to delete usercache.json?

With the server stopped, yes. It is only a cache and is rebuilt as players join. Deleting it means the first command naming an offline player has to ask Mojang again.

Why does /op not give the level I set in ops.json?

It writes op-permission-level from server.properties every time, overwriting whatever level that player had. Set levels in the file with the server stopped, and avoid re-opping that player afterwards.

Does level-type change anything on an existing world?

No. The generator is fixed when the world is created and stored in level.dat. Changing the line only affects a world generated after the change, usually because level-name points at a new folder.