# Moderation

> Bans and kicks with a reason, an end, and a screen the player actually reads — one list, not two.

Source: https://stygion.eu/docs/keystone/moderation
Updated: 2026-10-10

Bans and kicks that carry a reason, an end, and a screen the player actually
reads. All of it in `moderation.toml` and on the Players screen in the editor.

## The commands

```
/keystone ban <player> [for how long] [reason]
/keystone unban <player>
/keystone kick <player> [reason]
/keystone bans
/keystone history <player>
```

The length is an ordinary duration — `7d`, `12h`, `30m`. Without one the ban
does not end. The reason is what the player is shown, so it is worth writing for
them rather than for you.

`/keystone bans` lists what still stands. `/keystone history <player>` lists
everything this server has ever done to somebody, including what has expired.

The permissions are `keystone.ban` and `keystone.kick`, separately. They apply to
the game's own `/kick`, `/ban`, `/pardon` and `/banlist` too when Keystone takes
them over (see below). A moderator who is not an operator **cannot kick or ban an
operator**, or anybody in a heavier group than their own.

**The player really reads the reason.** Every kick, ban, maintenance close,
restart and failed sign-in now reads the client dry first, sends the reason and
only then closes the connection. Otherwise Linux resets the connection and the
player reads `Connection reset by peer` instead of why. A Discord ban no longer
disconnects somebody off the server thread.

## The game's own `/ban` and `/kick`

```
moderation.take-over-vanilla = true
```

**On** (the default): the commands people's fingers already know write into this
ledger, end on their own date, and show the screen you wrote.

**Off**: they are the game's again — which means a ban typed as `/ban` is
invisible to `/keystone bans`, never expires, and shows "You are banned from this
server." Leaving this off is choosing to have two ban lists.

## The screen a player is shown

Three of them belong here, and all are overridable in `operations.toml` under
`screens`:

| Screen | When |
|---|---|
| `screens.banned` | A ban with no end |
| `screens.banned-until` | A ban with a date |
| `screens.kicked` | A kick |

The text takes `{reason}`, `{by}`, `{until}`, `{left}`, `{player}` and
`{appeal}`. Blank lines are allowed and worth using — this is the one thing that
person is reading at that moment.

**`screens.appeal` is where to appeal**, and it fills in on every screen that
carries `{appeal}`. A ban with nowhere to write to is a ban you never find out
was wrong.

## Discord

With the bridge on, a ban can mean it in both places:

- `discord.ban.game-to-discord` — a ban in game bans on Discord too,
- `discord.ban.discord-to-game` — and the other way,
- `discord.ban.who-may` — whose bans on Discord are allowed to reach the game.

Both directions are off by default, because it is a decision rather than a
convenience. In detail on the [Discord page](/docs/keystone/discord).

## What is not here yet

**There are no warnings in the mod yet**, and a muted player can still write on signs and in books.

## Mutes

From Keystone 0.2.4.

```
/mute <player> <time> [reason]      30m, 12h, 7d, or forever
/unmute <player>
/mutes
```

Also as `/keystone mute`, `/keystone unmute` and `/keystone mutes` (where another mod owns `/mute`). Permission `keystone.mute`, asked again every time the command runs.

- **Into the same ledger as bans:** `/keystone history` shows every mute with its reason, author and end; `GET /v1/mutes` (`players.read`) lists those that stand. It ends on its own date — nothing has to remember to lift it — and survives a restart and a reconnect.
- **What it closes:** the player's chat, the commands in `mute.blocked-commands` (by default `/msg`, `/tell`, `/w`, `/whisper`, `/me`, `/r`, `/reply`, `/teammsg`, `/tm` — add another mod's mail or party chat by name), and what their **linked Discord account** writes into the bridged channel. Each try tells them the reason and how long is left. The Discord bridge never hears a muted line.
- **Never above you:** nobody mutes somebody in a heavier group or an operator (unless they are one), nor anybody with `keystone.mute.exempt`. A mute somebody above you gave is **theirs to lift**: `/unmute` refuses you and says who. Nobody lifts their own mute. The console may do anything.
- A second `/mute` replaces the first; it does not add up.
- `mute.enabled = false` switches all of it off (after a restart).
