Skip to content

Verification outcomes

seal verify produces two layers of result:

  1. A per-bundle state for each recorded or discovered bundle.
  2. An overall outcome that combines those states with policy to determine the exit code.

These tables describe the standalone CLI. It checks files when invoked; it does not intercept an agent’s loads. Native integrations have additional load-time obligations.

The CLI checks:

  • every bundle entry recorded in seal.json, including roots now absent from disk;
  • new roots matched by the lockfile’s discovery patterns.

A root is either an individually pinned file or a pinned directory. A directory bundle covers the regular files beneath it recursively, subject to the specification’s exclusions. Every recorded root must be statically covered by discovery; otherwise the lockfile is invalid.

A new file outside both discovery and existing pinned directories is outside the CLI’s scope. No state is assigned to it. Verification does not discover every file an agent could load.

State Meaning
Verified The root is present and its file paths and hashes match the lockfile.
Unverified A root was discovered on disk but has no lockfile entry.
Removed A recorded root is absent from disk.
Mismatch A present root differs from its recorded contents, or a recorded/discovered root cannot be hashed as a supported bundle.

Within a present directory bundle, a changed hash is a modified file, a new path is an added file, and a lost path is a missing file. Each makes the containing bundle Mismatch. A per-bundle hashing error also becomes Mismatch; current CLI detail can list recorded files as missing when hashing fails. Path-resolution or discovery errors can instead abort verification with exit 2.

Assuming verification can complete, the highest-priority condition below determines the result. Removed bundles remain listed even when a mismatch or unverified bundle determines the outcome.

Condition policy: block policy: warn
Fatal configuration or system error Exit 2; no overall JSON result Exit 2; no overall JSON result
Any Mismatch or Unverified, with or without Removed entries Blocked, exit 1 Warning, exit 0
One or more Removed, with no Mismatch or Unverified Drift, exit 0 Drift, exit 0
All checked bundles match, with no other states Verified, exit 0 Verified, exit 0

A valid empty lockfile with no discovery matches also produces Verified. It does not mean agent files elsewhere in the project were checked.

Assume the lockfile is valid and the files are readable regular files. Each row describes the affected bundles; combine their states using the policy table above. Unchecked means outside scope, not verified or approved.

