ModsJava
SentinelAC
Java mod listed for Minecraft 26.x. Downloads from the MC Java Mods app on Android.
⬇ Download on Hangar# Sentinel
Anti-cheat, anti-bot and anti-xray for Paper — and a framework you can build on.
Sentinel ships 18 detections, a bot guard with captcha, plugin-side ore
obfuscation and mining analytics. Every part of it is reachable through a public
API, so you can add your own checks, reroute punishments into your own
moderation system, or use Sentinel purely as a detection engine behind your own
plugin.
- No external dependencies — Paper API only
- No database, no web calls, no telemetry
- Java 25, Paper 26.3 (`paper-plugin.yml`, Brigadier commands)
---
## Install
1. Drop `sentinel.jar` into `plugins/`.
2. Start the server. `config.yml`, `messages.yml` and `verified.yml` are created
in `plugins/Sentinel/`.
3. Give your staff `sentinel.alerts` and `sentinel.admin`.
## Commands
All of them require `sentinel.admin`. `/sn` is an alias of `/sentinel`.
| Command | Effect |
|---|---|
| `/sentinel` | Version, check count, profile count, current TPS |
| `/sentinel reload` | Reload `config.yml` and `messages.yml` |
| `/sentinel menu` | Open the admin GUI |
| `/sentinel checks` | List every registered check with its state |
| `/sentinel check on\|off` | Toggle one check until the next reload |
| `/sentinel alerts [on\|off]` | Toggle your own alert messages |
| `/sentinel verbose [on\|off]` | Also show the detail line of every flag |
| `/sentinel violations ` | Current violation level per check |
| `/sentinel clear ` | Reset a player's violations |
| `/sentinel xray top` | Mining-analytics suspect list, highest score first |
| `/sentinel xray report ` | Full mining breakdown for one player |
| `/sentinel xray reveal ` | Send the real blocks (debugging) |
| `/sentinel xray hide ` | Re-hide everything around the player |
| `/sentinel bot status` | Bot guard state, blocked connections, lockdown |
| `/sentinel bot lockdown` | Toggle lockdown (refuse new connections) |
| `/sentinel bot verify ` | Mark a player as verified for good |
| `/sentinel bot captcha ` | Start a captcha for a player |
## Permissions
| Node | Default | Meaning |
|---|---|---|
| `sentinel.admin` | op | Use `/sentinel` |
| `sentinel.alerts` | op | Receive flag alerts |
| `sentinel.bypass` | false | Skip every check |
| `sentinel.bypass.xray` | false | See real blocks, no mining analytics |
| `sentinel.bypass.captcha` | false | Never asked to verify |
| `sentinel.bypass.bot` | false | Skip the behaviour profiler |
---
## How a detection becomes a punishment
1. A check sees something impossible and calls `flag`, which raises that
player's **violation level** (vl) for that check.
2. `SentinelFlagEvent` fires. Cancel it and the violation is rolled back —
nothing is logged, alerted or punished.
3. Staff with `sentinel.alerts` get a message; the flag is written to
`violations.log`.
4. The **punishment ladder** runs. The highest rule whose `violations`
threshold the vl has reached is executed once.
5. Violations decay by `decay-per-second` for every second without a new flag,
so one bad reading never accumulates into a ban.
Every check is exempted while the player is in a state the server cannot model
cleanly: right after joining, teleporting, respawning, taking knockback,
leaving a vehicle, changing world, while gliding, riptiding, in creative mode,
and whenever the server is lagging or the player's ping is over the limit.
---
## Configuration
### `general`
| Key | Default | Meaning |
|---|---|---|
| `prefix` | gradient | MiniMessage prefix in front of every plugin message |
| `debug` | `false` | Reserved for verbose logging |
### `alerts`
| Key | Default | Meaning |
|---|---|---|
| `enabled` | `true` | Master switch for staff alerts |
| `console` | `true` | Also log flags to the console |
| `cooldown-millis` | `1500` | Minimum gap between two alerts for the same player and check |
| `enabled-by-default` | `true` | Staff get alerts without typing `/sentinel alerts on` |
| `format` | — | Alert line; placeholders ` ` |
| `verbose-format` | — | Second line in verbose mode; placeholder `` |
The alert line carries a hover with the full detail text and a click that runs
`/sentinel violations `.
### `logging`
| Key | Default | Meaning |
|---|---|---|
| `file` | `true` | Append flags to a log file |
| `file-name` | `violations.log` | File inside `plugins/Sentinel/` |
| `keep-days` | `14` | Delete the log once it is older than this |
### `performance`
| Key | Default | Meaning |
|---|---|---|
| `min-tps` | `17.0` | Below this the server counts as lagging and movement checks pause |
| `max-ping` | `400` | Players above this ping are exempt |
| `lag-grace-millis` | `2500` | How long the lag exemption lasts after a spike |
| `chunk-scans-per-tick` | `2` | Ore index builds per tick (raise to warm up faster, costs CPU) |
### `exemptions`
`creative`, `flying` and `operator` are switches. The `*-millis` keys set how
long the matching exemption lasts after the event: `join`, `teleport`,
`respawn`, `velocity`, `vehicle`, `world-change`, `damage`, `slime`.
Set `operator: true` if your ops should never be flagged.
### `punishments`
```yaml
punishments:
reset-violations: true
ladder:
- violations: 15.0
action: alert
- violations: 40.0
action: command
value: "kick Sentinel: suspicious behaviour ()"
- violations: 80.0
action: command
value: "ban Sentinel: unfair advantage ()"
```
Built-in actions: `alert`, `command` (console command in `value`), `kick`
(kick message in `value`), `none`. Placeholders in `value`: ``,
``, ``, ``, ``, ``.
`reset-violations: true` zeroes the check's vl after a punishment other than
`alert`, so the same ladder step does not fire twice in a row.
Plugins can register their own action ids — see API.
### `checks`
Every check has the same three keys plus its own `settings` block:
| Key | Meaning |
|---|---|
| `enabled` | Run this check at all |
| `decay-per-second` | How fast its violation level drains |
| `max-violations` | Ceiling, so one check cannot dominate the ladder |
Most `settings` blocks contain a `buffer`: how many consecutive bad readings
are needed before the check flags. Raising a buffer or a tolerance is the right
way to deal with a false positive; disabling the check is the last resort.
| Check | Category | What it looks at |
|---|---|---|
| `speed` | movement | Horizontal distance per move against a model built from sprint state, walk speed, speed/slowness potions, ice, soul speed and sprint-jumps |
| `flight` | movement | Airborne without descending, with no liquid, ladder, web, bounce block or ground below |
| `nofall` | movement | Landing after a long fall while the client reports no fall distance |
| `step` | movement | Ground-to-ground height gain above `max-step` (0.65 passes slabs and stairs) |
| `jesus` | movement | Moving on a liquid surface with air at the feet and nothing solid below |
| `timer` | movement | Movement packet rate against the 20 Hz tick rate, measured over `sample-size` moves |
| `invalid-motion` | movement | Non-finite or absurd deltas and impossible pitch; cancels the move |
| `reach` | combat | Distance from the eye to the victim's bounding box |
| `killaura` | combat | Attack angle outside the view cone, several victims inside one window, attacking while blocking and sprinting |
| `autoclicker` | combat | Clicks per second and the standard deviation of click intervals |
| `aim` | combat | Pitch snaps during a fight and pitch deltas that never vary |
| `fastbreak` | interaction | Break time against `Block#getBreakSpeed` for the tool actually held |
| `nuker` | interaction | Blocks broken per second and how far apart they are |
| `fastplace` | interaction | Blocks placed per second |
| `scaffold` | interaction | Placing below yourself while moving and looking away from the block |
| `inventory` | interaction | Inventory clicks while sprinting, airborne or moving |
| `chat-spam` | chat | Message interval and repeated messages |
| `command-spam` | chat | Command interval |
### `bot`
**`connection`** — refuses connections before they reach the login stage.
| Key | Default | Meaning |
|---|---|---|
| `max-per-address` | `3` | Connections per address inside the window |
| `window-seconds` | `60` | Length of that window |
| `max-joins-per-second` | `6` | Server-wide rate; exceeding it triggers lockdown |
| `lockdown-seconds` | `30` | How long lockdown refuses every new connection |
| `max-online-per-address` | `2` | Simultaneous players per address |
**`names`** — `min-length`, `max-length`, `max-digit-ratio`,
`min-entropy`/`max-entropy` (Shannon entropy over the name — random strings
score high, words score low) and `blocked-patterns`, a list of Java regexes.
Use single quotes in YAML so backslashes survive.
**`activity`** — a client that has not moved or rotated within
`require-seconds` is kicked.
**`behaviour`** — `straight-line-threshold` and `straight-line-samples` catch
paths with no lateral deviation at all; `zero-rotation-samples` catches moving
without ever turning the head. Each signal is worth 30 points;
`alert-score` notifies staff, `kick-score` kicks.
**`captcha`** — `mode: chat` sends a code to type, `mode: gui` opens a menu and
asks for one named item. `only-unverified: true` asks each account once and
remembers it in `verified.yml`. `freeze` blocks movement and commands,
`blindness` applies the effect for the duration. Wrong answers count against
`max-attempts`, silence against `timeout-seconds`; both end in a kick.
### `xray`
**`obfuscation`** — Sentinel replaces every block in `hidden-blocks` with
stone, deepslate or netherrack in the packets the client receives, and sends the
real block once the player is within `reveal-radius`. `hysteresis` keeps blocks
visible a little past that radius so walking along the edge does not flicker.
`update-interval-ticks` is how often the near window is recomputed,
`max-blocks-per-update` caps one update, and the three `replacement-*` keys
choose the filler per block family.
> Paper has a server-side anti-xray (`anticheat.anti-xray` in
> `paper-world-defaults.yml`) that is cheaper than any plugin can be, because it
> runs inside the chunk serializer. If you enable Paper's engine-mode 2 or 3,
> set `xray.obfuscation.enabled: false` and keep only the analytics below. Run
> Sentinel's obfuscation when you cannot touch the server config, or when you
> want per-permission control (`sentinel.bypass.xray`).
**`analytics`** — scores mining behaviour instead of hiding anything, which
catches the players who found their ores legitimately-looking but far too often.
Four weighted signals over a rolling window:
| Signal | Weight | Meaning |
|---|---|---|
| `ore-ratio` | 40 | Actual ore rate against `expected-ore-ratio` per material |
| `tunnel` | 25 | Share of breaks that continue a straight 1×1 tunnel |
| `beeline` | 20 | Share of ores reached right after a direction change |
| `depth` | 15 | Share of breaks at or below `deep-y` |
A player is listed as a suspect once the score reaches `suspect-score` and at
least `min-samples` blocks were seen. `alert-cooldown-minutes` throttles the
alert, not the scoring. Analytics never punishes on its own — it produces a list
for staff and a `SentinelXrayEvent` for your own plugin.
---
## Files
| File | Content |
|---|---|
| `config.yml` | Everything above |
| `messages.yml` | Every player-facing string, MiniMessage |
| `verified.yml` | UUIDs that passed the captcha |
| `violations.log` | One line per flag |
---
## For developers
Sentinel is built to be used as a detection engine by other plugins: register
your own checks, read per-player state, replace the punishment layer, or listen
for flags and feed them into your own moderation backend.
# Sentinel API
**API version 1.0.0** — everything under `org.sentinel.api` is the public
surface and follows semantic versioning. Everything outside it is internal and
may change without notice.
Contents
1. [Setup](#setup)
2. [Entry point](#entry-point)
3. [Writing your own check](#writing-your-own-check)
4. [Reading player state](#reading-player-state)
5. [Violations](#violations)
6. [Exemptions](#exemptions)
7. [Punishment actions](#punishment-actions)
8. [Events](#events)
9. [Bot guard and captcha](#bot-guard-and-captcha)
10. [X-ray](#x-ray)
11. [Using Sentinel as a framework](#using-sentinel-as-a-framework)
12. [Version history](#version-history)
---
## Setup
Sentinel has no repository publication, so depend on the jar directly. The
build produces a sources jar next to it.
```kotlin
dependencies {
compileOnly(files("libs/sentinel.jar"))
}
```
```yaml
# paper-plugin.yml
dependencies:
server:
Sentinel:
load: BEFORE
required: true
```
Use `required: false` plus `SentinelProvider.available()` if Sentinel is
optional for your plugin.
---
## Entry point
```java
SentinelApi sentinel = SentinelProvider.get();
```
`SentinelProvider.get()` throws `IllegalStateException` while Sentinel is not
loaded; `find()` returns an `Optional` and `available()` a boolean. Sentinel is
also registered with the Bukkit services manager:
```java
RegisteredServiceProvider registration =
Bukkit.getServicesManager().getRegistration(SentinelApi.class);
```
`SentinelApi` gives you every subsystem:
| Method | Returns |
|---|---|
| `version()` | Plugin version |
| `checks()` | `CheckRegistry` |
| `violations()` | `ViolationService` |
| `exemptions()` | `ExemptionService` |
| `punishments()` | `PunishmentRegistry` |
| `bots()` | `BotService` |
| `xray()` | `XrayService` |
| `profile(Player)` / `profile(UUID)` / `profiles()` | `PlayerProfile` |
| `serverHealthy()` | `false` while the server is lagging |
| `reload()` | Reload the configuration |
---
## Writing your own check
Extend `Check`, override the hooks you need, call `flag` when something is
wrong. You never touch violation levels, alerts, logging or punishments — that
is handled for you.
```java
public final class GlideCheck extends Check {
public GlideCheck() {
super("glide", CheckCategory.MOVEMENT, "Falls slower than gravity allows");
}
@Override
public void onMove(final MoveContext context) {
if (context.onGround() || context.profile().airTicks() < 10) {
return;
}
final double expected = -0.08D * context.profile().airTicks();
final double tolerance = settings().getDouble("tolerance", 0.05D);
if (context.vertical() > expected + tolerance) {
if (buffer(context.profile(), 1.0D, 5.0D) > 3.0D) {
flag(context.profile(), "dy " + context.vertical() + " expected " + expected);
resetBuffer(context.profile());
}
} else {
buffer(context.profile(), -1.0D, 5.0D);
}
}
}
```
Register it with your own plugin as the owner, and clean up on disable:
```java
@Override
public void onEnable() {
SentinelProvider.get().checks().register(this, new GlideCheck());
}
@Override
public void onDisable() {
SentinelProvider.find().ifPresent(api -> api.checks().unregisterAll(this));
}
```
### Hooks
| Hook | Fired when |
|---|---|
| `onRegister()` / `onUnregister()` | The check is added or removed |
| `onJoin(profile)` / `onQuit(profile)` | A player joins or leaves |
| `onMove(MoveContext)` | The player's position changed |
| `onRotate(profile, yawDelta, pitchDelta)` | The player's rotation changed |
| `onAttack(AttackContext)` | The player damaged an entity |
| `onSwing(profile)` | The player swung their arm |
| `onBlockDamage` / `onBlockBreak` / `onBlockPlace` (`BlockContext`) | Block interaction |
| `onInventoryClick(profile)` | A click in any inventory |
| `onChat(profile, message)` | A chat message (on the main thread) |
| `onCommand(profile, command)` | A command, including the slash |
| `onTick(profile)` | Every 5 ticks, per online player |
All hooks run on the main thread. `MoveContext`, `AttackContext` and
`BlockContext` carry `cancel()` so a check can block the action outright;
`MoveContext` exposes `from`, `to`, `deltaX/Y/Z`, `horizontal()`, `vertical()`,
`onGround()` and `wasOnGround()`, `AttackContext` the `victim()`, `reach()` and
`angle()`, `BlockContext` the `block()`, `action()` and `distance()`.
### Reporting
| Method | Effect |
|---|---|
| `flag(profile, details)` | One violation |
| `flag(profile, amount, details)` | Weighted violation |
| `reward(profile, amount)` | Lower the violation level after a clean reading |
### Per-player state
`buffer(profile)`, `buffer(profile, delta, max)` and `resetBuffer(profile)` give
you a clamped counter per check and player. For anything richer use
`state(profile, key, Supplier)`, which lazily creates and caches your own
object:
```java
private static final class Samples {
private final double[] values = new double[32];
private int index;
}
final Samples samples = state(context.profile(), "samples", Samples::new);
```
`clearState(profile)` drops everything your check stored for that player. All
of it lives in `profile.storage()` and disappears when the player leaves.
### Configuration
`settings()` returns a `CheckSettings` backed by `checks.` in
`config.yml`. A check that is not in the file still works — it gets
`enabled: true`, `decay-per-second: 1.0` and `max-violations: 120.0`. Admins can
add a block for it at any time:
```yaml
checks:
glide:
enabled: true
decay-per-second: 0.5
max-violations: 100.0
settings:
tolerance: 0.05
```
`CheckSettings` reads `getBoolean`, `getInt`, `getLong`, `getDouble`,
`getString`, `getStringList` and `getDoubleMap`, each with a fallback, and
`enabled(boolean)` toggles the check at runtime.
---
## Reading player state
`PlayerProfile` is a live, read-mostly view of one online player.
```java
PlayerProfile profile = SentinelProvider.get().profile(player);
profile.ping();
profile.airTicks();
profile.lastHorizontal();
profile.clicksPerSecond();
profile.recentTargets();
profile.totalViolations();
profile.violationSnapshot(); // check name -> vl
profile.mining().score(); // 0..100
profile.botScore(); // 0..100
profile.verified();
profile.exempt(ExemptType.TELEPORT);
profile.storage(); // your own scratch space
```
`mining()` returns `MiningStats`: `totalBlocks()`, `totalOres()`, `ores()` per
material, `oreRatio()`, `tunnelRatio()`, `beelineRatio()`, `deepRatio()`,
`score()` and `reset()`.
---
## Violations
```java
ViolationService violations = SentinelProvider.get().violations();
violations.violations(profile, "speed");
violations.total(profile);
violations.snapshot(profile);
violations.add(profile, "my-check", 2.0, "reason"); // runs alerts + ladder
violations.subtract(profile, "speed", 5.0);
violations.clear(profile);
violations.recent(profile); // last 30 flags
violations.recent(); // last 200 server-wide
```
`add` with an unregistered check name still works — it is logged under the
`custom` category. That is the shortest path if your detection logic lives
entirely in your own plugin and you only want Sentinel's violation
bookkeeping, alerts and punishment ladder.
A `Violation` is a record: `player`, `playerName`, `check`, `category`,
`amount`, `total`, `details`, `timestamp`, `ping`, `tps`.
---
## Exemptions
```java
ExemptionService exemptions = SentinelProvider.get().exemptions();
exemptions.exempt(profile, ExemptType.LAG);
exemptions.exemptAny(profile, ExemptType.JOIN, ExemptType.TELEPORT);
exemptions.apply(profile, ExemptType.MANUAL, 5000L); // exempt for 5 seconds
exemptions.clear(profile, ExemptType.MANUAL);
exemptions.active(profile);
```
Use `ExemptType.MANUAL` for your own effects — a custom dash ability, a lobby
launch pad, a cutscene — and the built-in movement checks will ignore the
player for that window.
---
## Punishment actions
Register an id and use it in the ladder. This is how you route Sentinel into
your own moderation system without touching its code.
```java
SentinelProvider.get().punishments().register("mybans", context -> {
MyBanService.ban(context.profile().uuid(),
"Sentinel: " + context.violation().check(),
context.violation().details());
});
```
```yaml
punishments:
ladder:
- violations: 60.0
action: mybans
value: "7d"
```
`PunishmentContext` carries the `profile()`, the `violation()`, the
`threshold()` that was crossed and the `value()` string from the config.
`ids()` lists all registered actions, `unregister(id)` removes one, and
`execute(id, context)` runs one directly.
Overwriting a built-in id (`alert`, `command`, `kick`, `none`) replaces it.
---
## Events
All events are Bukkit events. Four of them are cancellable, and cancelling is
the clean way to take control.
| Event | Cancelling it |
|---|---|
| `SentinelFlagEvent` | Rolls the violation back; no alert, no log, no punishment |
| `SentinelPunishEvent` | Stops that punishment action from running |
| `SentinelBotEvent` | Lets the connection or player through |
| `SentinelXrayEvent` | Suppresses the suspect alert (the suspect is still listed) |
| `SentinelCaptchaEvent` | Not cancellable — reports `STARTED`, `PASSED`, `FAILED`, `TIMEOUT` |
```java
@EventHandler
public void onFlag(final SentinelFlagEvent event) {
if (event.profile().player().getWorld().getName().equals("minigames")) {
event.setCancelled(true);
return;
}
MyWebhook.post(event.violation());
}
```
`SentinelBotEvent` fires asynchronously during pre-login for connection and
name signals, and synchronously for behaviour kicks. It exposes the
`playerName()`, the `address()`, the `signal()` and a mutable `kickReason()`.
---
## Bot guard and captcha
```java
BotService bots = SentinelProvider.get().bots();
bots.lockdown(); // currently refusing connections?
bots.lockdown(60_000L); // refuse for a minute
bots.blockedConnections();
bots.pendingCaptchas();
bots.verified(uuid);
bots.verify(uuid); // also completes a running captcha
bots.unverify(uuid);
bots.requestCaptcha(player);
bots.verifying(player);
bots.score(profile);
bots.signals(profile); // Set
```
`BotSignal` values: `CONNECTION_RATE`, `ADDRESS_LIMIT`, `LOCKDOWN`,
`NAME_PATTERN`, `NAME_ENTROPY`, `NAME_LENGTH`, `NAME_DIGITS`, `NO_ACTIVITY`,
`STRAIGHT_LINE`, `ZERO_ROTATION`, `CAPTCHA_TIMEOUT`, `CAPTCHA_FAILED`.
To replace the captcha with your own, listen for
`SentinelCaptchaEvent.Result.STARTED`, run your flow, and call
`bots.verify(uuid)` when the player passes.
---
## X-ray
```java
XrayService xray = SentinelProvider.get().xray();
xray.obfuscationEnabled();
xray.hiddenBlocks();
xray.hideAll(player);
xray.revealAll(player);
xray.hiddenCount(player);
xray.invalidate(chunk); // after changing blocks yourself
xray.analyticsEnabled();
xray.score(profile);
xray.suspects(); // List, best score first
xray.clearSuspect(uuid);
```
Call `invalidate(chunk)` whenever your plugin changes blocks in a way Sentinel
cannot see (world edits, schematic pastes, custom generation), otherwise the ore
index for that chunk stays stale until it unloads.
`XraySuspect` is a record: `player`, `playerName`, `score`, `blocks`, `ores`,
`detectedAt`.
---
## Using Sentinel as a framework
Three ways to build on it, from least to most invasive.
**1. Detection engine, your reaction.** Leave the checks on, set the whole
ladder to `action: none` or drop it, and listen to `SentinelFlagEvent`. Sentinel
does the detecting, decaying and logging; your plugin decides what happens.
```yaml
punishments:
ladder: []
```
**2. Your checks, Sentinel's plumbing.** Register your own `Check` subclasses.
You get exemptions, violation decay, the buffer helpers, per-check config,
alerts with hover and click, the log file, the admin GUI entry, the ladder and
`/sentinel check on|off` — for free, for a class with one method.
**3. Your moderation backend.** Register a `PunishmentAction` and point the
ladder at it. Sentinel never calls `ban` or `kick` itself in that case; your
code decides, and `SentinelPunishEvent` still lets a third plugin veto it.
Migration notes if you are moving an existing anti-cheat into Sentinel:
- One check class per detection, named in `kebab-case`; the name is the config
key, the command argument and the violation key, so pick it once and keep it.
- Replace your own tick counters with `profile.airTicks()`,
`profile.groundTicks()`, `profile.lastHorizontal()` and
`profile.clicksPerSecond()` instead of tracking movement twice.
- Replace your own false-positive guards with the `ExemptType` checks before
you port the detection logic itself — most of what looks like a detection bug
is a missing exemption.
- Anything you stored in a `Map` becomes
`state(profile, key, Something::new)` and gets cleaned up on quit for you.
- Keep your thresholds in `settings()` rather than as constants so servers can
tune them without a rebuild.
---
## Version history
### 1.0.0
Initial API.
- `SentinelApi`, `SentinelProvider`
- `Check`, `CheckCategory`, `CheckRegistry`, `CheckSettings`,
`MoveContext`, `AttackContext`, `BlockContext`
- `PlayerProfile`, `MiningStats`
- `Violation`, `ViolationService`
- `ExemptType`, `ExemptionService`
- `PunishmentAction`, `PunishmentContext`, `PunishmentRegistry`
- `BotService`, `BotSignal`
- `XrayService`, `XraySuspect`
- `SentinelFlagEvent`, `SentinelPunishEvent`, `SentinelBotEvent`,
`SentinelXrayEvent`, `SentinelCaptchaEvent`
Quick facts
- Edition: Minecraft Java
- File type: .jar
- Minecraft version listed: 26.3
- How to install: Install the matching mod loader (Forge, Fabric or NeoForge) for your Minecraft version. → Download the .jar. → Put it in the .minecraft/mods folder and launch that loader profile.
- Where to get it: Opens on Hangar — not every file is mirrored on our own servers.
Install steps are the general flow for this file type — How to install Minecraft Java mods & modpacks walks through it step by step.
SentinelAC is a free Minecraft Java mod. Compatible with Minecraft 26.3. Downloaded 26 times (via Hangar). Download it and open it directly in the game.