---
title: "Commands and health check — Sentinel for Laravel | Roundly"
description: "Every sentinel:* command with its options and exit codes, the self-registering schedule and the eleven-check health report."
url: https://roundly-consulting.com/open-source/docs/sentinel-for-laravel/commands
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Sentinel for Laravel](https://roundly-consulting.com/open-source/docs/sentinel-for-laravel.md)

# Commands and health check

Fifteen commands cover installation, verification, upkeep and keys. Models are given as a class name or a morph alias, and the commands call the injected SentinelManager, so they also run against Sentinel::fake() in your tests.

| Command | Purpose |
| --- | --- |
| `sentinel:install {--force}` | Publish, generate the default key when missing (with the ring’s algorithm), print the next steps. |
| `sentinel:check {--json} {--strict}` | The installation health check; exit 1 on a failure (--strict: on a warning too). |
| `sentinel:verify {model?*} {--seal=} {--chunk=500} {--limit=} {--ledger} {--anchor=} {--check-schema} {--json} {--fail-on=*} {--max-findings=1000} {--allow-empty} {--isolated}` | Scan sealed rows (and the ledger); exit 1 on findings, 2 when there is nothing to scan — for cron and CI. |
| `sentinel:checkpoint {--connection=*} {--batch=} {--isolated}` | Fold pending ledger entries into checkpoints and publish them to the anchors. |
| `sentinel:reseal {model} {--seal=} {--from-key=} {--only-outdated} {--upgrade-format} {--chunk=500} {--dry-run} {--acknowledge=}` | Re-seal after a rotation or definition change; never launders without --acknowledge. |
| `sentinel:seal-missing {model} {--seal=} {--reason=} {--chunk=500}` | Baseline rows that were never sealed. |
| `sentinel:inspect {model} {id} {--seal=} {--show-values} {--check-schema}` | One row: seal row, verdict, manifest, history, changed fields (values only with --show-values). |
| `sentinel:prune {--idempotency} {--nonces} {--dry-run}` | Delete expired idempotency keys and nonces. |
| `sentinel:key:generate {--ring=} {--algorithm=hmac-sha256} {--kid=} {--database} {--activate-at=} {--owner-type=} {--owner-id=} {--label=}` | Generate a key (environment lines, or a database row). |
| `sentinel:key:import {kid} {--ring=} {--algorithm=} {--signing} {--file=} {--activate-at=} {--owner-type=} {--owner-id=} {--label=}` | Import a partner’s (or your own) key; material from --file (PEM or base64:…) or a hidden prompt, never an argument. |
| `sentinel:key:rotate {--ring=} {--algorithm=} {--activate-at=}` | Rotate a ring’s signing key (a custom driver’s key is refused). |
| `sentinel:key:revoke {kid} {--ring=} {--reason=}` | Revoke a key. |
| `sentinel:key:retire {kid} {--ring=} {--force}` | Retire a key (refused while seals still use it). |
| `sentinel:key:list {--ring=}` | The key inventory — labels included, quoted and escaped — and the seals each key made (on every ledger connection); never material. |
| `sentinel:key:reseal {--ring=} {--dry-run}` | Re-seal database keys in the 1.2 envelope so their labels are bound — the upgrade step from 1.1; prints each label before writing. |

```bash
php artisan sentinel:verify                                   # every sealable model
php artisan sentinel:verify "App\Models\Invoice" --seal=financial --ledger
php artisan sentinel:verify --fail-on=tampered,missing --fail-on=backlog
php artisan sentinel:verify --ledger --anchor='<json>'           # compare a payload from a log anchor
php artisan sentinel:verify --check-schema --limit=10000         # CI against a staging snapshot
php artisan sentinel:reseal "App\Models\Invoice" --from-key=default-20250901-a1b2c3 --dry-run
php artisan sentinel:seal-missing "App\Models\Invoice" --reason="Initial baseline 2026-10"
php artisan sentinel:inspect "App\Models\Invoice" 42 --seal=financial
php artisan sentinel:prune --dry-run
php artisan sentinel:key:list --ring=http
```

## Options worth knowing

- \--fail-on takes VerificationStatus values (tampered, missing, … — also outdated and unsealed, which never fail otherwise) and LedgerFindingKind values, repeated or comma-separated. Naming statuses replaces the default — only those fail; naming only ledger kinds keeps every failing status. Ledger violations always fail; backlog and anchor\_unreachable fail only when named. An unknown name exits 2.
- \--anchor='<json>' (a payload copied out of a write-only log anchor) is read only together with --ledger; an undecodable payload exits 2.
- \--isolated\[=EXIT\] (sentinel:verify, sentinel:checkpoint) skips the run — exit 0, or the given code — while another instance holds the lock.
- Ranges: --chunk 1–100 000, --limit ≥ 1 (rows in total, across models), --max-findings 0–1 000 000, --batch 1–100 000; reasons 1–sealing.reason\_max\_length characters, a revoke reason at most 1 000.
- \--json: sentinel:verify prints models, unresolved\_types, scanned, counts, findings, truncated and a ledger object; sentinel:check prints failed and the list of checks, where failed honours --strict.
- sentinel:inspect keeps values redacted unless --show-values (they may contain personal data); sentinel:key:import reads material from --file or a hidden prompt, never from an argument that would land in shell history. A seal row with a corrupt sealed\_at is still shown by sentinel:inspect (sealed at —, status malformed (sealed\_at), exit 1) instead of aborting it.
- sentinel:key:reseal walks every ring that stores keys in the database (--ring for one), row by row under the row’s lock, and prints ring:kid → "label" before writing — binding, would bind (--dry-run, which writes nothing) or already bound — with control and invisible characters, < and > escaped like sentinel:key:list’s. It is idempotent, and on success without --dry-run it reminds you to set SENTINEL\_REQUIRE\_BOUND\_LABEL=true.

## Exit codes

0 means success, 1 failure and 2 invalid input:

| Command | 0 | 1 | 2 |
| --- | --- | --- | --- |
| `sentinel:install` | installed — a key generated, already present or left for after migrating (a second run is harmless) | generating the default ring’s key failed (e.g. its algorithm is not in the ring’s allow-list) | — |
| `sentinel:check` | no failure (with --strict: no warning either) | a failure (with --strict: or a warning) | — |
| `sentinel:verify` | nothing failing found (or nothing to scan, with --allow-empty) | a failing status (see --fail-on) or a ledger violation | invalid input or configuration, or nothing to scan |
| `sentinel:checkpoint` | every connection checkpointed (one held by another run is skipped with a warning) | a connection’s checkpoint failed | an invalid --batch or configuration |
| `sentinel:reseal` | nothing skipped, nothing failed | rows that are not intact were skipped (listed), or a row failed | unknown model or seal, invalid option or configuration, an acknowledgement refused by the policy |
| `sentinel:seal-missing` | every candidate baselined | rows with history but no seal (reported, never baselined), or a row failed | no --reason, unknown model or seal, invalid option |
| `sentinel:inspect` | every inspected seal is intact | a seal is not intact, or the row does not exist | unknown model or seal, configuration error |
| `sentinel:prune` | deleted (or counted, with --dry-run) | a store error | — |
| `sentinel:key:generate` | generated | unknown --algorithm, owner not found, refused (taken kid, algorithm not allowed, invalid kid or label) | — |
| `sentinel:key:import` | imported | refused (taken kid, read-only ring, invalid material, private material without --signing) | invalid input (no --algorithm, owner not found, unreadable, empty or > 64 KB file, no terminal and no --file) |
| `sentinel:key:rotate` | rotated | unknown --algorithm, refused (algorithm not allowed, a key from a custom driver) | — |
| `sentinel:key:revoke` | revoked — for a config key, the SENTINEL\_REVOKED\_KEYS line printed | no --reason, unknown key, refused | — |
| `sentinel:key:retire` | retired — for a config key, the instruction printed | unknown key, still used by seals without --force, refused | — |
| `sentinel:key:list` | listed | invalid ring or configuration | — |
| `sentinel:key:reseal` | every key re-sealed or already bound (with --dry-run: would be) | a key fails its integrity check, a legacy key is refused under require\_bound\_label or holds a label that is not valid UTF-8 (each listed, none re-sealed); an unknown ring or one without a database store | — |

## Scheduling

The provider registers the upkeep on the scheduler while schedule.enabled is on (the default), each task withoutOverlapping()->onOneServer(). The full scan defaults to daily — hourly full-table scans are a silent load on large tables — while the checkpoint stays every minute, because its interval is the window in which a rollback goes unseen:

| Task | Command | Default | Env |
| --- | --- | --- | --- |
| `checkpoint` | sentinel:checkpoint (only while ledger.enabled) | `everyMinute` | `SENTINEL_SCHEDULE_CHECKPOINT` |
| `verify` | sentinel:verify --allow-empty (+ --ledger while the ledger is on) | `daily` | `SENTINEL_SCHEDULE_VERIFY` |
| `prune` | sentinel:prune | `daily` | `SENTINEL_SCHEDULE_PRUNE` |

To wire them yourself, set SENTINEL\_SCHEDULE=false and add them to routes/console.php. Don’t keep hand-written lines next to the automatic schedule — withoutOverlapping() prevents concurrent runs, not double scheduling.

```php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('sentinel:checkpoint')->everyMinute()->withoutOverlapping();
Schedule::command('sentinel:verify --ledger')->hourly();
Schedule::command('sentinel:verify --json')->dailyAt('03:00')->emailOutputOnFailure('ops@example.com');
Schedule::command('sentinel:prune')->daily();
```

Sentinel::sealables() lists what sentinel:verify scans when it is given no model: sentinel.models first, then every class with seal rows or ledger entries. With nothing to scan it exits 2, so a misconfiguration never reports green. In CI, sentinel:verify --check-schema --limit=10000 against a staging snapshot fails the pipeline on findings, and sentinel:check --strict fails it on any weakened guarantee.

## A host without Sentinel’s tables

A host that gets Sentinel only as another package’s dependency never publishes its config or migrations — yet the schedule is on by default. So each scheduled task skips while none of the tables the configuration needs exists (the list the tables check reads):

- No tables at all: checkpoint, verify and prune stay registered (schedule:list shows them) but never start — schedule:run dispatches ScheduledTaskSkipped for each. Nothing fails, so such a host needs no SENTINEL\_SCHEDULE=false.
- Any one table present: the host uses Sentinel’s storage and every task runs as before — a half-migrated install fails as loudly as it always did.
- prune skips only while it would prune a Sentinel table: with a cache store, or a store the host bound, it runs whatever tables exist.
- Cannot tell (a connection that fails, a setting that cannot be read, a store that cannot be built): the task runs and reports the failure itself.
- The look-up happens only for a task that is due, once per schedule:run: one hasTable per needed table until the first is found. Migrating turns the tasks on at the next schedule:run.

The skip is silent on purpose, so the health check is not: while a task skips, the schedule check fails and names it, and php artisan about shows auto (checkpoint, verify, prune skipped: no tables). Commands you run yourself and lines you schedule by hand are never skipped.

## The health check

sentinel:check (or Sentinel::check()) reports every misconfiguration that silently weakens the guarantees, in one place. Messages name settings, rings, tables and classes — never key material, key ids or labels:

```php
$report = Sentinel::check();          // HealthReport
$report->failed();                    // any failure (warnings do not fail it)
$report->failures();                  // list<HealthCheck>: name, status, message
$report->warnings();
```

| Check | Fails when | Warns when |
| --- | --- | --- |
| `configuration` | Any setting or signature profile is invalid. | — |
| `signing_keys` | The default ring, the ledger ring or a ring a sealable model uses cannot sign (while sealing.auto is on). | The same on a verify-only node. |
| `app_key` | Database keys or idempotency.encrypt need APP\_KEY and it is empty. | — |
| `tables` | A sentinel\_\* table is missing on its connection. | — |
| `models` | A sealable model does not compile, or its table lacks a sealed column. | There are no sealable models, stored types no longer resolve, or a model lives on a connection ledger.connections does not list. |
| `anchors` | A configured anchor is unreachable, or the cache anchor lives in process memory or in the database it protects. | None is configured, or the cache anchor uses the default store. |
| `checkpoints` | — | Ledger entries older than ledger.backlog\_warning\_seconds are not checkpointed. |
| `schedule` | Scheduling is on, but the tasks skip every run because none of Sentinel’s tables exists (names the tasks). | Scheduling is off and no sentinel:checkpoint is scheduled. |
| `retired_keys` | — | Seals still use a revoked, retired or unknown key (counts per ring). |
| `stores` | The idempotency or nonce store cannot be built. | — |
| `key_envelopes` | require\_bound\_label is on while database keys still use the pre-1.2 envelope — they fail their integrity check. | Database keys still use the pre-1.2 envelope, whose label is not bound (counts per ring): run sentinel:key:reseal, check the labels, then set SENTINEL\_REQUIRE\_BOUND\_LABEL=true. |

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/sentinel-for-laravel.md)
