Securing Game Data in the Agentic Age

Scott Davis ยท October 2026

Client-side protection has always slowed attackers down without stopping them. AI is making the slowdown barely matter.

To run, a game has to keep a lot of information on the player’s computer that the player is not supposed to use. It knows where every monster is, including the ones nobody has walked up to yet. It knows the real hit points behind a health bar that only shows a sliver of red. It holds the keys that encrypt its own network traffic. The player is not meant to see any of this, but the game needs it to work, and anything the game can read, the person who owns the computer can read too. While this has always been true in the past, what is new is how cheap it has become to take advantage of it with AI.

Why this can’t be fixed on the client

Any value the game uses, it has to have in plain form the moment it uses it. The processor cannot work on scrambled data, so the real value is sitting in memory whenever the game needs it, and whoever controls the machine can read memory. Encrypting the data on disk or scrambling the program file does not change that. It changes when and where the real value shows up, not whether it shows up.

A developer protecting data on the player’s machine is asking that machine to keep a secret from the person using it, while giving that person everything they need to uncover it. No amount of cleverness in the protection gets around that.

Why encrypting the data only buys time

The usual approach is to ship the data encrypted and have the game decrypt it when it needs it. The key is usually not written down in a file. The game either works it out or gets it from the server, but either way it has to hold the key, because it cannot decrypt without it. Anyone who can read the game’s memory gets to watch it decrypt.

When the encryption is lightweight, and for performance it usually is, watching the game decrypt hands you the key directly. Many games scramble their data with something like an XOR, where each byte is mixed with the key in a way that undoes itself. If you can see the scrambled bytes going in and the real bytes coming out, lining the two up gives you the key. There is nothing to guess. The game does the hard part and you read the result. Once you also recover the names the game uses for its own data, you know which bytes are the health value or the position, and you read them straight out, with the key sitting right next to them.

Obfuscation only slows people down

Since the real value always ends up readable, the practical goal is to make it annoying to find. Developers move things around every patch. They shuffle constants, change offsets so last month’s numbers point at nothing, change how the key is built, swap the key on a timer, and move where the key is stored. None of this hides the value. It breaks whatever the attacker worked out last time, so they have to work it out again.

That is the whole plan: cost the attacker time. It worked as long as a person had to spend that time, because the people who can do this are rare and have better things to do. A protection you can beat in a weekend, and have to beat again next patch, is still worth something as long as a human has to spend the weekend.

What changes in the agentic age

A person no longer has to spend an entire weekend trying to understand assembly. Give an AI model a copy of the running game’s memory and it will work out the same protection in a few minutes, where a skilled person would have taken hours or days. It writes the steps down as a script while it does it.

We tried this on a real game. A general-purpose model with safety guardrails, not a tool built for cheating, reconstructed the game’s startup decryption from a memory snapshot in about four minutes. Nothing about the request set off a guardrail, because reading data out of a program looks like ordinary reverse engineering, not an attack on anyone.

It is fast because the hard part used to be reading compiled code, and that is exactly what the model is good at. A disassembler turns the game into thousands of lines of assembly. A person used to have to sit with that and figure out what each piece was doing. The model just reads it, points at the part that copies and decrypts the data, and rewrites it as a few clear lines with the key spelled out.

This changes what a patch is worth. A patch used to buy the developer a fresh round of work for every attacker. Now the attacker runs the script the model wrote, and if the patch moved things around, the model redoes the work in a few minutes and updates the script. One person does it once, shares the script, and everyone else just runs it. Shuffling constants every patch barely helps when replaying the attack is this cheap.

The heavier options, and what they actually get you

There are heavier options, and it is worth being clear about who each one stops. Oblivious RAM moves a value to a new spot and re-encrypts it every time it is touched, so someone who can only see which memory addresses were accessed learns nothing. That helps against someone watching the memory bus or a remote storage server. It does nothing here, because the table that tracks where each value currently lives, and the decrypted value itself, are both in the same memory the attacker is already reading. It is also slow: tens of microseconds per access against about a nanosecond for a normal read, which rules it out for anything the game touches every frame.

Lighter tricks do better on cost. You store the value under a key that keeps changing and leave a fake value nearby as a trap, which beats anyone scanning memory for a number that stays the same. That stops most casual cheating. It does not stop someone who follows the game’s own data straight to the value, with the key a few bytes away, which is exactly what the automated approach does. That used to be a hard, skilled attack. Now it is not.

