Skip to content
← All posts
4 min read

How an anticheat can detect the cheat client itself, not just the cheating

Most anticheats wait for a player to fly, reach or aimbot. There is an older trick that identifies the client on join, by asking it to render text only a cheat client knows how to translate.

client detectiondetection engineering

Almost every anticheat you can buy works the same way: it watches what a player does. It models movement and flags impossible physics. It raytraces attacks and flags impossible reach. It counts clicks and flags impossible CPS. That approach is sound, and it is most of what any serious anticheat should be doing.

It also has a structural weakness. Behavioural detection needs the cheater to cheat first, and modern clients are tuned specifically to stay under those thresholds: a killaura that only snaps within legal reach, a fly that respects gravity for a few ticks at a time. You catch them eventually, but "eventually" is measured in fights your honest players already lost.

There is a second question worth asking, and you can ask it the instant a player connects: what client is this?

Language files are a fingerprint

A Minecraft client renders text through a translation table. The server can send a translatable component (a key such as key.category.movement) and the client replaces it with whatever that key maps to in its language files. Vanilla knows the vanilla keys. It does not know anything else, and when a key is missing, the client renders the raw key back, unchanged.

A cheat client ships its own language entries for its own UI: its module names, its keybind categories, its config screens. So if you ask a client to render a key only a particular mod defines and it hands back rendered, human-readable text, you have not caught a behaviour. You have caught the client admitting, in its own handwriting, that the mod is installed.

That is the whole idea:

  • Raw key echoed back → the client has never heard of it → clean.
  • A rendered, human-readable string → the client has a translation for it → that mod is present.

Getting the client to answer

The obvious problem is that you need the client to render a component and then tell you what it rendered. Chat does not work; the client renders locally and never reports back.

The anvil does. When a player renames an item in an anvil, the client sends a NAME_ITEM packet containing the rendered text. So:

  1. Open an anvil window for the player.
  2. Put an item in it whose name is the translatable component you want to test.
  3. The client renders the name and echoes it back in NAME_ITEM.
  4. Compare what came back with the key you sent.

Done naively this flashes a GUI in the player's face, which is unacceptable. Done properly, with the window open, the slot contents and the window close packed into a single bundle so they are applied in one tick, the player sees nothing at all. No GUI, no flicker, no input eaten.

The part everybody gets wrong: the spoof trap

Once this technique is known, the counter is obvious. A cheat client can simply refuse to translate: strip its own language keys, or detect the probe and echo the raw key back like vanilla would. Now your clean signal means nothing, because "clean" and "hiding" look identical.

The fix is to include a key that no honest client can possibly translate, invented for this purpose. Vanilla echoes it raw. Any client that is faking honesty by echoing everything raw also echoes it raw, which is fine. But a client that tries to look maximally vanilla by translating known keys and passing through unknown ones will get caught the moment its behaviour diverges from the real vanilla baseline on a key it has no entry for.

In practice you want both directions covered: a trap key that proves the client is echoing honestly, and a comparison that tolerates the protocol's quirks. The rename field truncates at 50 characters, so a naive equality check on a long key produces a false "translated" result on a perfectly honest client. Compare with a prefix test, not equality. This detail is the difference between a working check and one that bans vanilla players.

What this does not catch

Be honest about the limits, because a client-detection check that is oversold is worse than none:

  • Clients that hardcode their UI text instead of using translation keys have very few keys to find. Some popular clients fall into this category, and no amount of key-list tuning changes that. A second detection vector (enumerating the plugin channels a client registers) covers part of this gap.
  • A recompiled client with renamed keys is invisible to a fixed list. This is a fingerprint database problem, the same arms race as antivirus signatures.
  • Key lists leak. If a detection list ships in readable form, cheat authors will read it and strip exactly those entries. The list has to live somewhere they cannot get at it. How we do that is not something we publish.

Why it is worth doing anyway

Because it changes when you catch someone. Behavioural detection is a probability that accumulates over a session. Client fingerprinting is a yes/no question answered in under a second, before the first hit connects, with a confidence level behavioural checks take a long time to reach.

The right architecture uses both. Fingerprinting catches the lazy majority instantly and cheaply. Behavioural checks catch the careful minority who compiled their own client, and those are the players your movement and combat models were built for in the first place.