Quick answer: Run mods in a restricted environment with os, io, debug, and package removed or whitelisted, set instruction/time limits, and expose only a curated game API.

You embed Lua so players can script mods, but a mod can call os.execute or read arbitrary files. Opening the full standard library hands untrusted code the same power as your game, which is a real security hole, not just a bug.

How to fix it

1. Strip dangerous libraries

Do not open the full stdlib for mod states. Remove or nil out os, io, debug, package, require, and loadstring/load so mods cannot reach the filesystem or load native code.

2. Run in a sandboxed environment

Give each mod a custom environment table (via _ENV or setfenv) containing only safe globals and your curated API. Code outside that table cannot touch engine internals.

3. Bound execution time and memory

Install a debug hook that counts instructions and aborts runaway scripts, and cap the Lua memory allocator. This stops a mod from hanging the game with an infinite loop or allocation.

4. Expose a deliberate API, not raw engine objects

Wrap engine calls in a thin API that validates arguments. Never hand mods raw pointers or unrestricted spawn/file functions that could crash or escape the sandbox.

Catching the ones you can't reproduce

The hardest version of this to fix is the one you can't reproduce — it only happens on a player's hardware, OS, driver, or save state, under conditions that simply aren't present on your machine. A report that says “it crashed” or “it froze” gives you nothing to act on, so the bug survives release after release while quietly costing you players.

Automatic error capture closes that gap. Each failure arrives with its full stack trace, the device and OS, the build number, and a breadcrumb trail of what the player did right before it broke, so even a failure you have never seen becomes a specific, reproducible issue. Fold identical failures into one signature ranked by how many players each hits, and your worklist sorts itself worst-first instead of arriving as a stream of vague complaints.

This is where a tool like Bugnet earns its place. Its SDK captures every error automatically with the full stack trace plus device, OS, memory, build, and game-state context, folds duplicates into one grouped issue with an occurrence count, and ties each to the build it first appeared on — so you fix the problem that hurts the most players first and confirm it is gone when its signature disappears from the next release.

Ship the fix, watch the signature disappear from the next build. That's how you know it's really gone.