What actually holds up

The real answer has not changed, and it matters more now: do not put a secret on the client that the client then has to unlock in the open. Keep the authority on the server. Send the client only what it needs to draw what the player is seeing right now, and do not trust anything it sends back.

When you really do have to hide a value on the client, a few things make it harder to read:

  1. Use a different key for each session. The server hands it out at login, over the connection that is already encrypted, so there is no single constant baked into the build to grab once and share.
  2. Key it per monster or player. Mix each thing’s ID into the key so every value is scrambled differently. Cracking one tells you nothing about the others.
  3. Change the key often. Hand out a new one every few minutes so a key someone found stops working.
  4. Move where the key is kept. The client has to store it somewhere, and moving that spot each patch stops the search from being a one-time job.
  5. Watermark what each client gets. The server knows every session, so it can give each one tiny harmless differences. If a map or a data dump shows up online, those differences point to the account it came from.

Treat all of this as raising the cost, not stopping the attack. The client still has to turn the hidden value into a real number to use it, and an attacker can grab that number the moment it is written, or grab the key and recompute it. Moving and rotating just change where and when. The one thing that still pays off is watermarking, because it turns a leak into a trail back to an account, and that works even when pulling the data is free.

Our approach: a value that never sits still

Our own answer leans on the one thing the attacker is counting on, which is that a value stays put long enough to find. We wrap each protected number in a small type that never leaves it in the same state twice. The real value is stored combined with a key, the key is replaced on every write, and every so often the whole thing is copied to a fresh location in memory and the old copy is wiped. A checksum sits next to the stored value, so any edit to it fails the next read, and a plain decoy is kept alongside, so a memory scanner has something to find and change that does nothing.

The goal is not to make the value unreadable, which is impossible for all the reasons above. The goal is to make a lasting cheat too expensive to keep working. A scanner looking for a stable number finds the decoy. A reader that follows the data to the real value can still combine the current key with the current cell and recover it, but by then the key has changed, the cell is about to move, and whatever address or offset the attacker wrote down is already stale. Even with an AI doing the work, nobody solves this once. They solve it again on every read, every write, and every move, which is the repeated effort automation was supposed to save them.

For the game the cost is tiny, a few cheap operations and an occasional small allocation, which is fine even for a value read every frame. For the attacker the same small cost is paid over and over, and that asymmetry is the whole point.

The stored word, the checksum, and the decoy. The key rotates on write, and the cell moves every 32 accesses.

public sealed class MaskedInt {
    const int RelocateEvery = 32;
    int[] _cell = new int[2];   // [0] = value XOR key
    int _key;
    int _decoy;
    int _opsSinceMove;

    public int Value {
        get {
            int v = _cell[0] ^ _key;
            if (_cell[1] != Check(v, _key)) {
                OnCheat("encrypted value was modified");
                v = _decoy; Store(v);
            } else if (_decoy != v) {
                OnCheat("decoy was modified");
                _decoy = v;
            }
            Tick(v);
            return v;
        }
        set { _key = NewKey(); Store(value); _decoy = value; Tick(value); }
    }

    void Store(int v) { _cell[0] = v ^ _key; _cell[1] = Check(v, _key); }

    void Tick(int v) {
        if (++_opsSinceMove < RelocateEvery) return;
        _opsSinceMove = 0;
        var old = _cell;
        _cell = new int[2]; _key = NewKey(); Store(v);
        Array.Clear(old);
    }

    static int NewKey() { int k; do k = Random.Shared.Next(); while (k == 0); return k; }

    static int Check(int v, int key) =>
        (int)(((uint)v * 0x9E3779B1)
            ^ BitOperations.RotateLeft((uint)key, 13)
            ^ 0x5BD1E995);
}

None of this moves the trust boundary. It raises the price of staying attached to a value that keeps slipping away, and it buys time by making every patch and every frame cost the attacker a little more. The secret is still on the client, so the real protection still lives on the server.

Conclusion

Client-side protection only ever slowed attackers down, and now it barely does that. The skilled work it used to cost, finding the real value and the key next to it, is becoming cheap, repeatable, and easy to hand to someone else. The safe assumption now is that anything you put on the player’s machine is already available to anyone who wants it, because the people who want it have an assistant that will go and get it. The only thing you can actually keep is what the player’s machine never sees, and that lives on the server.