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:
sealasserts 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.
sealcommits thero_compatbit 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:
-
fswill not mint a writable capability to a sealed file. Refused atopen, 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. -
Every write path refuses:
write, the streamingwrite_at, the journaled variant, andwrite_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
Entryeach write route walks to, so a new route "cannot forget to ask". Butwrite_newdoes 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.copyiswrite_newfollowed by streamingwrite_at, each in its own transaction, sowrite_newtruncated and committed beforewrite_atrefused - the command reported failure and the file was already empty. Found by adding seal coverage toselfcheck, and pinned byosdev test fs-time, which fails on exactly that check if the guard is removed. -
The volume records a
ro_compatfeature 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.