Wrap any objects the player wishes to access and only give them access to your proxy object with restrictions on what can be accessed from the original object.
You can overwrite the globals by calling getfenv(fn) and then overwriting them there. You could have a wrap function that recursively “wraps” an object by creating a new proxy object for any instance / property accessed through the original.
The sandboxing would come by not allowing specific methods or properties to be used on certain instances or under other conditions.
local fnenv = getfenv(fn)
fnenv.game = wrap(game)
fnenv.Instance = wrap(Instance)
Here is an article on wrapping.
Annoying part is you will have to manually define every global since you can’t iterate over the fenv for specific key/value pairs like game because of how they do those environment globals.
You could make it error if a certain method is called,
__index = function(self, index)
if index == 'Kick' then
return error('You cannot kick a player!')
-- or if you're worried about it accessing a member that would be called kick
if typeof(rawget(self, index)) == "function" then return function() error("hey!") end end