NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Sentinel for Laravel

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.

CommandPurpose
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=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.

Exit codes

0 means success, 1 failure and 2 invalid input:

Command012
sentinel:installalways (a second run is harmless)——
sentinel:checkno failure (with --strict: no warning either)a failure (with --strict: or a warning)—
sentinel:verifynothing failing found (or nothing to scan, with --allow-empty)a failing status (see --fail-on) or a ledger violationinvalid input or configuration, or nothing to scan
sentinel:checkpointevery connection checkpointed (one held by another run is skipped with a warning)a connection’s checkpoint failedan invalid --batch or configuration
sentinel:resealnothing skipped, nothing failedrows that are not intact were skipped (listed), or a row failedunknown model or seal, invalid option or configuration, an acknowledgement refused by the policy
sentinel:seal-missingevery candidate baselinedrows with history but no seal (reported, never baselined), or a row failedno --reason, unknown model or seal, invalid option
sentinel:inspectevery inspected seal is intacta seal is not intact, or the row does not existunknown model or seal, configuration error
sentinel:prunedeleted (or counted, with --dry-run)a store error—
sentinel:key:generategeneratedunknown --algorithm, owner not found, refused (taken kid, algorithm not allowed, invalid kid or label)—
sentinel:key:importimportedrefused (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:rotaterotatedunknown --algorithm, refused—
sentinel:key:revokerevoked — for a config key, the SENTINEL_REVOKED_KEYS line printedno --reason, unknown key, refused—
sentinel:key:retireretired — for a config key, the instruction printedunknown key, still used by seals without --force, refused—
sentinel:key:listlistedinvalid 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:

TaskCommandDefaultEnv
checkpointsentinel:checkpoint (only while ledger.enabled)everyMinuteSENTINEL_SCHEDULE_CHECKPOINT
verifysentinel:verify --allow-empty (+ --ledger while the ledger is on)dailySENTINEL_SCHEDULE_VERIFY
prunesentinel:prunedailySENTINEL_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();
CheckFails whenWarns when
configurationAny setting or signature profile is invalid.—
signing_keysThe 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_keyDatabase keys or idempotency.encrypt need APP_KEY and it is empty.—
tablesA sentinel_* table is missing on its connection.—
modelsA 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.
anchorsA 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).
storesThe 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 crypto

By 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.