Utility: seal - freeze a file's content, permanently

Status: Built. Verified in QEMU by osdev test fs-time (15/0), which seals a file, proves the write is refused, reboots the machine, and proves it is still refused and the content is still the original.

seal <path> [yes]

Freeze a file's content. After this the file can be read, listed, renamed, moved and deleted - but its bytes can never change again.

It asks [y/N] first, because there is no unseal. seal <path> yes skips the prompt - a confirm reads the console, so a script cannot answer one, and the warning still prints either way. What yes buys is not silence, it is the ability to be automated.

Why there is no unseal

A seal a holder can lift is not a guarantee, it is a request. The entire value of the flag is that nothing short of deleting the file undoes it: an auditor reading a sealed file does not have to establish who had write access in the meantime, because nobody did.

That is also why seal is the only command here that asks before acting on a single file. Every other one can be undone by doing the opposite.

Sealing an already-sealed file SUCCEEDS

seal on a file that is already sealed returns Ok and changes nothing.

This was decided in the code and never written down here, which is how it was found. The differential oracle (osdev test fs-model, docs/gsfs-carnage.md §3.2) builds a model of the filesystem from THIS page and compares it against the real one. The model predicted a refusal - a reasonable reading of "there is no unseal", and wrong. fs has carried if e.sealed { return Ok(()); } // idempotent: already frozen all along; a reader of this page had no way to know.

Idempotent is the right answer, and worth stating rather than merely recording:

  • seal asserts an invariant, it does not perform an event. The invariant is "this file's bytes never change again". If it already holds, the caller has what they asked for.
  • An error should mean something went wrong. Nothing did. A script that seals defensively - because it cannot know whether an earlier run got that far - would have to special-case a failure that reports success-by-another-name, and that is the kind of refusal CLAUDE.md §26.7 is about from the other direction.
  • It is also the only choice that composes with a crash. seal commits the ro_compat bit before the record flag, so an interrupted seal can leave the volume announcing the feature without the file being sealed; re-running must then be able to finish the job rather than refuse it.

The one thing it does NOT do is re-confirm: the [y/N] prompt still appears, because the command cannot know the file is already sealed until it has asked fs.

What it does NOT promise

Stated plainly, because a security feature that is vague about its edges is worse than one that is narrow and clear:

  • It does not stop deletion. A seal freezes CONTENT, not existence, and deleting a file needs authority over its parent directory rather than over the file. Refusing deletion would make a sealed file unremovable, so a disk could be filled with rubbish nobody is permitted to clear - a denial of service bought with a guarantee nobody asked for.
  • It does not stop renaming or moving. The bytes are what is frozen. Archiving a sealed log into another directory is a reasonable thing to want, and it changes nothing about the content.
  • It is not encryption. A sealed file is as readable as any other, to anyone who can read the disk.

Output

gsh> seal /audit.log
seal /audit.log - its content can NEVER be changed again, and there is no unseal.
 Seal it? [y/N]: y
sealed /audit.log

gsh> write /audit.log tampered
write: failed - file is sealed - its content cannot be changed

gsh> dir /
/
  NAME                  TYPE       SIZE  MODIFIED
  audit.log             seal      812 B  2026-09-17 07:33
  1 entries

The refusal says WHY, and did not always. It read write: failed (bad path, or parent missing?) until the audit that followed this utility shipping: fs knew the reason, logged it, and dropped it at the reply, so sealing a file and then writing to it sent the operator hunting for a typo in a path that was perfectly correct. A failure that misdirects is worse than one that says nothing (CLAUDE.md §26.7). The reason now rides back after the unchanged error byte.

A sealed file shows as seal in the TYPE column of dir, not as a marker beside file. It is less an attribute of a file than a different kind of thing to have on a disk - one you cannot change - and burying that in a suffix makes it easy to miss exactly when it matters.

How it is enforced

Three refusals, and the first is the one that matters:

  1. fs will not mint a writable capability to a sealed file. Refused at open, not at each write, because handing out a capability that looks writable and fails on use is a worse answer than refusing plainly - and §7.3 says rights narrow, so a capability that cannot be honoured should never exist.

  2. Every write path refuses: write, the streaming write_at, the journaled variant, and write_new, which did not until 2026-09-18.

    That omission is worth keeping, because the reasoning that produced it sounded sufficient. This said the seal is carried on the Entry each write route walks to, so a new route "cannot forget to ask". But write_new does not READ the entry it replaces - it OVERWRITES it, constructing a fresh one with the flag cleared - so there was nothing to forget. Carrying a flag on a record only protects the routes that consult that record.

    The reachable consequence was not a subtle one: copy <anything> <sealed file> destroyed the sealed content. copy is write_new followed by streaming write_at, each in its own transaction, so write_new truncated and committed before write_at refused - the command reported failure and the file was already empty. Found by adding seal coverage to selfcheck, and pinned by osdev test fs-time, which fails on exactly that check if the guard is removed.

  3. The volume records a ro_compat feature bit the first time anything is sealed, so a build that does not know about seals mounts the whole volume READ-ONLY (§6.15) rather than writing through a flag it cannot see.

Where the bit lives, and what that costs

The 64-byte directory record was full, so the flag rides the top bit of the 64-bit size - room no file can reach, since 2^63 bytes is eight exabytes. Every size read goes through an accessor that masks it off, which is not tidiness: write_at bounds a fragmented file's extent by its size, so the flag leaking into that arithmetic would let a write run past the file's own blocks.

The one cost is that a build which does not know this feature would display a nonsense size for a sealed entry. That is why the ro_compat bit exists: such a build mounts read-only, so it can neither act on the wrong number nor change anything. Full reasoning in docs/gsfs-next.md §3.

Conventions

Obeys utilities/0_conventions.md: seal help prints usage, the argument is a path (so seal is NOT in NO_PATH_CMDS and Tab completes paths), raw facts without editorialising, and the confirm uses the same line-edited prompt as drives flash - backspace works, and the decision is the final line.