What happens after pinning Per-bundle result
Contents and paths stay unchanged Verified.
An individually pinned file changes Mismatch.
A file inside a pinned directory changes The directory bundle is Mismatch.
Only whitespace or line endings change Mismatch; file hashes use raw bytes.
An individually pinned file is deleted Removed.
An entire pinned directory is deleted Removed.
A file inside a still-present pinned directory is deleted The directory bundle is Mismatch.
Every tracked file is deleted, leaving the pinned directory present Mismatch; a present, empty bundle cannot match a non-empty pin.
A regular file is added anywhere inside a pinned directory The directory bundle is Mismatch.
A hidden, Git-ignored, or vendored dependency file is added or changed inside a pinned directory Mismatch, unless explicitly excluded by Seal. Git ignore rules do not control hashing.
A new regular file or non-empty directory matches discovery but has no pin Unverified.
A new file/root appears outside discovery and existing pinned directories Unchecked.
A sibling appears beside an individually pinned file with only an exact discovery pattern Unchecked, unless another pattern covers it.
A new sibling matches a broader pattern such as prompts/*.md Unverified.
An existing root is replaced by a different supported type: file to directory or directory to file Mismatch; the file-map shape changes.
Original paths and contents are restored Verified again.

Seal compares paths and contents; it does not have a separate “moved” state. A move is observed as removal at one path and addition or replacement at another.

What moves Per-bundle result
A file is renamed or moved to a new path within the same pinned directory That bundle is Mismatch: old path missing, new path added.
A file moves between two pinned directories to a new destination path Both bundles are Mismatch.
An entire root moves or is renamed to a new path covered by discovery Old root Removed; new root Unverified.
An entire root moves outside discovery and other pinned roots Old root Removed; destination unchecked. Removed alone produces Drift, exit 0.
An individually pinned file moves inside another pinned directory as a new file Old file root Removed; destination directory Mismatch.
An internal file moves outside its pinned directory to an uncovered path Source bundle Mismatch; destination unchecked.
An internal file moves to a new path independently covered by discovery Source bundle Mismatch; new root Unverified.
A move overwrites an existing pinned destination Each root is compared with its own snapshot. The destination can remain Verified if its final paths and bytes still match.
A filename changes only in case inside a pinned directory Mismatch; paths are case-sensitive.
A root name changes only in case Old root Removed; the new name is Unverified only if discovery matches it, otherwise unchecked.
Situation Result
A new empty directory matches discovery Skipped; no state.
A new directory contains only excluded .git entries Skipped; no state.
Empty subdirectories are added or removed inside a pinned directory No hash change; directories themselves are not hashed.
A regular file has zero bytes Valid tracked content; normal comparison rules apply.
Only timestamps, ownership, permissions, or executable bits change, and files remain readable No hash change; filesystem metadata is outside v1 verification.
.git metadata inside a pinned directory changes Excluded; no hash change.
A seal.json file inside an ordinary bundle changes Ordinary bundle content; Mismatch. It is not the project lockfile.
A symlink, device, socket, or FIFO appears inside a pinned root or as a discovery leaf match Mismatch; unsupported types are not followed or silently accepted.
A recorded path contains an intermediate symlink Mismatch; verification does not hash through it.
A per-bundle file read or hashing operation fails Mismatch if the rest of verification can complete.
Two file paths inside a bundle collide after Unicode NFC normalization Hashing fails; Mismatch if verification can otherwise complete.
Resolving recorded paths or traversing discovery encounters a system/I/O error Fatal; exit 2.
What changes Result
seal.json is missing or the CLI runs from a directory without it Fatal; exit 2. The CLI does not search parent directories.
The lockfile has malformed JSON, invalid hashes, unsupported fields/version, empty file maps, or inconsistent aggregate hashes Fatal; exit 2, before classification.
Discovery is narrowed so it no longer statically covers a recorded root Invalid lockfile; exit 2, even if that root is absent from disk.
Bundle roots are nested, or discovery patterns can produce nested roots Invalid lockfile; exit 2.
A lock entry is removed while its root remains discoverable Unverified.
An entry and all discovery coverage for its root are removed together That root becomes unchecked, assuming the remaining lockfile is valid.
Broad discovery is narrowed to exact patterns that still cover all recorded roots Existing roots remain checked; new siblings outside those exact patterns become unchecked.
Content changes and is successfully re-pinned It matches the new baseline; Verified if nothing else differs.
Content and its hashes are edited together consistently Can be Verified. Hashing does not establish who approved the change.
Only a valid informational revision value changes Does not affect content verification.
policy changes from block to warn Mismatch/Unverified produces Warning, exit 0, instead of Blocked, exit 1.
policy has an unsupported value Invalid lockfile; exit 2.
Situation Result
A remote payload changes while the pinned downloader stays identical No local hash change; the downloader can remain Verified.
A dependency or instruction outside all sealed roots and discovery changes Unchecked by the CLI.
Files change after verification completes Not detected until another verification.
Changed files are restored before the next verification They can be Verified; Seal checks current contents, not mutation history.

See the threat model for external dependencies, trust in the lockfile, and native load-time enforcement.

Given these lockfile entries (hashes are abbreviated for illustration; this is not a usable lockfile):

{
"version": 1,
"discovery": [".claude/skills/*"],
"policy": "block",
"bundles": {
"./.claude/skills/code-review": {
"contentHash": "sha256:9f86…",
"files": { "SKILL.md": "sha256:3a7b…" }
},
"./.claude/skills/deploy-guard": {
"contentHash": "sha256:7d86…",
"files": { "SKILL.md": "sha256:f4d3…" }
},
"./.claude/skills/deleted-skill": {
"contentHash": "sha256:1234…",
"files": { "SKILL.md": "sha256:abcd…" }
}
}
}

And this on‑disk state:

.claude/skills/
├── code-review/ ← matches lockfile
├── deploy-guard/ ← SKILL.md was edited
├── new-thing/ ← exists on disk, not in lockfile
└── (deleted-skill is gone)

Per‑bundle states:

Bundle State
./.claude/skills/code-review Verified
./.claude/skills/deploy-guard Mismatch
./.claude/skills/deleted-skill Removed
./.claude/skills/new-thing Unverified

Because policy is "block" and there is a Mismatch (and an Unverified), the overall outcome is Blocked. The CLI exits with code 1.

The --json payload for this run:

{
"status": "Blocked",
"verified": ["./.claude/skills/code-review"],
"unverified": ["./.claude/skills/new-thing"],
"removed": ["./.claude/skills/deleted-skill"],
"mismatch": ["./.claude/skills/deploy-guard"]
}

Each array is sorted lexicographically by UTF‑8 bytes (NFC). The verified, unverified, removed, and mismatch keys are always present, even when empty.

State Action
Mismatch from an intentional update Review the actual content changes, run seal pin <root>, then review and commit the source and lockfile diffs together.
Unexpected Mismatch Investigate or restore the pinned contents before verifying again. Do not re-pin blindly.
New bundle you intend to approve Review its contents, then run seal pin <root> and review the resulting lockfile change.
Unexpected Unverified bundle Investigate how it appeared before approving it.
Intentional Removed bundle seal pin --prune can remove its lockfile entry. This is a bulk operation that can also re-pin other changed or new roots; review the entire proposal.
Unexpected Removed bundle Restore the root at its original path and verify its contents.
Moved root you intend to keep Review the destination, pin it, and prune the old missing entry. Ensure discovery covers the intended future additions.
Fatal error Correct the lockfile or filesystem problem, then rerun verification.

A native integration classifies the project-local units it would load and applies policy immediately before loading them. It must also suppress project-external extensions in sealed projects; inability to do so is a native Blocked condition. The standalone CLI does not enforce that suppression or control an agent’s loading decisions.

See for agent authors for the native contract and exit codes for command-level results.