How to make a jail players can't reset out of

Jails and freezes that only move the character or set its walk speed end with a reset. How to hold players on the server instead.

warningLuaudit check: character-only-punishments

What it looks like

A jail, freeze, time-out or penalty box is applied only to the player's current character: the server moves the character into a cell with PivotTo, or sets its WalkSpeed and JumpHeight to 0, and nothing else remembers the punishment. A new character spawns at a normal spawn with normal speed, so resetting ends it. Some games turn off the reset button with StarterGui:SetCore("ResetButtonCallback", false) to stop this.

Luau
character:PivotTo(jailCell)
humanoid.WalkSpeed = 0

How to detect it

  • Find every jail, freeze, stun, time-out, penalty box and similar hold, whether it comes from staff, a game rule or an anti-cheat.
  • Check where the hold is recorded. A hold kept only on the character, or not kept at all, ends when the character is replaced. Look for a server table or attribute keyed by the Player or UserId, and a server loop or CharacterAdded handler that puts each new character back in place.
  • Players can respawn whenever they like with the Reset Character button in the settings menu. SetCore("ResetButtonCallback", false) is called from client code, and an exploiter runs their own code on their client, so they can call it again with true. Hiding the button doesn't stop a reset.
  • Roblox's Humanoid reference says WalkSpeed is a client-replicated property that can be changed locally, and shouldn't be used as a security mechanism or the only way to stop a humanoid moving. A freeze that only sets WalkSpeed or JumpHeight is a finding even without a reset.
  • Holds that are meant to last past leaving, such as a moderation jail for hours, also need to be saved. Judge saving with Bans that don't last.

What an exploiter could do

They reset, or change their own walk speed, and walk out of jail or a freeze straight away. Staff punishments and game rules such as penalty boxes stop working for anyone willing to do it, and players who follow the rules are the only ones held.

How to fix it

  • Record each hold on the server, keyed by the player, with its end time.
  • Check held players on the server, every second or so, and move them back into the cell when they're outside it. This also catches each new character after a reset.
  • Release them on the server when the time is up, and clear the record when they leave.
  • Keep any reset-button change as a convenience only.

Vulnerable example

Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local moderators: { [number]: boolean } = { [123456789] = true }
local cell = CFrame.new(0, 500, 0)

ReplicatedStorage.Remotes.JailPlayer.OnServerEvent:Connect(function(player: Player, target: unknown, minutes: unknown)
	if not moderators[player.UserId] then
		return
	end
	if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then
		return
	end
	if typeof(minutes) ~= "number" or minutes ~= math.floor(minutes) or minutes < 1 or minutes > 60 then
		return
	end
	local character = target.Character
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	if not character or not humanoid then
		return
	end
	character:PivotTo(cell)
	humanoid.WalkSpeed = 0
	task.delay(minutes * 60, function()
		if target.Parent and target.Character == character then
			target:LoadCharacterAsync()
		end
	end)
end)

Fixed example

Fixed
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local moderators: { [number]: boolean } = { [123456789] = true }
local cell = CFrame.new(0, 500, 0)
local cellRadius = 12
local jailedUntil: { [Player]: number } = {}

ReplicatedStorage.Remotes.JailPlayer.OnServerEvent:Connect(function(player: Player, target: unknown, minutes: unknown)
	if not moderators[player.UserId] then
		return
	end
	if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then
		return
	end
	if typeof(minutes) ~= "number" or minutes ~= math.floor(minutes) or minutes < 1 or minutes > 60 then
		return
	end
	jailedUntil[target] = os.clock() + minutes * 60
end)

Players.PlayerRemoving:Connect(function(player)
	jailedUntil[player] = nil
end)

while true do
	task.wait(1)
	local now = os.clock()
	for player, releaseAt in jailedUntil do
		if now >= releaseAt then
			jailedUntil[player] = nil
			task.spawn(player.LoadCharacterAsync, player)
			continue
		end
		local character = player.Character
		local root = character and character:FindFirstChild("HumanoidRootPart")
		if character and root and root:IsA("BasePart") and (root.Position - cell.Position).Magnitude > cellRadius then
			character:PivotTo(cell)
		end
	end
end

Common false positives

  • Holds that are meant to end on death, such as a stun from an attack or a short slow, are fine when a reset costs the player more than waiting it out.
  • A short freeze during a cutscene or countdown that the server ends on its own doesn't need to survive a reset unless escaping it gives an advantage.
  • Holds that the server records and reapplies, as in the fixed example, aren't this finding even if they also change WalkSpeed or the reset button for a smoother feel.