Skip to content

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 that discovery matches, plus every existing key in seal.json.

pin requires seal.json to exist. If it does not, run seal init first.

Terminal window
seal pin <path>... [--verbose] # targeted
seal 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.

Terminal window
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:

  1. Locate and validate seal.json. Missing → exit 2 with seal: pin: seal.json not found; run 'seal init' first. Invalid → exit 2 with the validation error.
  2. 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.
  3. 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 2 with an actionable diagnostic.
  4. Hash every named sealed root before any classification, so an unreadable bundle aborts cleanly without partial in‑memory changes.
  5. Classify each target as one of:
    • new: not currently in seal.json.
    • unchanged: already in seal.json, contentHash matches.
    • modified: already in seal.json, contents differ.
  6. If every target is unchanged: print No changes and exit 0 without prompting or touching the lockfile.
  7. Otherwise, print the summary and prompt: Apply these pin changes? [y/N].
  8. On y: update the named bundle entries. If a target key is not already statically produced by discovery, append the narrowest exact pattern that produces it, then acquire the file lock, write seal.json atomically, print Pinned <n> bundle(s), exit 0.
  9. On any other answer: print pin aborted, exit 2.

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.

  • Path outside the project root. Exit 2 with a diagnostic naming the path and project root.
  • Path does not exist or has the wrong case. Exit 2 with 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/MySkill but the disk reports myskill, they are treated as separate bundles. The summary shows the lockfile entry as removed and the disk directory as new.
Terminal window
seal pin

Use 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:

  1. Locate and validate seal.json (same as targeted mode).
  2. Expand discovery patterns to candidate sealed roots.
  3. Build the union: discovered ∪ existing-keys-in-lockfile.
  4. 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.
  5. Count pending writes:
    • new and modified always count.
    • removed counts only if --prune is set. Without --prune, removed entries are reported in the summary but the lockfile keeps them, per the spec’s pruning rule.
  6. If no writes are pending: print the summary (so you still see reported‑but‑not‑applied removals), then No changes, exit 0.
  7. Otherwise, print the summary and prompt: Apply these pin changes? [y/N].
  8. On y: write seal.json atomically, print Pinned <n> bundle(s), exit 0. On any other answer: pin aborted, exit 2.
  • Unreadable file during hashing (permission denied, I/O error): abort, leave seal.json unmodified, exit 2.
  • Symlink or other non‑regular file inside a sealed root: abort, leave seal.json unmodified, exit 2. (During verify, 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 contentHash does not equal the aggregate of its files: invalid lockfile, exit 2, independent of policy.

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 available

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

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.

Re‑pin a single skill after editing it:

Terminal window
$ seal pin ./.claude/skills/code-review
~ modified ./.claude/skills/code-review (12 files)
0 unchanged
Apply these pin changes? [y/N] y
Pinned 1 bundle(s)

Bulk re‑pin with prune after a refactor:

Terminal window
$ 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] y
Pinned 4 bundle(s)

Bulk run when nothing has changed (no prompt, no write):

Terminal window
$ seal pin
2 unchanged
No changes