seal pin
seal pin records the current contents of one or more bundles into seal.json. It has two modes, distinguished by whether positional arguments are supplied:
- Targeted:
seal pin <path>...: re‑hash the named bundles and, for new uncovered keys, add exact discovery coverage. - Bulk:
seal pin(no args): rebuild every entry thatdiscoverymatches, plus every existing key inseal.json.
pin requires seal.json to exist. If it does not, run seal init first.
Synopsis
Section titled “Synopsis”seal pin <path>... [--verbose] # targetedseal pin [--prune] [--verbose] # bulk| Flag | Mode | Effect |
|---|---|---|
--prune |
bulk only | Drop lockfile entries whose sealed roots are no longer on disk. Without it, removed entries appear in the summary but are kept in seal.json. |
-v, --verbose |
both | Accepted by the CLI. The current pin summary remains per-bundle; use seal verify --verbose for per-file mismatch detail. |
There is no -y / --force flag. Confirmation is required and cannot be bypassed.
Targeted mode
Section titled “Targeted mode”seal pin <path> [<path>...]Use targeted mode to pin specific bundles by path. This is useful when you just added a new skill, edited an existing one, or want to record a precise set of changes. A target can be a directory sealed root or a regular-file sealed root.
What happens, in order:
- Locate and validate
seal.json. Missing → exit2withseal: pin: seal.json not found; run 'seal init' first. Invalid → exit2with the validation error. - For each path argument:
- Resolve to its true filesystem string case (no case‑insensitive OS fallback).
- Reject any symlinked path segment; Seal v1 never follows symlinks.
- Verify the resolved path is inside the project root, points to a directory or regular file, and produces a valid bundle key.
- Reject nested boundaries before hashing. If the target is inside an existing recorded sealed root, or would become a parent of an existing recorded sealed root, exit
2with an actionable diagnostic. - Hash every named sealed root before any classification, so an unreadable bundle aborts cleanly without partial in‑memory changes.
- Classify each target as one of:
- new: not currently in
seal.json. - unchanged: already in
seal.json,contentHashmatches. - modified: already in
seal.json, contents differ.
- new: not currently in
- If every target is unchanged: print
No changesand exit0without prompting or touching the lockfile. - Otherwise, print the summary and prompt:
Apply these pin changes? [y/N]. - On
y: update the named bundle entries. If a target key is not already statically produced bydiscovery, append the narrowest exact pattern that produces it, then acquire the file lock, writeseal.jsonatomically, printPinned <n> bundle(s), exit0. - On any other answer: print
pin aborted, exit2.
Targeted mode leaves other recorded bundle entries untouched, but it may append exact discovery patterns to keep the lockfile valid.
Exact discovery coverage makes the named key verifiable; it does not fence siblings. If you pin docs/contributor-prompt.md, the added pattern is docs/contributor-prompt.md, not docs/*.md. To detect future sibling files in that directory, use a broader per‑child pattern such as docs/*.md, or pin the containing directory as the single sealed root.
Resolution edge cases
Section titled “Resolution edge cases”- Path outside the project root. Exit
2with a diagnostic naming the path and project root. - Path does not exist or has the wrong case. Exit
2with a diagnostic naming the unmatched path component. Paths are resolved case-truly even on case-insensitive filesystems. - Path is neither a regular file nor a directory. Exit
2. - Path contains a symlink segment. Exit
2. Symlinks are unsupported even when they point at a regular file or directory. - Path would create nested bundle keys. Exit
2. Re‑pin the containing root, remove the child entry, or intentionally change the discovery boundary first. - Case mismatch on a case‑insensitive filesystem. The CLI requires the path argument to match the directory’s true filesystem case. If the lockfile has
./plugins/MySkillbut the disk reportsmyskill, they are treated as separate bundles. The summary shows the lockfile entry as removed and the disk directory as new.
Bulk mode
Section titled “Bulk mode”seal pinUse bulk mode to re‑hash everything: every entry already in bundles plus every directory or regular file matched by discovery. This is what to run after git pull (to pick up any legitimate upstream changes) or after deleting/adding a few bundles at once.
What happens, in order:
- Locate and validate
seal.json(same as targeted mode). - Expand
discoverypatterns to candidate sealed roots. - Build the union:
discovered ∪ existing-keys-in-lockfile. - For each candidate:
- If the sealed root does not exist on disk → classify as removed.
- Otherwise, hash it and classify as new, unchanged, or modified.
- Count pending writes:
- new and modified always count.
- removed counts only if
--pruneis set. Without--prune, removed entries are reported in the summary but the lockfile keeps them, per the spec’s pruning rule.
- If no writes are pending: print the summary (so you still see reported‑but‑not‑applied removals), then
No changes, exit0. - Otherwise, print the summary and prompt:
Apply these pin changes? [y/N]. - On
y: writeseal.jsonatomically, printPinned <n> bundle(s), exit0. On any other answer:pin aborted, exit2.
Edge cases shared by both modes
Section titled “Edge cases shared by both modes”- Unreadable file during hashing (permission denied, I/O error): abort, leave
seal.jsonunmodified, exit2. - Symlink or other non‑regular file inside a sealed root: abort, leave
seal.jsonunmodified, exit2. (Duringverify, the same situation classifies the bundle as Mismatch instead of aborting.) - Lockfile contains unknown fields: invalid lockfile, exit
2. The CLI does not silently strip them. - Lockfile’s recorded
contentHashdoes not equal the aggregate of itsfiles: invalid lockfile, exit2, independent ofpolicy.
TTY requirement
Section titled “TTY requirement”pin requires a TTY whenever changes are pending. If you run it in a non‑interactive environment with changes to apply:
seal: pin: changes require confirmation but no TTY availableExit 2. This is intentional: pinning is strictly a user‑supervised act.
Tools that batch‑update lockfiles in CI should regenerate the file from a known clean state, commit the diff, and let humans review the PR. Never auto‑accept the prompt.
Concurrency
Section titled “Concurrency”pin acquires an exclusive flock on a sibling seal.json.lock file and writes via atomic rename. Multiple concurrent invocations serialise cleanly; you never get a torn file.
Examples
Section titled “Examples”Re‑pin a single skill after editing it:
$ seal pin ./.claude/skills/code-review ~ modified ./.claude/skills/code-review (12 files)
0 unchanged
Apply these pin changes? [y/N] yPinned 1 bundle(s)Bulk re‑pin with prune after a refactor:
$ seal pin --prune ~ modified ./.claude/skills/code-review (12 files) ~ modified ./.claude/skills/deploy-guard (4 files) + new ./.claude/skills/new-skill (3 files) - removed ./.claude/skills/old-skill
0 unchanged
Apply these pin changes? [y/N] yPinned 4 bundle(s)Bulk run when nothing has changed (no prompt, no write):
$ seal pin
2 unchangedNo changes