Commands and health check
Fourteen 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, 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. |
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 and the seals each key made (on every ledger connection) — never material. |
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=httpOptions 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.
Exit codes
0 means success, 1 failure and 2 invalid input:
| Command | 0 | 1 | 2 |
|---|---|---|---|
sentinel:install | always (a second run is harmless) | — | — |
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 | — |
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 | — |
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.
// 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('[email protected]');
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.
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 or key ids:
$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 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. | — |
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 cryptoBy donating, you agree to our donation terms.
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.