Skip to content

Bundles & components

A bundle is the unit Seal verifies. Understanding what counts as one bundle versus a component inside one is the difference between a clean lockfile and a confusing one.

Concept Meaning
Bundle A loadable agent extension with a single top‑level identity and a sealed root. Bundles are the primary subject of Seal v1.
Component A file or sub‑resource within a bundle (e.g. a skill, hook definition, prompt, script, template). Tracked as part of its containing bundle.
Sealed root The project-local directory or regular file that makes up the bundle. Directory roots are hashed recursively; regular-file roots are hashed as a single tracked file.
Bundle key The lockfile identifier: a ./‑prefixed, project‑root‑relative path to the sealed root.

A bundle may contain skills, hooks, prompts, scripts, templates, tool definitions, MCP configs, assets, and related resources. A component does not receive a separate Seal entry merely because the host can use it through that bundle. A component has independent Seal identity only if the implementation can discover and load it directly as its own unit.

Seal applies to the directly loadable unit. If an implementation loads an enclosing bundle, the bundle is the unit that is classified and verified against seal.json.

Two normative rules follow:

  1. A bundle key MUST NOT be a subdirectory of another bundle key present in seal.json. Paths are evaluated using discrete directory segments, not raw string prefixes.
  2. If a nested directory exists inside a bundle’s sealed root, it is a component of that bundle and MUST NOT be sealed as a separate entry.

If nested bundle keys appear in seal.json, the lockfile is invalid and the implementation MUST fail closed.

A plugin contains a skill at skills/review/SKILL.md. The host loads the skill only via the plugin. Then:

  • The plugin is the directly loadable unit and gets a seal.json entry.
  • The skill is covered by the plugin’s files and contentHash.
  • The skill does not get its own entry.

If the host can also load that skill directly (without going through the plugin), the skill directory becomes its own directly loadable unit, and it pins as a separate entry instead. The two situations are mutually exclusive for a given implementation.

A bundle key identifies the project‑local sealed root. Path normalization ensures that independent implementations produce the same key for the same directory or regular file.

Input Normalized bundle key
plugins/foo ./plugins/foo
./plugins/foo/ ./plugins/foo
.agents/skills/review ./.agents/skills/review
docs/agent-prompt.md ./docs/agent-prompt.md

Rules:

  • Project‑root‑relative path, prefixed with ./, forward slashes only.
  • The exact key . is invalid. Seal v1 does not support the project root itself as a sealed root.
  • Keys MUST NOT contain .., repeated separators, or a trailing slash.
  • Keys MUST NOT be nested under another bundle key in the same lockfile.
  • When bundles is non-empty, each key MUST be statically produced by at least one discovery pattern.
  • All path strings are normalized to Unicode Normalization Form C (NFC) before sorting, hashing, or writing to the lockfile.
  • Paths are strictly case‑sensitive. The CLI MUST NOT rely on case‑insensitive OS fallback to resolve a bundle.

A bundle key identifies the local sealed root, not the bundle’s upstream provenance. During verification, the implementation derives the bundle key from the project‑local sealed root it would otherwise load, then hashes that sealed root. Verification never fetches remote content.

Term Where the sealed root lives
Project‑local loadable unit Inside the project root containing seal.json. Authorized by the lockfile.
Project‑external extension Outside the project root: user‑level, home‑directory, organization‑level, global, admin, cache‑installed.

Seal v1 lockfiles authorize only the project‑local extension surface. Project‑external extensions are unconditionally suppressed in a sealed project, regardless of policy.

If a repository pins both a Claude‑oriented and a Codex‑oriented project‑local bundle in the same seal.json, each agent verifies only the project‑local bundles it can identify and load. An entry for a bundle outside the current implementation’s loadable surface does not affect that implementation’s verification result.

For a directory sealed root, every regular file under the sealed root is hashed, with these exclusions:

  1. Any file or directory exactly named .git (at any depth within the sealed root), along with its contents. This ensures version‑control metadata does not cause hash mismatches.
  2. The seal.json lockfile, but ONLY if the bundle’s sealed root contains the project lockfile. This is a circular case normal v1 lockfiles avoid because the project root itself is not a valid sealed root.

Everything else is tracked, including dependency directories (node_modules, vendor, etc.).

For a regular-file sealed root, the file itself is hashed as the sole tracked entry named ..

A seal.json file inside an ordinary bundle directory is regular bundle content and is tracked.

Symlinks and other non‑regular files are unsupported in v1. The exact handling differs between commands:

  • During verify, encountering an unsupported file classifies the bundle as Mismatch.
  • During pin, encountering one aborts the operation and leaves seal.json unmodified.

Runtime state (caches, logs, downloads, temp files, generated artifacts) MUST be stored outside the bundle root. If a bundle writes runtime state into its own root after pinning, those changes are treated like any other modification and cause verification to fail until the state is removed or the bundle is re‑pinned.

For each tracked file:

file_hash = SHA-256(raw file bytes)

No content transformation: no line‑ending normalization, no text decoding, no whitespace stripping, no structured‑file parsing.

The aggregate contentHash is computed deterministically:

  1. Normalize each tracked path to a sealed‑root‑relative path with forward slashes (NFC).
  2. Sort the normalized paths lexicographically by their UTF‑8 byte sequence.
  3. For each file, build "<path>:sha256:<hex>".
  4. Join all entries with a single LF (\n).
  5. UTF‑8 encode the joined string.
  6. SHA‑256 of those bytes. Hex‑encoded lowercase, prefixed with sha256:.
contentHash = SHA-256( UTF8( sort(paths).map(p => p + ":" + files[p]).join("\n") ) )

The LF delimiter prevents concatenation collisions. The aggregate changes if any file is added, removed, or modified.