# Using Peios

> Operating a Peios system — the registry, filesystems, and the everyday tools.

---

# Threads and processes

_Peios / Using Peios / Threads and processes_

> A process is a running program; a thread is a single line of execution inside it. Every action the system takes happens on some thread.

A **process** is a program that is running. When a program starts, the system
gives it a block of private memory that only it can see, a table of the files
and other resources it holds open, and a name the system tracks it by. That
running program, plus everything the system keeps for it, is a process. When
the program finishes, its process goes away.

A process can also do several things at once by running more than one
**thread**. A thread is a single line of execution — one sequence of steps the
system is working through. A process always has at least one thread (its
initial execution is its first thread), and it can start more. Every thread in
a process shares the same private memory and the same open resources. What
each thread keeps to itself is its own place in its sequence: each runs its
own steps, at its own pace, possibly in parallel with the others.

## The actors of the system

Processes and threads are what carry out work on the system. Every action —
opening a file, sending data over a network, starting another program — is
performed by some thread. When the system decides whether an action is
allowed, the question it answers is "is this thread allowed?" When it records
that something happened, it records which thread did it.

A thread always acts as someone — a person, a service, or the system itself.
Peios carries that identity along with the thread on an object called a
**token** (see [Tokens](/peios/security-fundamentals/tokens/overview.md)). Two facts matter here:

- Every thread is always acting as someone. There is no "nobody" state.
- When one process starts another, the new process begins acting as the same
  identity as the process that started it.

## What a process has

The things the system keeps for every process:

| A process has | What that means |
|---|---|
| Private memory | working space only this process can see; other processes cannot read it |
| Open resources | the files, connections, and other things it currently holds open |
| An identity | who it is acting as (carried on its token) |
| A place in a family tree | every process was started by another, so processes form a tree |
| A lifecycle | it is created, it runs, and it ends — and its end is always observed |
| One or more threads | the lines of execution doing its work |

This is not the full list — a process also carries other per-process state,
such as its
[Process Security Block](/peios/using-peios/threads-and-processes/the-process-security-block.md)
(PSB), which holds its security-related settings.

## Where to start

Continue with [The process and thread model](/peios/using-peios/threads-and-processes/the-process-and-thread-model.md)
for how a thread and a process relate, and why the thread — not the process —
is the more basic unit, with a "process" being one particular way of using
them.

This topic also covers:

- [Creating processes](/peios/using-peios/threads-and-processes/creating-processes.md) — how a
  process starts another, and what the new one begins with.
- [Process lifecycle](/peios/using-peios/threads-and-processes/process-lifecycle.md) — how a
  process ends, and how the system cleans up after it.
- [Process relationships and job control](/peios/using-peios/threads-and-processes/relationships-and-job-control.md)
  — the process tree, process groups, and sessions (distinct from the logon
  sessions in [Logon sessions](/peios/security-fundamentals/logon-sessions/overview.md)).

---

# The process and thread model

_Peios / Using Peios / Threads and processes_

> A thread is the basic unit of work; a process is the boundary around its threads, holding the memory, open resources, and identity they share.

A running program has two separable parts: the threads, which run, and the
process, which contains them.

The **thread** is the part that runs — one line of execution, a single
sequence of steps the system works through. A machine with several cores runs
that many threads genuinely in parallel, one per core; a single core runs many
threads by switching between them. Which thread runs, on which core, and for
how long is decided by the system's **scheduler** — a subject of its own. Some
tools and logs call a thread a **task**; it is the same thing.

The **process** is the boundary around a group of threads. It does not run —
it contains. A process holds the things its threads share: their private
memory, their open files and connections, and their identity. Every thread
inside one process sees the same memory and the same open resources; threads
in two different processes do not. That separation is what stops one program
from reaching into another's memory or files.

The thread does the work; the process is the shared space the work happens in.
A program that does one thing at a time is a process with a single thread. A
program that does several things at once is a process with several threads,
all sharing one space.

## Why the thread is the more basic idea

Threads, not processes, are the fundamental unit: what the system runs is
threads, and a "process" is the answer to the question "what does this thread
share, and with whom?" Bundle some threads together so they share one memory,
one set of open resources, and one identity, and that bundle is a process.
Give a thread its own fresh, separate memory instead of sharing an existing
one, and you have made a new process.

This matters because it explains creation. Making a new thread and making a
new process are the same mechanism with a different answer to "how much does
the new line of execution share with the one that made it?" Share everything,
and the result is another thread in the same process. Share nothing, and the
result is a new process. Everything in between is possible too. How the
operation is actually performed is covered in
[Creating processes](/peios/using-peios/threads-and-processes/creating-processes.md).

## What threads share, and what they don't

Within one process, every thread shares:

| Shared across threads | What that means in practice |
|---|---|
| The process's memory | One thread can read and change data another thread is using — cooperation is cheap, but concurrent access must be coordinated. |
| The open resources | A file one thread opens is usable by all of them. |
| The identity | By default every thread acts as the process's identity. |

Each thread keeps its own place in its sequence — which step it is on right
now. That is the point of having more than one: each thread makes progress
through its own work independently of the others.

There is one exception to the shared identity. A single thread can temporarily
act as a different principal — for example, a service handling a request can
act as the requester for that one piece of work, then revert. Only that one
thread is affected, and only until it reverts. This is covered in
[Impersonation](/peios/security-fundamentals/impersonation/overview.md); here it is enough to know that
"every thread in a process shares the process's identity" is the default, not
an unbreakable rule.

## Naming a process

The system gives each process a number called its **process ID**, or **PID**,
so that people and other programs can refer to it — to inspect it, signal it,
or wait for it to finish. Threads are individually identifiable too, by a
thread ID — see
[thread operations](/peios/using-peios/threads-and-processes/thread-operations-reference.md).

A PID is short and convenient, but it only names a process while that process
exists. Once a process ends, its PID can later be reused for a completely
different process. A PID tells you which process is which right now, but it is
useless as a lasting record — two unrelated processes can hold the same PID at
different times.

For that, every process also has a **Process GUID** (globally unique
identifier): a name that is never reused, assigned to each process when it
starts and fixed for the rest of its life. Where the PID is the convenient
short handle for the moment, the Process GUID is the permanent one — it
identifies that one specific process and no other, even long after it has
ended. This is what lets a record of something that happened on the system
name exactly the right process, with no chance of confusing two that happened
to share a PID.

Both the PID and the GUID refer to a process. Neither says who the process is
acting as — that is a separate question, answered by its identity rather than
by any number or name.

## Where to go next

To see how a process or a thread is created — and what a new one starts out
with — read [Creating processes](/peios/using-peios/threads-and-processes/creating-processes.md).

---

# Creating processes

_Peios / Using Peios / Threads and processes_

> How processes are created — fork, exec, spawn, and clone — what a new process starts with, and how to hold a reliable handle to one with a pidfd.

Every process on the system was started by another process — a process that is
already running has to create it. Creation is built from two operations, which
combine to cover every case.

## Splitting: one process becomes two

A running process can **split** into a copy of itself. The original keeps
going, and a second process appears beside it that begins as a near-identical
copy. The original is called the **parent** and the copy the **child**.

The child starts with:

- **Its own private memory**, copied from the parent. From the moment of the
  split the two memories are separate — a change one process makes is
  invisible to the other.
- **Copies of the parent's open resources** — the files and connections the
  parent had open, the child has open too.
- **The same identity** — the child begins acting as the same principal as its
  parent. (It inherits the parent's token; how the token carries across a
  split is in [Token lifecycle](/peios/security-fundamentals/tokens/lifecycle.md).)

It also gets things of its own that are not copied: a new PID, and a new
Process GUID — the child is its own distinct process, not a continuation of
the parent. A few pieces of per-process state start fresh rather than being
inherited, too.

This operation is called **fork**.

## Replacing: same process, different program

A process can also **replace** the program it is running with a different one.
It remains the same process — same PID, same Process GUID, same identity — but
from that point on it runs entirely different code. Whatever the previous
program was doing is discarded, replaced wholesale by the new one.

This operation is called **exec**.

## Putting them together

The two operations combine to launch a different program as a new process: a
process **splits**, and the child immediately **replaces** itself with the
program to run. This split-then-replace pair is how most programs are started
— when you run a command, something forks and the child execs your command.
There is also a single combined step, **spawn**, that does both at once for
the common case.

Underneath, splitting is one setting of a more general operation, **clone**,
that lets the creator choose how much the new line of execution shares with
it. Share everything, and the result is another thread in the same process;
share almost nothing, and the result is a new process — the two ends of the
range from
[the process and thread model](/peios/using-peios/threads-and-processes/the-process-and-thread-model.md).
Everyday code rarely uses the general form directly; it makes threads or
spawns programs, which are particular settings of it.

The exact contract — every `clone` sharing flag, precisely what `fork` passes to a
child, and the variants of `exec` — is collected in the
[process creation reference](/peios/using-peios/threads-and-processes/process-creation-reference.md).

## Getting hold of the new process

When a process creates a child, it usually needs to keep track of it — to wait
for it to finish, or to send it a signal later. For this it can hold a
**handle** to the child: a reference to that one specific process, called a
**pidfd**. It is the same kind of handle the system hands out for an open
file.

A process can be given a pidfd for its child at the moment it creates it, or
ask for one for a process that already exists. Either way the pidfd is tied to
that one specific process and can never be mistaken for another, so waiting on
it or signalling through it always reaches the intended target.

This is what makes a pidfd safer than a bare PID. A PID can be reused once the
process it named has ended; code that holds a bare PID risks later acting on a
completely different process that inherited the number. A pidfd never has that
problem — it refers to the one process it was created for and nothing else,
for as long as the program holds it.

## Where to go next

A process that has been created eventually ends. How it finishes, and how the
system cleans up after it, is covered in
[Process lifecycle](/peios/using-peios/threads-and-processes/process-lifecycle.md).

---

# Process lifecycle

_Peios / Using Peios / Threads and processes_

> How a process ends, how its result is collected, what happens when its parent ends first, and how the system cleans up after it.

Every process eventually ends. A process's life has three parts — it is
created, it runs, and it finishes — and the system ensures that when it
finishes, its result is collected and its resources are reclaimed.

## The states it passes through

While it exists, a process is in one of a handful of **states**, and the system
moves it between them. Most of the time it is either **running** — executing,
or ready to run the moment a core is free — or **sleeping**, paused while it
waits for something to happen, such as data to arrive or a timer to fire. A
sleeping process uses no processor time until what it is waiting for is ready;
most processes spend most of their lives asleep.

A few states are worth knowing by name:

- **Stopped** — suspended (for example by job control when a job is paused at
  the terminal); it does nothing until told to resume.
- **Traced** — stopped under the control of a debugger that has attached to it, so
  the debugger can inspect and step it.
- **Uninterruptible sleep** — waiting on something the system will not interrupt,
  almost always a brief piece of disk or device I/O. A process here cannot be woken
  or even killed until the operation finishes; it normally lasts an instant, and a
  process stuck in it is a sign that hardware or a filesystem is stuck too.
- **Zombie** — finished, but still held as a result waiting to be collected (see
  below).

Once its result has been collected the process is gone, removed from the system
entirely.

## Two ways a process ends

A process ends in one of two ways.

Most often it ends **on its own**: it finishes its work and stops. A process
that ends this way leaves behind an **exit status** — a small record of how it
went, usually just "succeeded" or "failed" (and, on failure, a number giving a
rough reason). The act of a process ending itself is called **exit**.

The other way is that a process is **stopped from outside** — something tells
it to stop, and it ends whether or not its work was done. (The messages that
do this are **signals**, a subject of their own.) A process ended this way
also leaves an exit status, marked to show it was stopped rather than
finishing on its own.

Either way, a record of the process remains until its result is collected.

## Collecting the result

Whenever a child changes state — ends, is stopped, or resumes — the system
sends its **parent** a signal, `SIGCHLD`, prompting it to check. The parent
then reads the child's exit status. This is called **waiting** for the
process.

Between the moment a process ends and the moment its parent collects the
result, the finished process is a **zombie**: no longer running, but with the
system still holding its exit status so the parent can read it. Despite the
name, a zombie is harmless — it does no work and uses almost nothing; it is a
result waiting to be collected. The instant the parent collects it, the zombie
is gone for good.

If a parent never collects, zombies accumulate — finished processes whose
results no one read. That is a fault in the parent, not normal behaviour.

A parent holding a [pidfd](/peios/using-peios/threads-and-processes/creating-processes.md) for its
child can wait on it directly — the dependable way to be told the exact moment the
child ends.

The exact calls — every `wait` variant, and how an exit status is read — are in the
[process lifecycle reference](/peios/using-peios/threads-and-processes/process-lifecycle-reference.md).

## When the parent ends first

A parent does not always outlive its children. If a parent ends while one of
its children is still running, that child becomes an **orphan**.

Orphans are not lost. The system's first process — PID 1, which on Peios is
**peinit** — adopts every orphan and stands in as its parent from then on:
when the orphan eventually ends, peinit collects its result, so no finished
process is ever left uncollected. Adoption is automatic; the orphan keeps
running unaffected, only with a new parent.

PID 1 is the catch-all, but a process can arrange to catch orphans within its
own subtree. By marking itself a **subreaper** (with
`prctl(PR_SET_CHILD_SUBREAPER)`), a process becomes the one that adopts any
orphan descended from it — its grandchildren and below — instead of letting
them reparent all the way up to PID 1. Service managers and container managers
use this to keep responsibility for everything they launched: when something
deep in their subtree loses its immediate parent, it reparents to the manager,
which still learns when it finally ends.

## What the system cleans up

When a process ends, the system reclaims what it was using. Its private memory is
freed and its open files and connections are closed.

It also **releases its identity**. A process acts as a principal, carried on
its token; when the process ends, its hold on that token is released. If other
processes were sharing the same identity — for example, siblings from the same
login — the identity lives on for them, and the system clears it away only once
nothing is using it any more. The details are in
[Token lifecycle](/peios/security-fundamentals/tokens/lifecycle.md).

The process's PID becomes available for the system to assign to a future
process. Its Process GUID is not reused — it remains a permanent marker of
that one process, so records of what it did still point unambiguously back to
it long after it is gone.

## Where to go next

Processes are arranged into a family tree and grouped together in ways that
matter for running them. That is
[Process relationships and job control](/peios/using-peios/threads-and-processes/relationships-and-job-control.md).

---

# Process relationships and job control

_Peios / Using Peios / Threads and processes_

> The two ways processes relate — descent, forming a family tree rooted at PID 1, and grouping into process groups and sessions for job control.

Every process is related to others in two different ways: by **descent** — who
started whom — and by **grouping** — which processes are handled together when
they are controlled at a terminal.

## The family tree

Every process was started by another, so every process has a **parent**: the
process that created it. A process can refer to its parent by the parent's PID,
sometimes called the **PPID** (parent process ID).

Follow the parents upward and they all lead back to the same place — PID 1, the
first process, which the system starts at boot. Every other process descends from
it, directly or through a chain of parents, so the processes on a running system
form a single **family tree** rooted at PID 1. This is the same tree that makes
orphan adoption work: when a process loses its parent, PID 1 takes over the role,
[as covered in the lifecycle](/peios/using-peios/threads-and-processes/process-lifecycle.md).

A few processes at the very root of the tree are special. **PID 1** is the first
process started at boot — on Peios, peinit — and the ancestor of all the rest; the
system protects it, refusing to kill it with any signal it has not chosen to handle,
because losing it would bring down the whole tree. Beneath the userspace processes
the kernel runs threads of its own: **PID 0** is the idle process (what a core runs
when it has nothing else to do), and **PID 2**, `kthreadd`, is the parent of every
kernel thread. The kernel's threads — among them the worker threads (`kworker`)
that run deferred work and the threads that service hardware interrupts — appear in
a process listing as the system's own work rather than as programs loaded from disk.

## Process groups

Several processes often do one job together. In a pipeline — the output of one
command feeding into the next — each command is its own process, but they form
one job: you stop, pause, or resume the whole job at once, not a piece at a
time.

To make that possible, related processes are collected into a **process group**.
A process group is a set of processes treated as a unit, so that an action — most
often a signal, like the one that stops a job — can be delivered to all of them
together instead of one by one.

## Sessions and the terminal

Process groups are themselves collected into a larger unit: a **session**, tied
to the **terminal** the work is running at.

A terminal can only take input for one job at a time, so within the session one
process group is the **foreground** group: the one connected to the keyboard, the
one typing reaches, the one a stop-or-interrupt keystroke acts on. Any other
groups are in the **background**, running without holding the keyboard. Moving a
job between foreground and background — and starting, stopping, and resuming jobs
— is **job control**.

## A different kind of "session"

The word "session" has two meanings here, and they are unrelated:

- A **job-control session**, described above, organises processes at a terminal
  — which groups share a terminal, which one is in the foreground. It says
  nothing about who anyone is.
- A **logon session** records one sign-in — a single authentication event — and
  ties together every identity token that came from it. It is part of the
  identity model, covered in [Logon sessions](/peios/security-fundamentals/logon-sessions/overview.md).

They often line up in practice — signing in at a terminal creates a logon session,
and the processes run there share a job-control session — but they are separate
things answering separate questions: "which processes share this terminal?" versus
"where did this identity come from?"

## Where to go next

Beyond its place in the tree and its groups, every process carries a bundle of its
security-related settings — its Process Security Block. That is
[The Process Security Block](/peios/using-peios/threads-and-processes/the-process-security-block.md).

---

# The Process Security Block

_Peios / Using Peios / Threads and processes_

> The Process Security Block (PSB) records what a process is — its program's trust, its hardening, and who may operate on it — as distinct from who it acts as.

A process's identity — who it is acting as — is carried on its token, and can
change moment to moment, since a thread can impersonate another principal. A
process has a second aspect that is independent of identity: **what it is**.
What program is it running? How trusted is that program? How hardened is it
against attack? Who is allowed to operate on it?

Those facts are gathered in one place — the **Process Security Block**, or **PSB**.
Every process has one. Where the token answers who this process is acting as, the
PSB answers what this process is. Unlike the token, the PSB never changes when a
thread impersonates: impersonation changes who, never what.

## What the PSB holds

- **Its permanent name.** The process's
  [Process GUID](/peios/using-peios/threads-and-processes/the-process-and-thread-model.md) — the
  never-reused identifier — lives on the PSB.
- **How trusted its program is.** When a process starts running its program, the
  system checks the program's cryptographic signature and records from it how
  trusted the program is. This is the process's **PIP** (Process Integrity
  Protection) label, and it decides which other processes are allowed to
  inspect, signal, or interfere with this one. The barrier is based on what
  program is running, not on who is running it: even a fully privileged process
  cannot disturb a more-trusted one. The full treatment is
  [Process integrity protection](/peios/security-fundamentals/process-integrity-protection/overview.md).
- **How it is hardened.** A set of **mitigations** — restrictions the process
  carries on what it may do with its own memory and code, so that a bug or
  injected code has far less room to do harm. They can only ever be tightened,
  never loosened. The catalog and rules are
  [Process mitigations](/peios/security-fundamentals/process-mitigations/overview.md).
- **Who may operate on it.** Every process has its own security descriptor — the
  rules for who is allowed to act on the process itself: inspect it, signal it, and
  so on. It lives on the PSB alongside the rest.

A few more specialised settings live here too — a process can be marked so that it
may no longer create children, for instance — but those four are the core.

## Inspecting and managing the PSB

Because the PSB is where a process's trust level and hardening live, there is a
dedicated tool for working with it: the **`psb`** command, which inspects a
process's PSB and manages its mitigations and PIP mode.

## Where to go next

The two largest parts of the PSB each have a topic of their own:
[Process integrity protection](/peios/security-fundamentals/process-integrity-protection/overview.md), for the
trust label that governs which processes may interfere with which, and
[Process mitigations](/peios/security-fundamentals/process-mitigations/overview.md), for the self-hardening
flags and how they are applied.

---

# Process creation reference

_Peios / Using Peios / Threads and processes_

> The precise contract for fork, vfork, clone and clone3, the clone sharing flags, the exec family, posix_spawn, and the pidfd calls.

This is the detailed counterpart to
[Creating processes](/peios/using-peios/threads-and-processes/creating-processes.md). That page
explains the ideas; this one gives the exact contract — what each call does, what a
new process inherits, and what each `clone` sharing flag controls.

## fork

`fork()` creates a new process by duplicating the caller. The child is a
near-identical copy that runs independently from the point of the call.

The child **inherits**:

- a private copy of the parent's memory, made lazily (copy-on-write), so the
  duplication is cheap and changes on either side stay invisible to the other
- copies of the parent's open file descriptors — each refers to the same open file
  description, so file offset, status flags, and signal-driven-I/O settings are
  shared between parent and child
- open directory streams and open message-queue descriptors
- open-file-description locks (`flock`, OFD locks) through those shared descriptors
- the parent's signal dispositions (the handler set for each signal)
- the file-creation mask (umask), the current directory, and the root directory
- resource limits and the timer-slack value
- the parent's identity — it begins acting on the same token (see
  [Token lifecycle](/peios/security-fundamentals/tokens/lifecycle.md))
- the parent's mitigations and protection, carried on its
  [PSB](/peios/using-peios/threads-and-processes/the-process-security-block.md)

The child does **not** inherit:

- the parent's PID — it receives a new PID and a new Process GUID
- memory locks (`mlock`, `mlockall`)
- memory regions marked `MADV_DONTFORK` (absent in the child) or `MADV_WIPEONFORK`
  (present but zeroed)
- process resource usage and CPU-time counters — reset to zero
- pending signals — the child's set starts empty
- semaphore adjustments (semadj)
- process-associated record locks (POSIX `fcntl` locks — distinct from the OFD and
  `flock` locks above, which are shared through the inherited descriptors)
- timers (`setitimer`, `alarm`, `timer_create`)
- outstanding asynchronous I/O
- directory-change notifications (`dnotify`)
- the `PR_SET_PDEATHSIG` parent-death-signal setting (reset)
- I/O-port access permissions (`ioperm`)

The child's termination signal — what its parent is notified with when it ends —
is `SIGCHLD`.

The child is created with a **single thread** — the one that called `fork()`. If
the parent had other threads, they do not exist in the child.

## vfork

`vfork()` is a lightweight variant of `fork()` for the one case where the child
will immediately replace itself with another program. It differs in two ways:

- the caller is **suspended** from the call until the child either `exec`s or exits;
- the child runs in the **parent's memory** — no copy is made — until that point.

Because the two share memory and the parent is frozen, the child must do almost
nothing first. The contract: the child must not return from the function that
called `vfork()`, must not modify any variable other than the one holding the
return value, and must not call any other function before a successful `exec` or
`_exit`. Doing otherwise is undefined behaviour.

`vfork` is an optimisation for the create-then-replace pattern. Ordinary code
should use `posix_spawn` (below) rather than reach for it directly.

## clone and clone3

`clone()` is the general creation call. `fork` and thread creation are both
`clone` with particular flag sets; `clone` exposes the choice directly. The caller
gives a set of **flags** controlling what the new task shares with the creator, a
stack for the new task, and locations for thread-ID and thread-local-storage
bookkeeping.

`clone3()` is the modern form, taking a structure instead of positional arguments
so it can grow new fields over time without changing the call:

| Field | Purpose |
|---|---|
| `flags` | The sharing flags (below). |
| `pidfd` | Where to store a pidfd for the new child. |
| `child_tid` / `parent_tid` | Where to record the new thread's ID, in the child's and the parent's memory. |
| `exit_signal` | The signal delivered to the parent when the child ends. |
| `stack` / `stack_size` | The new task's stack and its size. |
| `tls` | The new task's thread-local-storage area. |
| `set_tid` / `set_tid_size` | Request specific thread IDs for the new task — a privileged operation (see below). |
| `cgroup` | Place the child directly into a resource group at creation (see resource management). |

The structure is **size-versioned**: the caller passes the size it knows, the
kernel reads up to the size it knows, and any unknown trailing fields must be
zero. This is how new fields are added without breaking existing programs.

Requesting a specific thread ID (`set_tid`) is a **privileged** operation — it
exists for checkpoint-and-restore tools that must recreate a process with its
original ID. The privilege required follows the
[capability model](/peios/advanced-peios/linux-compatibility/dac-neutralization-and-capabilities.md).

## clone sharing flags

Each flag makes the new task **share** something with its creator instead of
getting its own copy. The flags that build threads and control creation:

| Flag | Effect |
|---|---|
| `CLONE_VM` | Share memory rather than taking a private copy. |
| `CLONE_FS` | Share filesystem context — current directory, root directory, file-creation mask. |
| `CLONE_FILES` | Share the open-descriptor table, so opening or closing a descriptor in one is seen by the other. |
| `CLONE_SIGHAND` | Share the table of signal handlers. Requires `CLONE_VM`. |
| `CLONE_THREAD` | Put the new task in the same process (thread group) as the creator. Requires `CLONE_SIGHAND`. |
| `CLONE_SETTLS` | Set the new task's thread-local-storage area. |
| `CLONE_SYSVSEM` | Share the System V semaphore-adjustment list. |
| `CLONE_IO` | Share the I/O context, so the two are accounted as one for disk I/O. |
| `CLONE_PIDFD` | Return a pidfd for the new child. |
| `CLONE_PARENT` | Make the new task a sibling of the creator — its parent becomes the creator's parent, which is also what is signalled when the task ends. An init process (PID 1) cannot use this flag, as it would create unreapable zombies. |
| `CLONE_PARENT_SETTID` / `CLONE_CHILD_SETTID` | Record the new task's ID in the parent's / child's memory. |
| `CLONE_CHILD_CLEARTID` | Clear the recorded thread ID and wake a waiter when the task exits — the mechanism behind joining a thread. |
| `CLONE_UNTRACED` | Prevent a tracer from forcing tracing onto the new child. |
| `CLONE_VFORK` | Suspend the creator until the child `exec`s or exits (the `vfork` behaviour). |

A **thread** is `CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD`
(plus TLS and thread-ID bookkeeping) — everything shared, same process. A `fork`
is the opposite end, with almost nothing shared.

Being in one thread group has consequences beyond shared memory. Every thread
returns the same PID from `getpid` (the thread-group ID), and signal **actions** are
process-wide — an unhandled fatal signal delivered to any thread ends them all —
though each thread keeps its own signal mask. A thread that ends does not notify the
thread that created it and cannot be collected with a wait call; only once every
thread in the group has ended is the process's parent sent `SIGCHLD`. The new thread
shares the creator's parent, and `CLONE_THREAD` requires `CLONE_SIGHAND` (and hence
`CLONE_VM`).

Two further flag groups select **separation** rather than sharing, each documented
where it belongs:

- The `CLONE_NEW*` flags create **namespaces** — separate views of system
  resources (mounts, process IDs, networking, and more), covered under namespaces.
- `CLONE_INTO_CGROUP` places the new process into a **resource group** at creation,
  covered under resource management.

`CLONE_DETACHED` is a historical flag with no effect and is ignored.

## The exec family

An `exec` replaces the program running in the current process with a different one.
The process keeps its PID, its Process GUID, and its identity — only the program
changes (see [Token lifecycle](/peios/security-fundamentals/tokens/lifecycle.md) for the identity detail).

| Call | What it does |
|---|---|
| `execve` | Replace the current program with the one at a given path. |
| `execveat` | Replace it with a program named relative to an open directory, or — with `AT_EMPTY_PATH` — by an open file descriptor directly. `AT_SYMLINK_NOFOLLOW` refuses a final symbolic link. |
| `fexecve` | Replace it with the program referred to by an open file descriptor (a thin wrapper over `execveat`). |

### What survives an exec

A successful `exec` keeps the process itself but replaces the program it runs.
Preserved across the call:

- the PID, the parent PID, and the Process GUID
- the process's identity (its token), its process group and session, and its
  controlling terminal
- open file descriptors — except those marked close-on-exec, which are closed
- the current directory, root directory, file-creation mask, and resource limits
- pending signals, and the dispositions of signals that were ignored or left at
  their default

Reset or discarded:

- all memory — the program's mappings, stack, heap, and data are replaced; memory
  locks are dropped and `MADV_*` region markings are gone
- every thread except the one calling `exec` — the new program starts single-threaded
- handlers for caught signals — reset to the default; the alternate signal stack is
  dropped
- attached System V shared memory, POSIX shared-memory mappings, open POSIX
  message-queue descriptors and named semaphores, and in-process synchronisation
  objects (mutexes, condition variables)
- POSIX timers, outstanding asynchronous I/O, open directory streams, and registered
  exit handlers
- the floating-point environment, and the process name (set to the new program)

What an `exec` does to a process's identity when the program file is marked to run
as another principal is **not** the Linux set-user-ID model — Peios handles that
through the token, covered in
[setuid and uid0](/peios/advanced-peios/linux-compatibility/setuid-and-uid0.md).

Whether a program is allowed to run at all is a separate, execution-policy
question — covered under binary signing — not part of these calls.

## posix_spawn

`posix_spawn()` is the standard library function for the create-then-replace
pattern: it makes a new process and runs a named program in it with a single call,
with controls for arranging the child's file descriptors and a few attributes
first. It is built on the calls above and is the recommended way to launch a
program, in preference to assembling `fork`/`vfork` and `exec` by hand.

## Process handles (the pidfd family)

A **pidfd** is a file descriptor referring to one specific process — the reliable
handle introduced in
[Creating processes](/peios/using-peios/threads-and-processes/creating-processes.md). `CLONE_PIDFD`
hands one back at creation; a few calls work with them afterwards:

| Call | What it does |
|---|---|
| `pidfd_open` | Obtain a pidfd for a process that already exists, given its PID. |
| `pidfd_send_signal` | Send a signal to the process through its pidfd — with no risk of the PID having been recycled for a different process in between. |
| `pidfd_getfd` | Duplicate one of the target process's open file descriptors into the caller. |

`pidfd_getfd` reaches into another process, so it is not something any process may
do to any other: it is gated by the right to act on the target, governed by that
process's [security descriptor](/peios/using-peios/threads-and-processes/the-process-security-block.md).

## See also

- [Creating processes](/peios/using-peios/threads-and-processes/creating-processes.md) — the conceptual counterpart to this page.
- [Process lifecycle reference](/peios/using-peios/threads-and-processes/process-lifecycle-reference.md) — ending a process and collecting its result.
- [Thread operations reference](/peios/using-peios/threads-and-processes/thread-operations-reference.md) — per-thread calls: TIDs, exit notification, TLS.

---

# Process lifecycle reference

_Peios / Using Peios / Threads and processes_

> The precise contract for ending a process and collecting its result — exit, _exit and exit_group, the exit-status encoding, and the wait family.

This is the detailed counterpart to
[Process lifecycle](/peios/using-peios/threads-and-processes/process-lifecycle.md). That page
explains how a process ends and is cleaned up; this one gives the exact calls — how
a process terminates, what its exit status encodes, and how a parent collects it.

## Ending a process

A process can end itself in a few ways, differing in how much tidying happens first:

| Call | What it does |
|---|---|
| `exit()` | The orderly end. Runs every handler registered with `atexit`/`on_exit` (in reverse order of registration), flushes and closes the standard I/O streams, removes temporary files, then terminates. |
| `_exit()` / `_Exit()` | The immediate end. Terminates at once — no registered handlers run and the standard I/O streams are not flushed — though open file descriptors are still closed. Used when a process must stop without running cleanup, such as a child after a failed `exec`. |
| `exit_group()` | Ends the **whole process** — every thread in it — not just the calling one. This is what ordinary termination uses: a normal `exit()`, and a return from the program's entry point, both end the entire process. |

Returning from the program's entry point is equivalent to calling `exit()` with the
returned value.

At the lowest level the raw `_exit` system call ends only the calling thread; the
library `_exit` and ordinary termination route through `exit_group` so the whole
process ends. Ending one thread while the rest of the process keeps running is the
unusual case.

A process can also be ended **from outside**, by a signal rather than by a call of
its own — covered under signals. Either way, it leaves an exit status behind.

## The exit status

When a process ends it leaves a small **exit status** for its parent to read. It
records one of two outcomes:

- **Ended on its own** — carries an exit code: the low 8 bits of the value passed to
  `exit`/`_exit` (so 0–255, with 0 conventionally meaning success).
- **Ended by a signal** — carries the number of the signal that ended it, plus a
  flag for whether the process produced a core dump.

The status is a packed value, read not directly but through a set of macros:

| Macro | Tells you |
|---|---|
| `WIFEXITED` | the process ended on its own — if so, `WEXITSTATUS` gives its exit code |
| `WIFSIGNALED` | the process was ended by a signal — if so, `WTERMSIG` gives the signal number and `WCOREDUMP` whether it dumped core |
| `WIFSTOPPED` | the process was **stopped** (not ended) by a signal — `WSTOPSIG` gives which |
| `WIFCONTINUED` | the process was resumed by a continue signal |

## Collecting the result

A parent collects a finished child's status — and in doing so clears the
[zombie](/peios/using-peios/threads-and-processes/process-lifecycle.md) — with one of the wait
calls. They differ in which child they target and how much they report:

| Call | What it offers |
|---|---|
| `wait` | Wait for **any** child to end; return its PID and status. The simplest form. |
| `waitpid` | Wait for a chosen target (see below). Options adjust the behaviour: `WNOHANG` returns immediately if nothing is ready (a poll rather than a wait), and `WUNTRACED`/`WCONTINUED` also report children that have stopped or resumed, not only ended. |
| `waitid` | The most precise form. It can target a child by a **pidfd** (`P_PIDFD`) — the reliable handle from [Creating processes](/peios/using-peios/threads-and-processes/creating-processes.md) — as well as by PID, by process group, or any child. It reports through a structured record (which child, and what happened: ended, killed, core-dumped, stopped, continued). Which kinds of change to wait for are chosen explicitly — `WEXITED` for children that have ended, `WSTOPPED` for those stopped by a signal, `WCONTINUED` for those resumed — and its `WNOWAIT` option peeks at the result while leaving the child collectable again later. |
| `wait4` | Like `waitpid`, and additionally returns the child's **resource usage** — processor time consumed, peak memory, and so on. |

`waitpid`'s target is chosen by a single number: a positive value waits for that
exact PID; `-1` for any child; `0` for any child in the caller's own process group;
and a value below `-1` for any child in the process group whose ID is its absolute
value.

Two further options matter for threads and other `clone`-created children, which may
not notify their parent with `SIGCHLD` the way an ordinary child does: `__WCLONE`
waits only for such clone children, and `__WALL` waits for every child regardless
of type.

## See also

- [Process lifecycle](/peios/using-peios/threads-and-processes/process-lifecycle.md) — the conceptual counterpart to this page.
- [Process creation reference](/peios/using-peios/threads-and-processes/process-creation-reference.md) — fork, clone, exec, and the pidfd calls.

---

# Thread operations reference

_Peios / Using Peios / Threads and processes_

> The per-thread operations a program uses directly — reading a thread's ID, the exit-notification mechanism behind thread joins, and thread-local storage.

This is the thread-level companion to
[the process and thread model](/peios/using-peios/threads-and-processes/the-process-and-thread-model.md)
— the operations that act on an individual thread rather than on the process as a
whole.

## Thread IDs

Every thread has its own **thread ID** (TID), returned by `gettid`. Within a
multi-threaded process all threads share one PID — the value `getpid` returns,
which is really the **thread-group ID** (TGID) — but each thread's TID is unique.
In a single-threaded process the TID, PID, and TGID are all the same number.

The TID is how the system targets one specific thread rather than the whole
process: sending a signal to a single thread, setting one thread's CPU affinity, or
addressing it under `/proc`. It names a thread the way a PID names a process — and,
like a PID, it is a reference, not an identity. What a thread is acting as is its
token, never its TID.

The TID is also distinct from the opaque handle a threading library returns when it
creates a thread; that is a library-level identifier, not the kernel's TID.

## Thread-exit notification

A thread can have the kernel notify waiters when it exits — the mechanism threading
libraries build thread join on. `set_tid_address` sets a **clear-tid address** for
the calling thread (and returns its TID). When that thread later terminates, if it
shares memory with other threads, the kernel writes 0 to the integer at that address
and wakes one thread waiting on it (a single futex wake).

A thread waiting for another to finish blocks on that address; the kernel clears it
and wakes the waiter at the exact moment the other thread ends. This is the same
machinery as the `CLONE_CHILD_CLEARTID` flag in
[process creation](/peios/using-peios/threads-and-processes/process-creation-reference.md) — that
flag arranges the clear-tid address at thread creation, where `set_tid_address` sets
it afterwards.

## Thread-local storage

**Thread-local storage** (TLS) gives each thread its own private copy of a variable
— a value that is per-thread rather than shared across the process, even though the
threads share the same memory. The per-thread data lives in an ordinary block of
memory; what makes it per-thread is a register pointing at this thread's block, so
the same code reaches a different copy depending on which thread is running.

The kernel's part is maintaining that per-thread pointer. It is set when the thread
is created (the `CLONE_SETTLS` flag carries the address) and, on x86-64, a running
thread can change it through `arch_prctl` with `ARCH_SET_FS` (the older
`set_thread_area` serves the same purpose on 32-bit). Everything above that — how
thread-local variables are laid out and reached in code — is handled by the compiler
and the threading library.

## See also

- [The process and thread model](/peios/using-peios/threads-and-processes/the-process-and-thread-model.md) — the conceptual counterpart to this page.
- [Process creation reference](/peios/using-peios/threads-and-processes/process-creation-reference.md) — the clone flags that create threads and arrange TLS and clear-tid addresses.

---

# peinit

_Peios / Using Peios / Services & jobs_

> peinit is PID 1 — the single service manager. Services, jobs, and operations; identity built in; and how it differs from the init systems you know.

**peinit** is the init system of Peios — the first userspace process, running at PID 1 for the lifetime of the machine. It is a single-threaded program with one job: manage services, from the first one started at boot to the last one stopped at shutdown. Every supervised process on a Peios system — a long-running daemon, a one-shot setup task, a health check, a scheduled job — is started, watched, and reaped by peinit.

What makes peinit its own thing is that **identity is built in**. peinit does not just launch processes; it launches them *as someone*. Each service runs under a [KACS token](/peios/security-fundamentals/tokens/overview.md) that decides what it can reach, and each service is itself a securable object with a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) that decides who may start, stop, or query it. Service management and access control are the same system, not two systems bolted together.

This page sketches what peinit is responsible for, names the handful of ideas that make it unlike other init systems, and points at the pages that cover each one.

## peinit in one sentence

**peinit is the sole service manager and PID 1 — it reads service definitions from the registry, starts them in dependency order under per-service identities, supervises them through a defined state machine, and tears them down in reverse at shutdown.**

Everything in this topic is an elaboration of that sentence: what a *service definition* is, what *dependency order* means, what the *state machine* looks like, how *identity* is materialised, and how you drive the whole thing from a shell.

## Three kinds of object

peinit is easiest to understand through the three first-class objects it works with. Keeping them straight is most of the battle when you read a status output or an event log.

| Object | What it is | Lifetime |
|---|---|---|
| **Service** | A *definition* — a named unit of execution with identity, policy, and configuration, stored in the registry. It has a runtime state (the [state machine](/peios/using-peios/services-and-jobs/the-service-lifecycle.md)) and, when running, a process. | Long-lived. Persists across restarts and reboots. |
| **Job** | A single *process execution*. Every time peinit forks — a service's main process, a hook, a health check, an ad-hoc run — that is one job, with its own GUID and exit result. | Short-lived. A restart creates a *new* job. |
| **Operation** | A *requested action* on a service — start, stop, restart, reload, reset. Control commands create operations that are validated, queued, and executed. | Short-lived. Resolves to a terminal state, then is dropped. |

A service is the *what*; a job is *what actually ran*; an operation is *what someone asked for*. A single `restart` command, for example, is one **operation**, which stops the current **job** and starts a new one, all against one **service**. Jobs and operations both carry GUIDs and are emitted to [eventd](/peios/security-fundamentals/auditing/overview.md) as they complete, which is how the history of a service is reconstructed after the fact. They get their own page: [Jobs and operations](/peios/using-peios/services-and-jobs/jobs-and-operations.md).

Underneath all three sits the fourth thing peinit is always handling — the **token**, the identity a service runs under. Tokens are not peinit's invention; they are the [system-wide identity object](/peios/security-fundamentals/tokens/overview.md). peinit's role is to obtain the right token for each service and install it before the process runs. That is the subject of [Service identity and privileges](/peios/using-peios/services-and-jobs/identity-and-privileges.md).

## What peinit is not

peinit borrows vocabulary from init systems you may already know, and the familiarity is a trap. Clearing three wrong mental models is most of understanding what peinit actually is.

**It is not systemd.** There are no unit files. peinit does not read, parse, or translate `.service` files, and there is no migration shim. A service definition is a key in the [registry](/peios/using-peios/registry-concepts/overview.md) under `Machine\System\Services\`, made of typed registry values — not a text file under `/etc`. Some *interfaces* are deliberately compatible where that helps: peinit speaks the `sd_notify` readiness protocol, and timer schedules use systemd's `OnCalendar` calendar-expression format. The compatibility stops at those wire formats; the model underneath is different.

**It is not Windows SCM.** The influence is real — services as securable objects with per-service descriptors, token-based service identity, an access-controlled control interface, a structured state machine — but it is *architectural*, not interface-level. peinit does not implement the SCM RPC protocol, Windows service types, or Windows control codes.

**It is not a logging system.** peinit holds a service's `stdout`/`stderr` pipes at birth, so it controls *where* output goes — but storage, indexing, and queries are [eventd](/peios/security-fundamentals/auditing/overview.md)'s job. peinit forwards; eventd is the historian. The same split applies to job and operation history: peinit emits structured events and forgets them; eventd keeps them. See [Service output and logging](/peios/using-peios/services-and-jobs/output-and-logging.md).

A fourth, smaller correction: **peinit does not supervise forking daemons.** A service that double-forks to "daemonise" is solving a problem that does not exist when the manager tracks the process it spawned. peinit tracks every process by [pidfd](/peios/using-peios/services-and-jobs/service-types.md); a legacy binary that insists on double-forking must be wrapped at the package layer.

## The one operational invariant

peinit is **single-threaded PID 1**, and that one fact explains a surprising amount of its design. In a single-threaded PID 1, a blocking system call blocks *everything* — child reaping, watchdog expiry, shutdown signals, every other event stops while the call is stuck. So peinit's hard rule is that **its event loop never blocks on a userspace service.**

Two consequences you will meet repeatedly:

- **peinit operates on an in-memory snapshot of the registry, not a live view.** It reads all service definitions synchronously *once*, during boot, and thereafter works from an in-memory model that it updates from [change notifications](/peios/using-peios/registry-concepts/watches.md). Supervision never waits on the registry being available. This is why a configuration change does not always take effect immediately — see [Defining a service](/peios/using-peios/services-and-jobs/defining-a-service.md).
- **Anything that could block is pushed off the main loop.** Filesystem condition checks run in a short-lived forked helper rather than a `stat()` on the event loop; token requests to authd are driven by a timed state machine, not a blocking call; log delivery is best-effort and never exerts back-pressure on peinit.

You do not need to think about the event loop to operate peinit, but it is the reason behind several behaviours that would otherwise look arbitrary.

## Where peinit sits in the system

peinit is part of the [Trusted Computing Base](/peios/security-fundamentals/boot-and-trust-establishment/overview.md): a compromise of peinit is a compromise of the whole machine. It runs as [SYSTEM](/peios/security-fundamentals/identity/well-known-principals.md) with all privileges, and it never drops that identity. It depends on, and is depended on by, the other TCB components:

| peinit relies on | For |
|---|---|
| **KACS** | Service identity (installing tokens on children), control-interface authentication (peer token + AccessCheck), and per-service access control. See [Access decisions](/peios/security-fundamentals/access-decisions/overview.md). |
| **LCS / the registry** | Service definitions, boot configuration, and its own parameters. See [The registry](/peios/using-peios/registry-concepts/overview.md). |
| **registryd** | The registry *source* daemon — the first service peinit starts. peinit treats it as an opaque dependency. (`registryd` is an interface; the default implementation is `loregd` — see [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md).) |
| **authd** | Minting tokens for non-platform services. peinit requests; authd mints. |
| **eventd** | Service logs (forwarded over a socket) and job/operation/audit events (emitted through KMES). |
| **JFS** | Delivering ad-hoc job submissions — a service asking peinit to run a process on a client's behalf. |

The bootstrapping puzzle — authd needs the registry, the registry needs to be started by *something*, and that something is peinit running before any of them exist — is resolved by the boot model in [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md).

## Where to start

If you are configuring a system, start with **[Defining a service](/peios/using-peios/services-and-jobs/defining-a-service.md)** — what a service definition is, where it lives, and the rule that explains why some changes take effect immediately and others wait for a restart.

If you want to *operate* a running system — start, stop, query, and troubleshoot services — go to **[Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md)** for the command set and **[The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md)** to read what a status actually tells you.

If you care about *what runs as whom*, read **[Service identity and privileges](/peios/using-peios/services-and-jobs/identity-and-privileges.md)** and **[Who can manage a service](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md)** — the two independent halves of the security model.

If you are investigating boot behaviour, **[Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md)** covers Full, Safe, and Recovery modes and the escalation between them.

And when something is wrong, **[Troubleshooting peinit](/peios/using-peios/services-and-jobs/troubleshooting.md)** works backward from the symptom.

---

# Defining a service

_Peios / Using Peios / Services & jobs_

> A service is a registry key under Machine\System\Services\, made of typed values. The schema, and the mutability class that decides when a change lands.

A service is defined by a single key in the [registry](/peios/using-peios/registry-concepts/overview.md), under `Machine\System\Services\<name>`. The key's name *is* the service name, and the typed [values](/peios/using-peios/registry-concepts/keys-values-and-types.md) inside it are the definition — the binary, who it runs as, what it depends on, how it is supervised. There is no file under `/etc`; there is a registry key.

peinit reads these definitions in two situations: once at boot, to build the service graph, and on demand, when an administrator starts a service or runs `reload-config`. Between those reads it works from an in-memory copy. That last fact is the source of the most common "why didn't my change take effect?" question, and the second half of this page is devoted to it.

## Service names

A service name must be made of characters from `[A-Za-z0-9._-]` and be 1–128 bytes long. Anything else is a validation error. Two characters are pointedly excluded:

- **`/`** — names map directly onto cgroup ids and registry key names, and a slash would be ambiguous in both.
- **`:`** — reserved for peinit-internal synthetic names (for example, the way a hook job is labelled).

The name is how you refer to the service everywhere: in `peiosctl` commands, in another service's dependency list, in a [ServiceSecurity](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) descriptor, and in the [per-service SID](/peios/using-peios/services-and-jobs/identity-and-privileges.md) derived from it.

## The definition schema

A definition is a set of typed registry values. Rather than list all of them in one wall of rows, the tables below group the fields by what they are *for*, with a pointer to the page that explains each group in depth. Every field is optional unless noted; the only hard requirement is `ImagePath`. The full type-and-default catalog lives in the [Registry key reference](/peios/using-peios/services-and-jobs/registry-key-reference.md).

**What to run** — the binary and its execution context.

| Field | Default | Purpose |
|---|---|---|
| `ImagePath` *(required)* | — | Absolute path to the service binary. |
| `Arguments` | — | Argument list passed to the binary. |
| `WorkingDirectory` | `/` | Working directory for the process. |
| `Environment` | — | `KEY=VALUE` pairs added to the environment. |
| `RuntimeDirectories` | — | Private directories created under `/run` just before the process starts. See [The execution environment](/peios/using-peios/services-and-jobs/execution-environment.md). |
| `LimitNOFILE`, `LimitCORE` | — | `RLIMIT_NOFILE` / `RLIMIT_CORE`. |

**What kind of service** — type and readiness. See [Simple and Oneshot services](/peios/using-peios/services-and-jobs/service-types.md).

| Field | Default | Purpose |
|---|---|---|
| `Type` | Simple | `Simple` (long-running) or `Oneshot` (run-to-completion). |
| `Readiness` | Notify | `Notify` (`READY=1` via sd_notify) or `Alive` (ready when the process exists). Ignored for Oneshot. |
| `RemainAfterExit` | 0 | Oneshot only — stay in `Completed` after a successful exit. |
| `SuccessExitCodes` | — | Non-zero exit codes to treat as success. |

**When to start** — triggers and conditions. See [Triggers and timers](/peios/using-peios/services-and-jobs/triggers-and-timers.md).

| Field | Default | Purpose |
|---|---|---|
| `Triggers` | — | `boot` and/or `timer:<schedule>`. Absent = demand-only. |
| `Disabled` | 0 | If 1, triggers must not activate the service (manual start still allowed). |
| `SafeMode` | 0 | If 1, attempt to start in [Safe mode](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md). |
| `Conditions`, `Asserts` | — | Start-time checks. A failed condition *skips*; a failed assert *fails*. |

**Who it runs as** — identity and privileges. See [Service identity and privileges](/peios/using-peios/services-and-jobs/identity-and-privileges.md).

| Field | Default | Purpose |
|---|---|---|
| `Identity` | `LocalService` | Principal name or SID for the service token. |
| `RequiredPrivileges` | — | Privilege allow-list; everything else is stripped from the token. |
| `HookIdentity` | service's `Identity` | Identity for `ExecStartPre`/`ExecStartPost` hooks. |

**How it relates to other services** — dependencies. See [Dependencies and ordering](/peios/using-peios/services-and-jobs/dependencies.md).

| Field | Purpose |
|---|---|
| `Requires` | Hard dependencies — must be satisfied first; their failure fails this service. |
| `Wants` | Soft dependencies — started first if present, but optional. |
| `BindsTo` | Runtime coupling — if the target stops, this stops too. |
| `Conflicts` | Mutual exclusion — starting this stops the named services. |
| `OnFailure` | Service to start when this one enters `Failed`. |

**How it is kept alive** — supervision and health. See [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

| Field | Default | Purpose |
|---|---|---|
| `ErrorControl` | Normal | `Normal` (stay Failed) or `Critical` (sync + reboot on irrecoverable failure). |
| `RestartPolicy` | OnFailure | `Never` / `OnFailure` / `Always`. |
| `RestartMaxRetries`, `RestartWindow`, `RestartDelay` | 5 / 120 / 1 | Restart budget, the window of health that resets it, and the backoff base. |
| `HealthCheck`, `HealthCheckInterval`, `HealthCheckTimeout`, `HealthCheckRetries` | — / 30 / 5 / 3 | Active health-check command and its timing. |
| `WatchdogTimeout` | 0 | Expected interval between `WATCHDOG=1` pings; 0 disables. |

**The transition phases** — hooks and timeouts. See [The execution environment](/peios/using-peios/services-and-jobs/execution-environment.md) and [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md).

| Field | Default | Purpose |
|---|---|---|
| `ExecStartPre`, `ExecStartPost` | — | Commands run before the binary / after readiness. |
| `ExecReload` | (SIGHUP) | Reload command or `signal:<NAME>`. |
| `StartTimeout`, `StopTimeout` | 30 / 10 | Seconds for the whole start sequence / between SIGTERM and SIGKILL. |

**The remaining knobs** — scheduling, notify, fds, metadata.

| Field | Default | Purpose |
|---|---|---|
| `TimerPersistent`, `TimerJitter` | 1 / 0 | Catch up missed timer runs after reboot / random delay per firing. |
| `NotifyAccess` | Main | Who may send sd_notify messages (only `Main` is supported). |
| `FdStoreMax` | 0 | Size of the per-service fd store; 0 disables it. |
| `ServiceSecurity` | inherit | Security descriptor controlling who may manage the service. |
| `DisplayName`, `Description` | — | Human-readable labels for status output. |

### Forward compatibility

The schema version lives at `Machine\System\Services\SchemaVersion` (currently `1`). peinit is deliberately forward-compatible:

- **Unknown values are ignored.** A definition written for a newer peinit does not break an older one.
- **A newer schema version does not block boot.** peinit logs a warning and continues.
- **The schema only grows.** New capability arrives as new optional fields, never as a breaking change to an existing one.

One defensive rule cuts the other way: a *known* field must not appear more than once in a collected definition. A duplicated known field is a validation error, even though the registry would ordinarily give you at most one value per name.

## peinit works from a snapshot, not the live registry

Here is the idea that explains most surprises. peinit does **not** re-read the registry every time it touches a service. It reads definitions at well-defined moments and operates on an in-memory model in between.

Two layers of snapshotting stack on top of each other:

- **Boot generation.** At the start of boot, peinit reads *all* definitions, builds the dependency graph, and validates it. The whole boot runs against that one snapshot. Changes made *during* boot — by an install script, a post-hook — do not perturb the boot in progress.
- **Activation generation.** When peinit starts a specific service, it snapshots *that* service's definition for the entire start. Pre-exec hooks, the token request, the readiness timeout, the first health checks — all use the values captured at activation. Edit a field while the service is `Starting`, and the edit waits for the next start.

peinit learns about registry edits through [change notifications](/peios/using-peios/registry-concepts/watches.md): it subscribes to `Machine\System\Services\` and `Machine\System\Init\` at boot, and processes events in its event loop at a time of its choosing. If the notification queue overflows — a bulk admin operation, a flurry of scripted writes — peinit detects the overflow marker and does a full `reload-config` to resynchronise. You never have to think about the queue; you do have to know that *a change is picked up, not pushed*, and that when it takes *effect* depends on the field.

## Field mutability: when a change takes effect

Every field falls into one of four mutability classes. This table is the one to keep handy.

| Class | When a change takes effect | Fields |
|---|---|---|
| **Immutable at runtime** | Next **restart** only. | `ImagePath`, `Type`, `Identity`, `RequiredPrivileges`, `ErrorControl` |
| **Apply on next start** | Next **start** or explicit graph reload — not while running. | `Requires`, `Wants`, `BindsTo`, `Conflicts`, `OnFailure`, `Conditions`, `Asserts` |
| **Hot-reloaded** | Next relevant **event**, no restart. | `ServiceSecurity` (next control request) |
| **Reloadable at runtime** | Next relevant **operation** (restart, health-check cycle, …), no restart. | `Arguments`, `SuccessExitCodes`, all timeout/retry values, the health-check fields, `RestartPolicy`, `Environment`, `WorkingDirectory`, the hooks, `Readiness`, `NotifyAccess`, `LimitNOFILE`/`LimitCORE`, `FdStoreMax`, `SafeMode`, `DisplayName`, `Description` |

The practical reading: changing *what a process is or runs as* (`ImagePath`, `Identity`, `Type`, privileges, `ErrorControl`) is fundamental enough that it only applies when a fresh process starts — you must `restart`. Changing *policy that peinit consults each time it acts* (timeouts, restart behaviour, health checks) is picked up the next time peinit acts. And `ServiceSecurity` is special: it is re-read on every control request, so an access-control change takes effect on the very next command without touching the running service.

For inactive services the rules are simpler, because there is no running process to protect: a new service entry becomes available once the change notification is processed; a changed timer arms on the next evaluation; a changed dependency takes effect on the next start.

> [!NOTE]
> `reload-config` is the explicit, atomic way to make peinit re-read *everything*. It builds and validates a complete new graph snapshot and swaps it in only if validation passes; running services are not disturbed. It is covered with the rest of the command set in [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md).

## Removing a service definition

Deleting a definition from the registry does **not** kill a running instance. peinit learns of the removal through the same notification path and behaves according to whether the service is running:

- **Not running** (Inactive, Failed, Completed, Skipped, Abandoned): peinit discards the in-memory entry immediately. There is nothing to supervise.
- **Running** (Active, Starting, Reloading, Backoff, Stopping): the running process is a [job](/peios/using-peios/services-and-jobs/jobs-and-operations.md), and a job outlives its definition. peinit marks the entry **definition-removed**, keeps the cached definition only to finish supervising the existing instance, and does *not* restart it when it exits. Once it exits, the entry — and any stored fds — are discarded.

While an entry is definition-removed it keeps satisfying its dependents (it is still running), `stop` still works so you can drain it cleanly, but `start`, `restart`, and `reload` are rejected with `UNKNOWN_SERVICE` — there is no definition to start from. A `status` query reports it with a `definition_removed: true` flag so the draining instance is never invisible.

> [!IMPORTANT]
> Removing a definition is not how you stop a service. It stops *future management*, not work in progress. To actually stop a running service, `stop` it. To prevent it from auto-starting, set `Disabled=1` (see [Triggers and timers](/peios/using-peios/services-and-jobs/triggers-and-timers.md)). To prevent it from being started at all, deny `SERVICE_START` in its [ServiceSecurity](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) descriptor.

## Where to start

To understand the `Type` and `Readiness` fields and the Simple/Oneshot split, read [Simple and Oneshot services](/peios/using-peios/services-and-jobs/service-types.md).

To understand how a definition becomes a running, supervised process — and what each state in a `status` output means — read [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md).

For the complete type-and-default catalog of every registry key peinit reads, see the [Registry key reference](/peios/using-peios/services-and-jobs/registry-key-reference.md).

---

# Simple and Oneshot services

_Peios / Using Peios / Services & jobs_

> peinit has exactly two service types — long-running Simple and run-to-completion Oneshot — plus the readiness models that decide when each counts as started.

A service's **type** answers one question: does the process keep running, or does it run once and exit? peinit supports exactly two answers — **Simple** and **Oneshot** — set by the `Type` field (default Simple). The type is independent of *when* the service starts; a Oneshot can be boot-triggered, timer-triggered, or demand-only, exactly like a Simple service.

## Simple services

A **Simple** service is a long-running daemon. peinit forks, installs the [token](/peios/using-peios/services-and-jobs/identity-and-privileges.md), and execs the binary. The process *is* the service: while it runs, the service is running; when it exits, the service has stopped. This is the default and covers the large majority of services — `registryd`, `authd`, `sshd`, application daemons.

The interesting question for a Simple service is *when does it count as started* — the moment its [dependents](/peios/using-peios/services-and-jobs/dependencies.md) are allowed to begin. That is the **readiness** model, set by the `Readiness` field.

### Readiness: Notify vs Alive

| Readiness | Ready when… | Use for |
|---|---|---|
| **Notify** *(default)* | The service sends `READY=1` via [sd_notify](#the-sd-notify-contract). | Any service that has meaningful startup work — opening a socket, loading state, connecting to a backend. The dependent genuinely should wait. |
| **Alive** | The process exists — readiness is immediate. | Services with no startup handshake, where "the process is up" is as good a signal as you will get. |

The distinction matters because it is a *promise to dependents*. With **Notify**, a service that declares itself a `Requires` target is telling peinit "do not start anything that needs me until I say `READY=1`." With **Alive**, peinit can only promise that the process was forked — not that it is functional.

> [!WARNING]
> Using `Readiness=Alive` on a service that other services `Requires` is a common configuration smell. peinit will let it boot, but it logs a validation **warning**: Alive readiness gives no guarantee the service is actually serving before its dependents start. If a dependent connects on boot and gets connection-refused, this is usually why. Prefer Notify wherever the service can signal it.

Once ready, a Simple service transitions to **Active** and stays there until its process exits or it is stopped. What happens on exit — clean exit, crash, restart — is the subject of [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) and [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

## Oneshot services

A **Oneshot** service is a run-to-completion task. peinit forks, installs the token, execs the binary, and waits for it to exit. It is the right type for setup work: creating directories, initialising a database, running a schema migration, applying a one-time fixup at boot.

For a Oneshot, **readiness is exit, not a signal**. The `Readiness` field is ignored entirely — `READY=1` is meaningless for a process whose whole job is to finish. Success and failure are decided by the exit code:

- **Exit 0** (or any code listed in `SuccessExitCodes`) → the service **succeeded**.
- **Any other exit, or death by signal** → the service **failed**, and the [failure policy](/peios/using-peios/services-and-jobs/supervision.md) applies.

`SuccessExitCodes` is for binaries that signal a meaningful outcome with a non-zero code — for example, a tool that exits `2` to mean "nothing to do." Each entry is a decimal `0`–`255`; code `0` is always success and need not be listed.

### Completed, and RemainAfterExit

A successful Oneshot transitions to **Completed**, and what happens next depends on `RemainAfterExit`:

| `RemainAfterExit` | After a successful exit |
|---|---|
| **0** *(default)* | The service passes *through* `Completed` — releasing its dependents — then transitions to `Inactive`. |
| **1** | The service *stays* in `Completed`. |

Both forms satisfy dependents; the only difference is what a later `status` query shows. Use `RemainAfterExit=1` when "this task is done" is a state worth seeing — a boot-time migration you want to confirm ran, say — rather than a service that quietly returns to `Inactive`.

### Oneshot timing and hooks

Two details distinguish a Oneshot's start sequence from a Simple one:

- **`StartTimeout` covers the entire run.** For a Simple service the timeout covers startup until readiness; for a Oneshot it covers everything from the first pre-hook to process exit. A long-running Oneshot must raise `StartTimeout` accordingly (or send `EXTEND_TIMEOUT_USEC` as it works — see [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md)).
- **`ExecStartPost` runs only on success.** Post-hooks fire after the successful exit. If the Oneshot fails, `ExecStartPost` does not run.

### Restarting a Oneshot

A Oneshot is not restarted on *success*, regardless of `RestartPolicy` — even `Always`. A successful exit is the goal, not a failure to retry. `RestartPolicy` governs only the response to *failure* (a non-zero exit). To re-run a Oneshot on a schedule, give it a [timer trigger](/peios/using-peios/services-and-jobs/triggers-and-timers.md); that is the intended mechanism.

## Simple vs Oneshot at a glance

| | Simple | Oneshot |
|---|---|---|
| Process lifetime | Long-running; *is* the service | Runs once, exits |
| Readiness | `Notify` (`READY=1`) or `Alive` | Successful exit — `Readiness` ignored |
| Satisfies dependents when | Active | Completed (with or without `RemainAfterExit`) |
| Successful end state | Active (until stopped) | Completed, then Inactive (unless `RemainAfterExit=1`) |
| `ExecStartPost` fires | After readiness | After successful exit only |
| `StartTimeout` covers | Startup until readiness | The whole execution |
| Watchdog & health checks | Supervised while Active | None — `WatchdogTimeout`/`HealthCheck` are inert |
| Restart on success | n/a | Never (use a timer to re-run) |

## The sd_notify contract

A Notify-readiness service signals readiness by sending `READY=1` over the [sd_notify](/peios/using-peios/services-and-jobs/output-and-logging.md) protocol — a datagram to the socket named in the `NOTIFY_SOCKET` environment variable, which peinit sets for *every* service. `READY=1` is only the most visible message; the same channel carries watchdog keepalives (`WATCHDOG=1`), graceful-stop notice (`STOPPING=1`), timeout extensions (`EXTEND_TIMEOUT_USEC`), status strings (`STATUS=`), and the fd store. These are covered where they belong — readiness here, the rest in [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md) and [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md).

Readiness is always **per start generation**. A `READY=1` left over from a previous incarnation of the process is never valid for the current start — peinit ties every notify message to the specific process it is currently supervising, verified by [pidfd](#no-forking-daemons).

Who is allowed to send these messages is governed by `NotifyAccess`, which currently has a single value: `Main` (`0`, the default). Under `Main`, only the tracked main PID — the process peinit forked and supervises — is authorised to send sd_notify readiness and status messages; a message arriving from any other PID (a child, a helper, an unrelated process) is ignored. So if a worker your service spawns sends `READY=1`, peinit will not act on it — the readiness signal must come from the main process itself.

## No forking daemons

peinit does **not** support services that double-fork to background themselves. The whole reason a traditional daemon double-forks — to detach from the launcher — is moot when the launcher tracks the process it spawned. peinit obtains a **pidfd** for every process at fork time, which is a kernel handle to that exact process, immune to PID-reuse races. There is no `MAINPID=` mechanism for a service to redirect supervision onto a different process; peinit supervises what it forked.

If a legacy binary insists on daemonising, the fix lives at the [packaging](/pekit/recipes/packages.md) layer — wrap it so it stays in the foreground (commonly a `--no-daemon`/`--foreground` flag) — not at the init layer.

## Where to start

To control *when* either type of service starts, read [Triggers and timers](/peios/using-peios/services-and-jobs/triggers-and-timers.md).

To follow a service from definition to running process and read its states, read [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md).

To configure restart, health checks, and watchdogs for a Simple service, read [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

---

# Triggers and timers

_Peios / Using Peios / Services & jobs_

> Boot and timer triggers, the Disabled flag, and the full OnCalendar grammar — persistent catch-up, jitter, and what happens when the clock jumps.

A **trigger** decides *when* a service starts on its own. A service's triggers are independent of its [type](/peios/using-peios/services-and-jobs/service-types.md): a boot-triggered Oneshot and a timer-triggered Simple service are both perfectly ordinary. Triggers live in the `Triggers` field as a list, each entry written `type` or `type:argument`.

| Trigger | Form | Meaning |
|---|---|---|
| **Boot** | `boot` | Start during the Phase 2 [boot](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) sequence, in dependency order. |
| **Timer** | `timer:<schedule>` | Start on a schedule. The schedule is a [calendar expression](#calendar-expressions). |

A service with **no** triggers is **demand-only**: it never starts itself, and is brought up only by an explicit [control command](/peios/using-peios/services-and-jobs/controlling-services.md) or because another service [depends](/peios/using-peios/services-and-jobs/dependencies.md) on it. Many services are demand-only by design.

## Waiting for the boot to settle

`boot:settled` is `boot` with one difference: the service starts once the Phase 2 boot set has stopped moving — every service in it reached a state it will not leave on its own — rather than during it.

It exists for services that write to a terminal. peinit reports its progress to `/dev/console`, and a service holding that same terminal writes there too, so a prompt started mid-boot gets written over: `login` emits `Username: ` with no newline and waits, peinit appends a service line, and the result is unreadable until you press Enter and login redraws.

The wait is capped (`Machine\System\Boot\SettleTimeout`, 5 seconds by default). A service stuck in Starting therefore costs a deferred service a short delay rather than preventing it — which matters most exactly when something is broken and a console prompt is what you need.

A `boot:settled` service is deliberately **not** part of the boot plan. It does not consume the [parallel-start budget](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md), is not counted towards boot success, and cannot block another service. Waiting for quiet is a preference; it must not change what the boot means.

> [!NOTE]
> Reach for this only to keep console output legible. If a service genuinely needs something to be running first, that is a [dependency](/peios/using-peios/services-and-jobs/dependencies.md) — say so with `Requires`. A console login uses both: `boot:settled` for *when*, and `Requires` on its authority for *what must be true*.

You can list more than one trigger, including more than one of the same type. `["timer:*-*-* 02:00:00", "timer:*-*-* 14:00:00"]` runs at 2 am and 2 pm. The model is built to grow — future trigger types (path, device, event) will slot into the same list without a schema change.

## The Disabled flag

`Disabled=1` suppresses **automatic activation**. A disabled service will not be started by *any* trigger — boot, timer, or anything added later — but it can still be started by hand through the control interface. It is the switch for "configured, but not running on its own right now."

Two related controls are easy to confuse with it:

| To… | Use |
|---|---|
| Stop a service from auto-starting, but keep manual start | `Disabled=1` |
| Stop a service from being started *at all*, even manually | Deny `SERVICE_START` in its [ServiceSecurity](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) descriptor |
| Stop a *running* service | The [`stop` command](/peios/using-peios/services-and-jobs/controlling-services.md) |

Enabling and disabling are registry writes performed by admin tooling, not peinit commands. peinit picks up a `Disabled` change through a [registry notification](/peios/using-peios/registry-concepts/watches.md) or on `reload-config`. A disabled service's definition is still loaded into peinit's model, so it is ready for an on-demand start the instant you ask.

## Calendar expressions

Timer schedules are written in systemd's `OnCalendar` format. peinit's grammar is normative and self-contained — what follows is the whole language. The general shape is:

```
DayOfWeek Year-Month-Day Hour:Minute:Second Timezone
```

Every field is optional and has a default, so most real schedules are short.

| Field | Form | If omitted |
|---|---|---|
| DayOfWeek | weekday name(s) | any day (`*`) |
| Date | `Year-Month-Day` | `*-*-*` (any date) |
| Time | `Hour:Minute:Second` | `00:00:00` |
| Second | the `:Second` of Time | `:00` |
| Timezone | IANA zone name | system-local time |

So `02:00:00` is a complete expression — date defaults to *every day*, and it means "every day at 02:00." `Hour:Minute` with no seconds defaults the seconds to `00`.

### Component syntax

Each numeric component — year, month, day, hour, minute, second — and the weekday accept the same operators:

| Operator | Example | Matches |
|---|---|---|
| **Wildcard** | `*` | any value |
| **List** | `1,15` | any listed value |
| **Range** | `Mon..Fri`, `8..17` | the inclusive range |
| **Repetition** | `0/15` | `0`, then every multiple of the step — 0, 15, 30, 45 |
| **Range + step** | `8..17/2` | `8`, `10`, `12`, … up to and including `17` |

Weekdays are English names, case-insensitive, abbreviated (`Mon`) or full (`Monday`), and take lists and ranges.

### Last day of the month

A `~` in place of the `-` between Month and Day counts the day **from the end** of the month: `~01` is the last day, `~02` the second-to-last, and so on. So `*-*~01` is the last day of every month. Repetition combines with it — `Mon *-05~07/1` is "the last Monday in May."

### Named shortcuts

These stand in for the expression shown:

| Shortcut | Equivalent |
|---|---|
| `minutely` | `*-*-* *:*:00` |
| `hourly` | `*-*-* *:00:00` |
| `daily` | `*-*-* 00:00:00` |
| `weekly` | `Mon *-*-* 00:00:00` |
| `monthly` | `*-*-01 00:00:00` |
| `quarterly` | `*-01,04,07,10-01 00:00:00` |
| `semiannually` | `*-01,07-01 00:00:00` |
| `yearly` / `annually` | `*-01-01 00:00:00` |

### Timezones and precision

A timezone specifier follows the IANA database — `Europe/London`, `US/Eastern`, `UTC`. With none, the expression is interpreted in system-local time.

Precision is **second-level**. Unlike systemd, peinit does not accept sub-second fractions; a fractional second is a parse error. Service scheduling has no use for finer granularity.

### Examples

| Expression | Meaning |
|---|---|
| `*-*-* 02:00:00` | Every day at 2 am (system-local) |
| `Mon *-*-* 00:00:00` | Every Monday at midnight |
| `*-*-1,15 12:00:00` | 1st and 15th of each month at noon |
| `*-*~01 00:00:00` | Last day of each month at midnight |
| `Mon..Fri *-*-* 09:00:00` | Weekdays at 9 am |
| `*-*-* *:00/15:00` | Every 15 minutes |
| `*-*-* 02:00:00 Europe/London` | Every day at 2 am London time |

## What happens when a timer fires

A timer firing does not blindly start the service — peinit considers the service's current state first, so a firing never collides with a run already in progress.

| Type | Current state | Action |
|---|---|---|
| Oneshot | Inactive / Completed / Failed | Start it (operation source `Timer`). |
| Oneshot | Active / Starting | Set a single **pending** flag — one catch-up run when the current one finishes. |
| Simple | Inactive / Failed | Start it. |
| Simple | Active / Starting | No-op; the firing is logged. |

The Oneshot "pending" flag is deliberately not a queue: many firings that land while a Oneshot is busy collapse into *one* catch-up run, taken when the current run ends. A service cannot stampede itself.

That single flag is shared across *all* of a Oneshot's timers, not one per timer. If a Oneshot has several timer triggers and two of them fire while it is busy, at most **one** run is still pending when the current one finishes. The timers are otherwise fully independent — each keeps its own next-firing time and its own last-run timestamp — but they can only ever queue up a single shared catch-up between them.

Each timer firing creates a normal [start operation](/peios/using-peios/services-and-jobs/jobs-and-operations.md), so a timer-started service is observable exactly like an admin-started one — same GUIDs, same events.

## Persistent timers and catch-up

`TimerPersistent` (default `1`) controls whether runs **missed while the machine was off** are caught up after a reboot.

peinit records the last-run timestamp of each timer in the registry. On boot, for each persistent timer it reads that timestamp, computes the next firing after it, and — if that time is already in the past — **fires once, immediately**, then resumes the normal schedule.

The catch-up is always a *single* run, never one-per-missed-occurrence. A daily timer that was off for five days fires once on the next boot, not five times. A non-persistent timer (`TimerPersistent=0`) ignores history entirely and simply computes its next future firing from now.

One subtlety worth knowing: the last-run timestamp is keyed by the **schedule string**. If you change a timer's schedule, the old timestamp is orphaned and the new schedule has no history, which causes exactly one spurious catch-up run. Schedule changes are rare and the cost is one extra run, so peinit accepts it rather than tracking trigger identity.

> [!NOTE]
> The timestamp is written *when the timer fires and the start is initiated*, not when the run completes. A service that crashes mid-run is not re-triggered on the next boot — the run was attempted, not missed.

## Jitter

`TimerJitter` (default `0`) adds a random delay of 0 to `TimerJitter` seconds to each firing, recomputed every time. A daily timer with `TimerJitter=900` fires at a slightly different moment between 00:00 and 00:15 each day. Jitter only ever delays — a timer never fires *before* its scheduled time. It is the standard remedy for a fleet of machines all hammering the same backend at exactly midnight.

## When the clock jumps

Calendar timers are **wall-clock** schedules: `*-*-* 02:00:00` means "02:00 on the wall," not "every 86,400 seconds." peinit arms them as absolute real-time timers that are cancelled and recomputed whenever the system clock is stepped, so the schedule stays anchored to the wall clock across corrections.

| Event | Behaviour |
|---|---|
| **NTP step or manual clock set** | Armed calendar timers are cancelled and recomputed against the new wall clock. A backward step pushes the next firing later; a forward step that crosses a missed time fires it once. |
| **Suspend / resume** | A scheduled time that elapsed while suspended fires once on resume — once, not once per missed occurrence. |
| **A missed occurrence within one uptime** | Fire once, then resume the normal schedule. peinit never replays every occurrence in the gap. |
| **Daylight-saving transition** (timezone-aware timer only) | A scheduled time in the *skipped* hour never fires; a scheduled time in the *repeated* hour fires once. See below. |

Daylight-saving is the case most likely to surprise you, and it only affects timers that name an IANA zone that observes DST (a bare `02:30:00` in system-local time follows whatever the system does). Say you schedule `*-*-* 02:30:00 Europe/London`:

- **Spring forward** — the clock jumps from 01:59 straight to 03:00, so 02:30 *does not exist* on that day. The timer **does not fire**; it simply resumes at the next valid `02:30`.
- **Fall back** — the clock reaches 02:30, then rewinds and reaches 02:30 a second time. The timer fires **exactly once**, on the first occurrence, not twice.

This is distinct from peinit's *interval* timers — the watchdog, health-check intervals, restart backoff, and the start/stop/reload phase timeouts. Those are genuine durations measured on the monotonic clock, so "wait 30 seconds" means 30 elapsed seconds and never lurches when the wall clock is set.

There is one boot-time case worth calling out on its own. When peinit reads a timer's stored last-run timestamp at boot and finds it is *in the future* relative to the current wall clock — meaning the clock has gone backwards since that run — it can't trust the timestamp, so it treats the timer as having an **unknown last run** and fires the persistent catch-up immediately. This only applies to the boot-time check; the runtime handling above is unaffected.

> [!CAUTION]
> If a machine boots with a wildly wrong clock (a dead RTC battery) and NTP corrects it only later, the boot-time persistent catch-up decision is made against the wrong time and may fire spuriously or not at all. The *runtime* half self-heals once NTP steps the clock, but the boot-time decision is already made by then. This is a known edge case short of NTP-aware rescheduling — worth knowing if you run timers on hardware with unreliable clocks.

## Where to start

To see how a timer-started run appears as a job and an operation, read [Jobs and operations](/peios/using-peios/services-and-jobs/jobs-and-operations.md).

To understand the states a timer-started service moves through, read [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md).

For the exact registry keys that store last-run timestamps, see the [Registry key reference](/peios/using-peios/services-and-jobs/registry-key-reference.md).

---

# The service lifecycle

_Peios / Using Peios / Services & jobs_

> The ten service states, the transitions that connect them, and the cause taxonomy that says why a service ended up where it is.

A service managed by peinit is, at every instant, in **exactly one state**. The state is what a `status` query reports, what gates dependents, and what decides which commands are valid. Alongside the state, peinit records the **cause** of the most recent transition — the *why* behind the *where*. Reading a status is reading these two things together: "Failed, because RestartBudgetExhausted" tells a very different story from "Failed, because ValidationError."

This page is the reference for both. It is worth internalising before [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md) and [Troubleshooting](/peios/using-peios/services-and-jobs/troubleshooting.md), because both lean on it.

## The states

| State | Process? | Satisfies dependents? | What it means when you see it |
|---|---|---|---|
| **Inactive** | No | No | Not started, or stopped cleanly and not set to restart. The neutral resting state. |
| **Starting** | Maybe | No | Activation in progress — conditions, hooks, fork, or readiness wait. Dependents are blocked. |
| **Active** | Yes | **Yes** | Running and ready. The normal state of a healthy Simple service. |
| **Reloading** | Yes | **Yes** | Re-reading configuration. Still counts as running. |
| **Stopping** | Yes (briefly) | No | SIGTERM sent; waiting for exit or SIGKILL escalation. |
| **Completed** | No | **Yes** | Oneshot finished successfully. Stays here only with `RemainAfterExit=1`. |
| **Backoff** | No | No | A restart is pending; the service is waiting out its backoff delay before the next attempt. |
| **Failed** | No | No | Exited abnormally and restart policy is exhausted or not configured. |
| **Abandoned** | Yes (unkillable) | No | SIGKILL was sent but the process survived in uninterruptible sleep (D-state). peinit has given up supervising it; its cgroup is leaked. |
| **Skipped** | No | **Yes** | A start-time [condition](/peios/using-peios/services-and-jobs/execution-environment.md) was not met. The service does not apply here. |

Three of these — **Active**, **Completed**, **Skipped** — satisfy dependents. Everything else blocks them. That single column is the rule the whole [dependency](/peios/using-peios/services-and-jobs/dependencies.md) system turns on: a service waiting on a `Requires` target does not move until that target reaches one of those three.

## The common path

Most of a service's life is a small loop. The diagram below shows the states a healthy Simple service moves through, plus the two ways it leaves Active.

```mermaid
stateDiagram-v2
    [*] --> Inactive
    Inactive --> Starting: start / trigger / dependency
    Starting --> Active: ready (READY=1 or alive)
    Active --> Stopping: stop / shutdown / conflict
    Stopping --> Inactive: exited (clean stop / shutdown)
    Stopping --> Failed: exited (conflict / BindsTo eviction)
    Active --> Backoff: crash + restart allowed
    Backoff --> Starting: backoff delay elapsed
    Active --> Failed: crash + no restart
    Starting --> Failed: timeout / hook / setup failure
    Failed --> Starting: explicit start
```

A few things this picture makes concrete:

- An automatic restart always routes **through Backoff**, never through Failed. `Backoff → Starting` is the retry; `Failed` is reached only when restarts are exhausted or disabled. This is why [`OnFailure`](/peios/using-peios/services-and-jobs/supervision.md) fires once at the end, not on every retry.
- `Starting` can fail *before any process exists* — a parent-side setup error, a failed pre-hook, a condition or assert. The cause records which.
- `Failed → Starting` is how a manual `start` (or a recovery path) revives a dead service; automatic restarts never originate from `Failed`.
- `Stopping` has **two** exits. A clean stop or a shutdown lands in `Inactive`. But a service stopped because it lost a [`Conflicts`](/peios/using-peios/services-and-jobs/dependencies.md) race (`ConflictEviction`) or because its [`BindsTo`](/peios/using-peios/services-and-jobs/dependencies.md) target went away (`BindsToPropagation`) comes to rest in `Failed`, carrying that cause — so an evicted or bound-out service shows as Failed in `status` and needs a `reset` (or, for a bound service, its target returning) before it starts again. It was not shut down on purpose, so peinit does not treat it as cleanly Inactive.

Oneshot services follow a parallel path through **Completed** instead of Active: `Starting → Completed`, and then either staying there (`RemainAfterExit=1`) or passing through to `Inactive`. See [Simple and Oneshot services](/peios/using-peios/services-and-jobs/service-types.md).

> [!NOTE]
> **A crash while `Reloading` is still a crash.** If a service's main process *dies* while it is re-reading its configuration, peinit treats that as an ordinary `ProcessCrash` and routes it through the [restart policy](/peios/using-peios/services-and-jobs/supervision.md) — `Reloading → Backoff` if a restart is warranted, or `Reloading → Failed` if it is not. This is why you can see a service jump straight from Reloading into Backoff or Failed. It is *distinct* from an `ExecReload` **command** failing: a failed reload command leaves the running process untouched and the service stays `Active`, reporting the failure without changing state.

## Transition causes

Every transition carries a cause. peinit keeps the cause of the *most recent* transition on the service, and emits all of them to [eventd](/peios/security-fundamentals/auditing/overview.md). The full taxonomy, grouped by what kind of thing happened:

**Why a service started**

| Cause | Meaning |
|---|---|
| `ExplicitStart` | An administrator or a trigger started it. |
| `DependencyStart` | Started to satisfy another service's dependency. |
| `RestartPolicy` | An automatic restart, after a backoff delay. |
| `BindsToRecovery` | A [bound](/peios/using-peios/services-and-jobs/dependencies.md) target returned to Active; the dependent is auto-restarted. Does **not** consume the restart budget. |

**Why a service stopped**

| Cause | Meaning |
|---|---|
| `ExplicitStop` | An administrator requested stop. |
| `ConflictEviction` | A [conflicting](/peios/using-peios/services-and-jobs/dependencies.md) service started and won. |
| `BindsToPropagation` | A bound target stopped, so this one stops too. |
| `ShutdownWave` | The system is shutting down. |

**Why a service failed or restarted** — the diagnostic causes

| Cause | Meaning |
|---|---|
| `ProcessCrash` | The main process exited non-zero or died on a signal. |
| `CleanExit` | A Simple process exited *successfully*, and policy is not Always — not a crash; goes straight to Inactive. |
| `CleanExitRestart` | A Simple process exited successfully but `RestartPolicy=Always`, so it is restarted. Logged clearly as *not* a crash. |
| `ReadinessTimeout` | `StartTimeout` expired before the service became ready. |
| `WatchdogTimeout` | A `WATCHDOG=1` keepalive did not arrive in time. |
| `HealthCheckFailure` | `HealthCheckRetries` consecutive health checks failed. |
| `PreHookFailure` | An `ExecStartPre` hook exited non-zero. |
| `ParentSetupFailure` | peinit could not even fork — resource exhaustion, cgroup error. No child was created. |
| `PreExecFailure` | Setup after fork but before exec failed (token install, rlimits, environment). |
| `RestartBudgetExhausted` | `RestartMaxRetries` reached within the window; no more retries. |

**Why a service was rejected** — definition and graph problems

| Cause | Meaning |
|---|---|
| `ValidationError` | The definition failed validation (bad field, unresolvable conflict, illegal health-check timing…). |
| `CycleDetected` | The service is part of a dependency cycle. |
| `DependencyFailure` | A `Requires` dependency entered Failed or does not exist. |
| `AssertionError` | A start-time `Assert` failed. |
| `ConditionSkipped` | A start-time `Condition` was not met → Skipped (this is *not* a failure). |
| `ProcessUnkillable` | The process survived SIGKILL (D-state) → Abandoned. |
| `ExplicitReset` | An administrator cleared Failed/Abandoned/Skipped without starting. |

The grouping matters for what peinit does next: the diagnostic causes are mostly **restart-eligible** (peinit consults [restart policy](/peios/using-peios/services-and-jobs/supervision.md)), whereas the definition/graph causes are **never restarted** — retrying a broken definition cannot help. The distinction is spelled out in [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

> [!TIP]
> When a service is `Failed`, the cause is the first thing to read. A `ProcessCrash` or `WatchdogTimeout` points at the service's own code or health; a `DependencyFailure` points at something it needs; a `ValidationError` points at its definition. [Troubleshooting peinit](/peios/using-peios/services-and-jobs/troubleshooting.md) is organised around exactly this lookup.

## Readiness gating during boot

The dependent-satisfaction rule is what makes boot orderly. A service's dependents do not start until it satisfies them:

- A **Simple** service satisfies dependents when it signals `READY=1` (Notify) or when its process exists (Alive).
- A **Oneshot** service satisfies dependents when it exits successfully and reaches Completed.
- A **Skipped** service satisfies dependents immediately — it succeeded by not needing to run.

If a service does not reach readiness within its `StartTimeout`, its dependents diverge by relationship: `Requires` dependents transition to Failed (`DependencyFailure`); `Wants` dependents start anyway. That difference is the whole point of the two relationship types — see [Dependencies and ordering](/peios/using-peios/services-and-jobs/dependencies.md).

## The Abandoned state

`Abandoned` is the one state that reflects a kernel-level problem rather than a service-level one. peinit reaches it when it has sent SIGKILL to a service's process group but the processes are still there after a grace period — the post-kill timeout, **5 seconds** by default. If the service's cgroup has not emptied within it, the processes are wedged in **uninterruptible kernel sleep** (D-state), typically behind a hung mount or a broken storage controller, and the service is marked Abandoned (its cgroup leaked).

peinit cannot kill a D-state process; nothing in userspace can. So it stops trying: it marks the service Abandoned, leaks the cgroup (it cannot be removed while populated), and moves on rather than hanging. The leak is never silent — it shows up in the service's `warnings` and, on a later start, peinit creates a fresh "generational" cgroup so the new instance is unaffected by the stuck old one.

An Abandoned service is cleared with `reset`, which re-checks the cgroup: if it finally emptied, peinit cleans up; if it is still populated, peinit leaves it leaked and warns you. An Abandoned service is a sign of an underlying I/O fault that needs investigating, not something to paper over with a restart.

## Invariants

A handful of rules hold without exception:

1. A service is in **exactly one** state at any moment.
2. Only the transitions peinit defines are valid — there are no others.
3. **Only peinit** changes a service's runtime state. No external process can reach in and set it.
4. A service's *securable identity* (its [ServiceSecurity](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) descriptor — who can manage it) is independent of its *process identity* (its [token](/peios/using-peios/services-and-jobs/identity-and-privileges.md) — what it can access).
5. Readiness is **per start generation** — a `READY=1` from a previous incarnation is never honoured for the current start.

## Where to start

To understand what happens *after* a service crashes — restart policy, backoff, health checks, watchdogs — read [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

To understand how dependents block on and are released by these states, read [Dependencies and ordering](/peios/using-peios/services-and-jobs/dependencies.md).

To see which commands are valid in which state, read the command × state matrix in [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md).

---

# Keeping services running

_Peios / Using Peios / Services & jobs_

> Restart policy with backoff and a budget, active health checks, the watchdog, timeout extension, OnFailure, and the Critical reboot path.

Once a service is [Active](/peios/using-peios/services-and-jobs/the-service-lifecycle.md), peinit's job becomes *keeping it that way* — or deciding, deliberately, when to stop trying. That decision is governed by a small set of policies: a restart policy with a throttling budget, optional active health checks, an optional watchdog, and an error-control level that decides how serious a final failure is. This page covers all of them.

## Restart policy

When a service fails in a way that *might* be transient, peinit consults `RestartPolicy`:

| Policy | Value | Behaviour |
|---|---|---|
| **Never** | 0 | Never restart. The service goes straight to Failed. |
| **OnFailure** *(default)* | 1 | Restart on a crash or runtime failure. An exit code in `SuccessExitCodes` is *not* a failure and is not restarted. |
| **Always** | 2 | Restart on any failure, *and* — for a Simple service — restart even a successful clean exit (see [clean exits](#clean-exits)). |

Not every failure is restart-eligible. peinit splits causes into three groups:

- **Restart-eligible** — peinit consults the policy and budget: `ProcessCrash`, `WatchdogTimeout`, `HealthCheckFailure`, `ReadinessTimeout`, `PreHookFailure`, `PreExecFailure`, `ParentSetupFailure`. (The startup failures are eligible on purpose — a pre-hook that failed on a momentarily-missing mount deserves another try.)
- **Never restarted** — retrying cannot help: an explicit stop or reset, shutdown, conflict eviction, a broken definition (`ValidationError`, `CycleDetected`, `DependencyFailure`, `AssertionError`), an already-exhausted budget, or an unkillable process.
- **Budget-exempt** — `BindsToRecovery`, where a [bound](/peios/using-peios/services-and-jobs/dependencies.md) service came back and its dependents are revived. The dependent did not fail on its own, so this restart does not count against its budget.

## The budget and backoff

Restarting an instantly-crashing service in a tight loop is worse than useless, so every restart is throttled by two mechanisms working together.

**Exponential backoff.** Before each restart the service sits in the [Backoff](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) state for a delay that *doubles* on each consecutive failure, starting from `RestartDelay` (default 1 s) and capped at 60 s. So a crash-looping service backs off 1 s, 2 s, 4 s, 8 s, … up to a minute between attempts.

**The budget.** A consecutive-failure counter tracks how many times the service has failed in a row. Once it reaches `RestartMaxRetries` (default 5), the *next* failure is not restarted: the service transitions to Failed with cause `RestartBudgetExhausted`.

**The reset.** The counter resets to zero once the service stays Active for `RestartWindow` seconds (default 120) without failing. This is the crucial detail: the budget is **not** "N restarts ever" — it is "N restarts faster than the service can sustain `RestartWindow` of health." A service that crashes once a day and recovers cleanly each time never exhausts its budget, because every crash starts from a counter of zero. Only failures recurring faster than the service can stay healthy accumulate.

```mermaid
flowchart LR
    A["Active"] -->|crash| B{"budget left?"}
    B -->|yes| C["Backoff<br/>delay doubles"]
    C -->|delay elapsed| D["Starting"]
    D -->|ready, stays Active<br/>RestartWindow| A
    B -->|no| E["Failed<br/>RestartBudgetExhausted"]
```

> [!TIP]
> If a service flaps — restarting endlessly without ever sticking — and never hits its budget, the usual cause is that it briefly reaches Active each time and resets the counter. Either it is recovering long enough to reset (raise `RestartWindow`) or you are looking at a [health-check flap](#health-checks), which has its own guardrail below.

### Clean exits

For a **Simple** service, exiting with code 0 (or a `SuccessExitCodes` match) is *success*, not a crash — a daemon chose to quit. What peinit does next depends on the policy:

- Under `Never` or `OnFailure`, the cause is `CleanExit` and the service goes straight to **Inactive**, with no restart-policy consultation at all.
- Under `Always`, the cause is `CleanExitRestart` and the service is restarted — but through the *same* backoff and budget as a failure, so a daemon that exits cleanly in a tight loop cannot bypass throttling. Logs and status make clear it exited successfully and was restarted only because the policy is Always.

(A **Oneshot** clean exit is never restart-eligible regardless of policy — see [service types](/peios/using-peios/services-and-jobs/service-types.md).)

## Health checks

A live process is not always a working one — it can lose its database connection, wedge in a bad state, or return errors while still "up." An active **health check** closes that gap by running a command periodically and treating its exit code as a verdict: 0 is healthy, non-zero is a failure.

Health checks apply to **Simple** services only. peinit starts a service's health-check timer once the service becomes Active; a [Oneshot](/peios/using-peios/services-and-jobs/service-types.md) has no long-running process to probe, so it never has one. `HealthCheck` and its companion fields are inert on a Oneshot — no timer is started — so don't set them there expecting supervision.

| Field | Default | Controls |
|---|---|---|
| `HealthCheck` | — | The command to run (runs under the service's own [token](/peios/using-peios/services-and-jobs/identity-and-privileges.md)). |
| `HealthCheckInterval` | 30 | Seconds between runs. |
| `HealthCheckTimeout` | 5 | Seconds before a check is killed and counted as failed. |
| `HealthCheckRetries` | 3 | Consecutive failures before the service is declared unhealthy. |

`HealthCheckRetries` consecutive failures mark the service unhealthy and restart it through the normal restart policy — backoff and budget included. A single success resets the failure count. If a check is still running when the next interval fires, the new one is skipped rather than stacked.

The `health` field in a `status` response reflects this: `healthy`, `unhealthy` (failing but not yet failed out), `unknown` (configured, no result yet), or `null` (no health check).

**The flap guardrail.** Health-check throttling only works if a failure cycle is shorter than the restart window — otherwise the counter resets between failures and the service restarts forever. peinit enforces this at validation time as a hard rule:

```
HealthCheckRetries × HealthCheckInterval < RestartWindow
```

A definition that violates it is a **validation error**, not a warning — the service is marked Failed with `ValidationError` rather than allowed to restart indefinitely.

> [!CAUTION]
> Be extremely cautious with health checks on `ErrorControl=Critical` services. A false-positive health-check failure on a Critical service will, after exhausting the restart budget, **reboot the machine** — and a flaky check can do this repeatedly. Critical platform services are better served by a passive [watchdog](#the-watchdog) with a generous timeout: the service itself knows whether it is healthy and simply stops sending keepalives if not. peinit applies the same health-check semantics to Critical services as to any other — there is no special-casing to save you.

A health check stuck in D-state is handled like any other stuck helper: its sub-cgroup is leaked and surfaced as a warning, but — unlike a stuck *main* process — it does **not** push the service to Abandoned. A health check holds no service resources; it is only a probe.

## The watchdog

The watchdog inverts the health check: instead of peinit asking the service if it is alive, the service must periodically tell peinit. Set `WatchdogTimeout` (seconds; 0 disables) and the service must send `WATCHDOG=1` via [sd_notify](/peios/using-peios/services-and-jobs/output-and-logging.md) at least that often. Miss the deadline and peinit treats it as a failure (cause `WatchdogTimeout`) and applies the restart policy.

Like health checks, the watchdog is a **Simple**-only supervision mechanism. peinit arms the watchdog timer only once a Simple service becomes Active; on a [Oneshot](/peios/using-peios/services-and-jobs/service-types.md) `WatchdogTimeout` is inert — no timer is started, and setting it changes nothing.

A service can adjust its own watchdog at runtime by sending `WATCHDOG_USEC=<microseconds>`: a positive value re-arms the timer with the new interval immediately; `0` disables the watchdog. This is for services whose phases have different latencies — a database might run a tight 5 s watchdog normally but widen it to 60 s during a compaction pass, because it knows its own phases better than the schema author did. The runtime value does **not** persist: a restart reverts to the schema's `WatchdogTimeout`.

## Timeout extension

A service that does genuinely slow work during a transition can ask for more time by sending `EXTEND_TIMEOUT_USEC=<microseconds>` while it is Starting, Stopping, or Reloading. Each message *replaces* the current phase deadline (it is not additive); a service that keeps sending them keeps proving it is making progress, and if it stops, the last deadline fires and peinit escalates normally.

The extension is capped to prevent a buggy service from extending forever:

| Phase | Base timeout | Maximum deadline |
|---|---|---|
| Starting | `StartTimeout` | `StartTimeout` × 4 |
| Stopping | `StopTimeout` | `StopTimeout` × 4 |
| Reloading | `StartTimeout` | `StartTimeout` × 4 |

During [shutdown](/peios/using-peios/services-and-jobs/shutdown.md) a second, global cap also applies — the deadline can never exceed the remaining time in the global shutdown timeout, and the stricter of the two caps wins. A message received outside a transition (the service is Active, Failed, …) is ignored — there is no timeout to extend.

## OnFailure

When a service enters **Failed**, an `OnFailure` field names another service to start in response — a fallback for *graceful degradation* (the main web UI failed; bring up a minimal emergency endpoint). It is not a monitoring or alerting hook; that is [eventd](/peios/security-fundamentals/auditing/overview.md)'s job.

`OnFailure` fires for runtime failures — `ProcessCrash`, `WatchdogTimeout`, `HealthCheckFailure`, the startup-failure causes, and non-Critical budget exhaustion. It deliberately does **not** fire for:

- **`ShutdownWave`** — nothing new starts during shutdown.
- **`ValidationError`, `CycleDetected`, `DependencyFailure`, `AssertionError`** — these are definition or graph breakage, not a running service degrading. The fallback would likely sit in the same broken graph anyway.

Because a fallback can itself fail and carry its own `OnFailure`, peinit bounds the chain: it tracks which services it has already started for one originating failure, refuses to start one twice, and never follows the chain beyond a depth of 16. When the guard trips, it logs the loop and stops.

## ErrorControl: when failure is not an option

`ErrorControl` decides how serious an *irrecoverable* failure is:

| Level | Value | On irrecoverable failure |
|---|---|---|
| **Normal** *(default)* | 0 | The service stays in Failed. The rest of the system carries on. |
| **Critical** | 1 | peinit **syncs filesystems and reboots immediately.** |

A Critical service is one the system genuinely cannot run without — the platform daemons `registryd`, `authd`, `lpsd`, `eventd` are all Critical. "Irrecoverable" means the restart budget was exhausted: peinit tried, backed off, retried up to `RestartMaxRetries`, and the service still would not stay healthy. At that point the fastest path to a known-good system is a reboot.

A few consequences worth holding onto:

- **Critical failure outranks `OnFailure`.** When a Critical service exhausts its budget, peinit reboots; it does *not* start the `OnFailure` handler.
- **`ErrorControl=Critical` implies `SafeMode`.** A Critical service is always eligible to start in [Safe mode](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md).
- **Critical services are OOM-protected.** peinit sets their `oom_score_adj` to `-1000`, so the kernel will not pick them as out-of-memory victims.
- **A runtime Critical crash does not enter Safe mode** — it follows the reboot path. Safe mode is only for boot-time *configuration* errors (a cycle or conflict involving a Critical service). The escalation between reboots, the boot-attempt counter, and Recovery mode are covered in [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md).

> [!WARNING]
> A Critical service that fails the same way on every boot becomes a **reboot loop**, which the boot-attempt counter is designed to break by escalating to Recovery mode after a few attempts. If you see a machine rebooting repeatedly, a crashing Critical service is the first suspect. See [Troubleshooting peinit](/peios/using-peios/services-and-jobs/troubleshooting.md).

## Where to start

To see exactly which states these policies move a service through, read [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md).

To understand how a failed service propagates to the ones that depend on it, read [Dependencies and ordering](/peios/using-peios/services-and-jobs/dependencies.md).

When a service will not stay up, work backward from the symptom in [Troubleshooting peinit](/peios/using-peios/services-and-jobs/troubleshooting.md).

---

# Dependencies and ordering

_Peios / Using Peios / Services & jobs_

> Requires, Wants, BindsTo, and Conflicts — start order, stop order, failure propagation, graph validation, and parallel start.

Services rarely stand alone. A web app needs its database; a network daemon needs the loopback interface up; two implementations of the same role must never run at once. peinit expresses all of this with four relationship fields — `Requires`, `Wants`, `BindsTo`, and `Conflicts` — that together decide the order services start in, what happens when one stops, and how a failure spreads to its neighbours.

The relationships are declared on the *dependent* (the service that needs something), naming the *target* (the service it needs) by name. `Conflicts` is the exception — it is symmetric, and one side declaring it is enough.

## The four relationships

### Requires — a hard dependency

If A `Requires` B:

- **Start.** B must reach a [satisfying state](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) before A starts. If B is not running, peinit starts it first (cause `DependencyStart`). If B fails, A is marked Failed with `DependencyFailure` and never even attempts to start.
- **Stop.** Stopping B does **not** stop A. Requires is a *start-ordering* constraint, not a runtime leash — A keeps running.
- **Runtime failure.** If B crashes while A is already Active, A is unaffected. B's own [restart policy](/peios/using-peios/services-and-jobs/supervision.md) handles B's recovery.

Requires is the workhorse: "I need this to have started, and if it can't, neither can I."

### Wants — a soft dependency

If A `Wants` B:

- **Start.** peinit starts B before A *if* B exists and is not [Disabled](/peios/using-peios/services-and-jobs/triggers-and-timers.md) — but if B fails to start, or does not exist, **A starts anyway.**
- **Stop / failure.** No effect either way. A and B are runtime-independent.

Wants is best-effort ordering: "start this first if you can, but I work without it."

### BindsTo — a runtime coupling

If A `BindsTo` B:

- **Start.** Identical to Requires — B must satisfy before A starts.
- **Stop.** If B stops for *any* reason — explicit stop, conflict, crash, shutdown — A is stopped too, transitioning to Stopping with cause `BindsToPropagation`. When A finishes stopping it comes to rest in **Failed** (carrying `BindsToPropagation`), *not* Inactive — so a `status` query shows the bound service as Failed, making it clear it was taken down by its target rather than shut down cleanly.
- **Recovery.** When B returns to Active, peinit **automatically restarts** A from that Failed state (cause `BindsToRecovery`). This is reactive — peinit watches B's transitions — and it is **budget-exempt**: A did not fail on its own, so the restart does not count against its [budget](/peios/using-peios/services-and-jobs/supervision.md). If B never comes back, A stays Failed until you `reset` (or start) it.

BindsTo is Requires plus a runtime leash: "I need this to start, *and* I should not outlive it." It is the right choice for a sidecar that is meaningless without its principal. `BindsTo` implies `Requires`; listing both for the same target is harmless, and BindsTo semantics win.

### Conflicts — mutual exclusion

If A `Conflicts` with B:

- **Start.** Starting A while B is Active creates a stop operation for B (source `ConflictResolution`), evicting it (cause `ConflictEviction`) before A starts — and vice versa. If the loser will not stop within its `StopTimeout`, [SIGKILL escalation](/peios/using-peios/services-and-jobs/shutdown.md) applies. The evicted loser does **not** land in Inactive: it comes to rest in **Failed** (carrying `ConflictEviction`), so it shows as Failed in `status` and needs a `reset` before it will start again. That is deliberate — an evicted service is not a clean stop, and leaving it Failed stops it from quietly restarting straight back into the conflict.
- **Symmetry.** Conflicts is two-way. If A declares `Conflicts=["B"]`, starting *either* stops the other; B need not declare it back.

Conflicts is for true mutual exclusion — two services binding the same port, or two implementations of one role where exactly one must run — not for ordinary resource contention.

### At a glance

| | Start order | Target failure stops dependent? | Target stop stops dependent? |
|---|---|---|---|
| **Requires** | target first; dependent fails if target fails | At start time only | No |
| **Wants** | target first if present; dependent starts regardless | No | No |
| **BindsTo** | target first; dependent fails if target fails | Yes (and auto-recovers) | Yes (and auto-recovers) |
| **Conflicts** | starting one evicts the other | n/a | n/a |

## Graph validation

Before peinit starts *anything*, it builds the dependency graph and validates it. Validation runs once per graph build — at boot for the whole boot graph, and per request for an [on-demand start](#on-demand-starts)'s transitive closure. It is not incremental.

**Cycles.** peinit topologically sorts the graph; if the sort fails, there is a cycle. Every service in the cycle is marked Failed with `CycleDetected`, and peinit logs the full path (`dependency cycle: A → B → C → A`) so you can find and break it. A service may not depend on itself — a self-reference is rejected as a cycle. If any service in the cycle is `ErrorControl=Critical`, peinit downgrades to [Safe mode](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) rather than rebooting, because a reboot would just hit the same cycle.

**Missing targets.** What a missing target means depends on the relationship:

| Relationship | Missing or disabled target |
|---|---|
| `Requires` | Dependent marked Failed (`DependencyFailure`). |
| `BindsTo` | Treated as a missing Requires — Failed (`DependencyFailure`). |
| `Wants` | Silently ignored — the entry is dropped. |
| `Conflicts` | Silently ignored — nothing to conflict with. |

**Unresolvable conflicts.** If two *boot-triggered* services conflict with each other, there is no way to honour both, so both are marked Failed with `ValidationError`. As with cycles, if either is Critical, Safe mode applies.

**Warnings.** Some conditions are logged but do not block boot — most notably a `Readiness=Alive` service that others `Requires` (see [service types](/peios/using-peios/services-and-jobs/service-types.md)). Warnings appear in the logs and do not change state.

**Validation errors.** Some conditions *do* fail the service — for example a health-check configuration that violates the [flap constraint](/peios/using-peios/services-and-jobs/supervision.md). These mark the service Failed with `ValidationError`.

When more than one finding applies to the same service, peinit records a single primary cause by precedence — `CycleDetected` > `ValidationError` > `DependencyFailure` — but it logs *all* findings, so the primary cause never hides the others.

## Parallel start

After validation, peinit starts services whose dependencies are all satisfied, and it does so **in parallel** up to a configurable limit:

| Registry key | Default | Meaning |
|---|---|---|
| `Machine\System\Boot\MaxParallelStarts` | 10 | Maximum services starting concurrently. |

The scheduler is simple: every service with no unsatisfied dependencies is eligible; peinit starts up to `MaxParallelStarts` of them; as each one reaches a [satisfying state](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) its dependents become eligible and join the queue. The result is that independent subtrees of the graph come up at the same time, while ordering constraints are still honoured exactly.

> [!NOTE]
> Boot order is *emergent*, not hardcoded. The typical sequence — `eudev`, `lpsd`, `authd`, `eventd`, networking, then application and login services — is simply what the standard role definitions' dependencies produce. Change the dependencies and you get a different order. The platform daemons come up first because everything else, directly or transitively, requires them.

## Failure propagation

When a service enters Failed during graph execution, the failure spreads along `Requires` and `BindsTo` edges — and only those:

1. Every service that `Requires` the failed one transitions to Failed with `DependencyFailure`.
2. Every service that merely `Wants` it is **unaffected** and starts normally.
3. Propagation is **transitive**: if A requires B and B requires C, and C fails, then B fails, then A fails — each with `DependencyFailure`.

This is the payoff of the Requires/Wants distinction. A hard dependency failing takes its dependents down with it; a soft one failing is shrugged off. Choosing the right relationship is choosing how far a failure is allowed to travel.

## On-demand starts

When you start a service explicitly rather than at boot, peinit does the same graph work on a smaller scope — the requested service's transitive closure:

1. Collect all transitive `Requires` and `BindsTo` dependencies (and best-effort `Wants`).
2. Validate that sub-graph (cycles, missing targets).
3. Resolve `Conflicts` — stop anything that conflicts.
4. Start the sub-graph with the same parallel scheduler.

Anything already in a satisfying state (Active, Completed, Skipped) is left alone — its dependency is already met, so there is no needless restart. Dependencies pulled in this way start with cause `DependencyStart`. If two on-demand starts need the same dependency at once, their start operations [merge](/peios/using-peios/services-and-jobs/jobs-and-operations.md) rather than racing.

## Shutdown reverses the graph

peinit does not need a separate stop-ordering configuration. [Shutdown](/peios/using-peios/services-and-jobs/shutdown.md) simply **reverses** the dependency graph: services with no dependents stop first, and services that others depend on stop last. A service is never stopped until everything that `Requires` or `BindsTo` it has already stopped.

The very last services to stop are the TCB daemons everything rests on — `eventd`, then `authd`, then `lpsd`, then `registryd` — with `registryd` stopped dead last, mirroring its position as the first service ever started. The full shutdown sequence is in [Shutdown](/peios/using-peios/services-and-jobs/shutdown.md).

## Where to start

To see the states services move through as they start and stop in order, read [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md).

To understand how a target's failure interacts with the dependent's own restart policy, read [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

To follow how dependency-driven starts appear as operations, read [Jobs and operations](/peios/using-peios/services-and-jobs/jobs-and-operations.md).

---

# Service identity and privileges

_Peios / Using Peios / Services & jobs_

> How a service token is materialised — minted for SYSTEM, requested from authd otherwise — plus the per-service SID and RequiredPrivileges trimming.

Every service process runs under a [KACS token](/peios/security-fundamentals/tokens/overview.md) — the kernel object that carries identity into every system call and is the input to every access decision. peinit's responsibility is to get the *right* token for a service and install it on the child process *before* the binary execs, so the service runs as the intended principal from its very first instruction.

One rule frames everything else: **peinit never shares its own SYSTEM token with a service.** Even a service configured `Identity=SYSTEM` gets a *separately materialised* token of its own — never peinit's. This keeps every service's identity independent and auditable, and it is one of peinit's [security invariants](#security-invariants).

## How a token is materialised

The `Identity` field decides where the token comes from:

| `Identity` value | Token source |
|---|---|
| `SYSTEM` | **Minted by peinit** from its own SYSTEM identity. |
| Any other principal name or SID | **Requested from [authd](/peios/security-fundamentals/boot-and-trust-establishment/authd-handoff.md).** |
| Absent or empty | authd, defaulting to `LocalService`. |

### The SYSTEM path

For a `SYSTEM` service, peinit mints an independent SYSTEM token using its own token as the template: it reads its own identity (user SID `S-1-5-18`, the group list, the privilege set) and mints a fresh primary token with the same identity, adding the service's [per-service SID](#the-per-service-sid). The minted token is fully independent — the [privilege trimming](#privilege-restriction) that follows affects only this token, never peinit's.

This path exists because of a bootstrapping problem: the platform services that *make* the normal token flow possible cannot use it, because they have to start *before* it exists. `registryd`, `authd`, `lpsd`, and `eventd` all run as SYSTEM and are all minted this way during [boot](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md), before authd is available to mint anything.

> [!NOTE]
> There is no allow-list restricting which services *may* be `SYSTEM`. The security boundary is the [registry key descriptor](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) on `Machine\System\Services\` — an administrator who can create service definitions is already trusted to assign any identity, so a second gate would add nothing. The dangerous case is never *implicit*, though: `SYSTEM` must be written out explicitly; an empty `Identity` defaults to the minimal `LocalService`, never to SYSTEM.

### The authd path

For any other identity, peinit asks [authd](/peios/security-fundamentals/boot-and-trust-establishment/authd-handoff.md) for a token: it sends the `Identity` value verbatim, and authd routes it to the right source —

- **Well-known principals** (`LocalService`, `NetworkService`, …) → a built-in identity with a predefined minimal privilege set;
- **Local service accounts** → [lpsd](/peios/security-fundamentals/identity/overview.md), the local principal store;
- **Domain accounts** → the directory connector;

— resolves the principal's SIDs, mints a token, creates a logon session, and hands the token back to peinit to install. peinit neither knows nor cares whether the identity is local or domain; routing is authd's job.

> [!IMPORTANT]
> Every non-SYSTEM service start depends on authd. peinit interacts with it over a non-blocking, timed channel — if authd is unreachable or unresponsive, the service start *fails* rather than hanging PID 1. This is why a broken authd takes down all non-platform service starts but leaves the SYSTEM-minted platform services unaffected. The authd wire interface is owned by authd, not by peinit, and is still being specified.

## The per-service SID

Every service token — minted or authd-issued — carries a **per-service SID** in its group list. It uses authority `S-1-5-80` and is derived deterministically from the service name: the SHA-1 of the uppercased name (as UTF-16LE), with the digest split into five sub-authorities. authd adds it automatically for the tokens it mints; peinit computes and adds it itself for SYSTEM tokens, no authd involvement needed.

The point of the per-service SID is **fine-grained access control even when services share an identity.** A dozen services might all run as `LocalService`, but each has a *unique* service SID, so an ACL can grant access to exactly one of them without inventing a dedicated account. It is also what keeps the platform services — all running as SYSTEM — distinguishable to [AccessCheck](/peios/security-fundamentals/access-decisions/overview.md). See [Well-known principals](/peios/security-fundamentals/identity/well-known-principals.md) for the SID landscape this fits into.

## Privilege restriction

A token comes from its source (authd or the SYSTEM mint) with a default set of [privileges](/peios/security-fundamentals/privileges/overview.md). `RequiredPrivileges` lets a definition trim that set down to only what the service needs:

- peinit reads the `RequiredPrivileges` allow-list and **removes every privilege not on it** from the token before exec.
- Restriction is **purely subtractive.** peinit removes privileges; it never adds them. A service cannot acquire a privilege its token's source did not grant.
- If `RequiredPrivileges` is absent, the token's default privilege set is used unchanged.

This is least-privilege made concrete: `eudev`, for instance, runs as SYSTEM (it needs device access during early boot) but with its privilege set stripped to the minimum it actually uses, so a compromise of `eudev` does not hand an attacker the full SYSTEM privilege set. Removal is permanent for the life of that token — it is not a disable that the service can re-enable. See [Privileges](/peios/security-fundamentals/privileges/overview.md) for what the individual privileges grant.

## Which identity runs what

A service is not just its main process — it has hooks, health checks, and a reload command, and each runs under a defined identity:

| Context | Runs as |
|---|---|
| **Main process** | The service's `Identity`. |
| **`ExecStartPre` / `ExecStartPost`** | `HookIdentity` if set, otherwise the service's `Identity`. |
| **Health checks** | The service's `Identity`, always. |
| **`ExecReload`** (command form) | The service's `Identity`, always — `HookIdentity` does not apply. |
| **[Ad-hoc jobs](/peios/using-peios/services-and-jobs/jobs-and-operations.md)** | The token captured by JFS — the submitter's (possibly impersonated) identity. |

Token materialisation for hooks follows the same rules: a `HookIdentity` of `SYSTEM` is minted; any other principal is requested from authd, at the point the hook runs.

`HookIdentity` exists so hooks can run with *different* — usually higher — privileges than the service itself: creating directories in privileged locations, or running a database migration as an admin identity, before the long-running daemon drops to a lesser identity.

> [!WARNING]
> peinit does **not** validate filesystem permissions on hook binaries. If `HookIdentity` grants elevated privileges, it is the administrator's responsibility to ensure the hook binary and every parent directory are not writable by a lower-privileged identity — otherwise a less-trusted principal could replace the hook and run code as the elevated identity. ([FACS](/peios/security-fundamentals/file-access/overview.md) enforces KACS descriptors on managed filesystems, but peinit itself performs no such check — protecting hook binaries still depends on correct SDs from packaging and controlled paths.)

## Security invariants

The identity model rests on a few rules peinit never breaks:

1. **peinit never shares its SYSTEM token.** Even `Identity=SYSTEM` services get a separately minted token.
2. **peinit never drops its own SYSTEM identity.** PID 1 runs as SYSTEM for the life of the system — its identity is axiomatic, granted by the kernel at boot.
3. **Privileges are subtractive only.** `RequiredPrivileges` removes; it can never add.
4. **Identity is deterministic.** Every service runs as a known principal. `SYSTEM` must be declared explicitly; an empty `Identity` is the minimal `LocalService`, never SYSTEM.

## Where to start

`Identity` decides what a service *can do*; the [ServiceSecurity descriptor](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) decides *who can manage it* — two independent concerns. Read [Who can manage a service](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) for the other half.

To see how the token is installed alongside the rest of the process setup, read [The execution environment](/peios/using-peios/services-and-jobs/execution-environment.md).

For the identity primitives themselves — tokens, SIDs, privileges — start at [Tokens](/peios/security-fundamentals/tokens/overview.md).

---

# The execution environment

_Peios / Using Peios / Services & jobs_

> What a service process starts with — its cgroup tree, stdio, layered environment, limits, hooks, conditions and asserts, and the fd store.

When peinit starts a service, the process it hands you is built deliberately — a clean context with a known identity, a known environment, and only the resources peinit chose to give it. This page covers everything about that context except the [token](/peios/using-peios/services-and-jobs/identity-and-privileges.md) (its own page): the cgroup the process lives in, its standard streams, the environment it sees, the limits on it, and the hooks and checks that run *around* the start.

## A clean context

A service inherits **only** what peinit explicitly hands it — its standard streams and any [stored file descriptors](#the-fd-store). Every other descriptor peinit holds — the control socket, the notify socket, the event-loop fd, the registry and authd and eventd connections — is opened close-on-exec, so it closes automatically at exec and can never leak into a service. peinit also resets the child's signal state to defaults (it runs with all signals blocked for its own [signalfd](/peios/using-peios/services-and-jobs/shutdown.md); a service must not inherit that). The guarantee is that a service starts from a clean slate, not from peinit's privileged one.

## The cgroup tree

Every service runs in its own [cgroup](/peios/using-peios/threads-and-processes/process-lifecycle.md) tree under `/sys/fs/cgroup/peinit/`, with separate sub-cgroups for the main process, hooks, and health checks:

```
/sys/fs/cgroup/peinit/<service>/
                       ├── main/     the main process
                       ├── hooks/    pre/post hooks
                       └── health/   health checks
```

peinit uses cgroups for two things only: **tracking** every process a service spawns (so a child that forks its own children is still accounted to the service), and **clean kill** (terminating the entire tree at once, so nothing is orphaned). It does *not* use cgroups for resource accounting or limits.

The split into sub-cgroups serves a real purpose: it satisfies cgroup v2's "no internal processes" rule, and it lets peinit kill a service's *hooks* or *health checks* without touching the main process.

When an old tree cannot be removed — because a process survived SIGKILL in [D-state](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) and leaked it — peinit creates a fresh **generational** tree (`<service>.gen<N>`) for the next start, so a stuck old instance never blocks a new one. Leaks are surfaced in the service's `warnings`; they are a sign of an underlying I/O fault, covered in [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

## Standard streams

peinit wires a service's standard streams before exec:

- **stdin** is redirected to `/dev/null`. peinit never gives a service an interactive input channel; a service that needs input must obtain it explicitly (a socket, a stored fd).
- **stdout** and **stderr** are redirected to pipes peinit holds, so it can capture and forward every line. This is the subject of [Service output and logging](/peios/using-peios/services-and-jobs/output-and-logging.md).

### Attaching a terminal

A service that sets **`TTYPath`** to an absolute terminal path — `/dev/console`, `/dev/tty1`, a serial line — gets that terminal on all three standard streams instead of the wiring above, and becomes a session leader owning it as its **controlling terminal**. That last part is what makes job control work: Ctrl-C interrupts, `fg`/`bg` behave, and a hangup reaches the process group. It is what a boot shell or a `login` prompt needs.

It carries a path rather than a flag because which terminal is a per-service question: a maintenance shell on `tty1` while the kernel console is a serial line is an ordinary thing to want.

> [!WARNING]
> Attaching a terminal **suppresses log capture** — the pipes eventd would read are closed, so the service's output goes to the terminal and nowhere else. Don't set `TTYPath` on a service whose output you expect to find in the logs.
>
> Two services pointed at the same terminal will fight over it: both read the same input, and neither is told about the other. peinit does not currently detect this. Until it does, treat "one service per terminal" as a rule you enforce yourself, or declare a `Conflicts` between them.

## The environment

peinit constructs each service's environment in layers, lowest precedence first — it does **not** pass through its own near-empty startup environment.

1. **Compiled-in base.** A fixed floor peinit always provides — currently just `PATH`:
   `/bin`.
2. **Global `EnvVars`.** Each value under `Machine\System\Init\EnvVars\` becomes a variable (value name → variable name). `EnvVars\PATH` overrides the base `PATH`; other names add. Every service gets this layer except **registryd**, which serves the key itself — see the warning below for why that exemption exists.
3. **Per-service `Environment`.** The definition's own `KEY=VALUE` entries, which override layers 1–2.
4. **Protocol variables.** `NOTIFY_SOCKET` (always) and `LISTEN_FDS` / `LISTEN_FDNAMES` (only when [stored fds](#the-fd-store) are injected). These have the **highest** precedence and cannot be overridden — a service that clobbered `NOTIFY_SOCKET` would break [sd_notify](/peios/using-peios/services-and-jobs/service-types.md).

A change to `EnvVars\` takes effect on a service's *next start*, like the per-service `Environment` field — it is not pushed into running services.

peinit deliberately does **not** set `HOME`, `USER`, `LOGNAME`, `SHELL`, or `TERM`. Peios identity is a [token](/peios/using-peios/services-and-jobs/identity-and-privileges.md) (a SID), not a passwd entry, so there is no canonical home directory or login shell to fill in. A service that needs any of these supplies it through `EnvVars\` or its own `Environment`.

> [!WARNING]
> Write access to `Machine\System\Init\EnvVars\` is equivalent to compromising **every service peinit starts** — a principal who can set it can inject `LD_PRELOAD`, `LD_LIBRARY_PATH`, and the like into all of them. peinit deliberately does not filter variable names; the key's [security descriptor](/peios/using-peios/registry-security/access-control.md) is the control boundary. Keep it tight — the recommended default is SYSTEM full, Administrators read-only.
>
> This is also why **registryd** is the one service that never receives this layer: it is the daemon that *serves* the key. Letting the key inject into the process that enforces who may write it would make the control boundary self-referential. Every other service, platform daemons included, gets the ordinary layering.

## Working directory and limits

| Field | Effect |
|---|---|
| `WorkingDirectory` | The process's working directory. Must be a non-empty absolute path; defaults to `/`. |
| `LimitNOFILE` | `RLIMIT_NOFILE` — the maximum number of open file descriptors. |
| `LimitCORE` | `RLIMIT_CORE` — the maximum core-dump size, in bytes. |

peinit also sets `oom_score_adj`: `-1000` (OOM-immune) for `ErrorControl=Critical` services, and the default `0` for everyone else — so the kernel never picks a Critical platform daemon as an out-of-memory victim.

## Runtime directories

`RuntimeDirectories` gives a service private scratch space under `/run` without an `ExecStartPre` that has to `mkdir` it by hand. Each name you list becomes a directory created **directly under `/run`** — `RuntimeDirectories=myapp` yields `/run/myapp` — set up immediately before the main process starts. Each one gets a [security descriptor](/peios/using-peios/registry-security/access-control.md) granting full access to SYSTEM, Administrators, and the service's own [SID](/peios/using-peios/services-and-jobs/identity-and-privileges.md), so the service can write to its directory while other principals cannot. If a directory can't be created, or its descriptor can't be applied, the start fails with `ParentSetupFailure`.

peinit does **not** remove these directories when the service stops. `/run` is a boot-scoped tmpfs, so they simply disappear at the next boot rather than being torn down on each stop — a restarting service finds its runtime directory (though not necessarily its contents) already in place.

For the exact naming rules on each entry and the field's type and default, see the [Registry key reference](/peios/using-peios/services-and-jobs/registry-key-reference.md).

## Pre-exec and post-exec hooks

Hooks let a service do work *around* its main process without baking it into the binary.

- **`ExecStartPre`** runs *before* the main binary. Each command runs in sequence, in the `hooks/` sub-cgroup; **any** hook exiting non-zero aborts the start (the whole cgroup is killed and the service Fails with `PreHookFailure`). Use it for prerequisites that must hold before the service starts — creating a runtime directory, waiting on a precondition.
- **`ExecStartPost`** runs *after* readiness (Simple) or *after a successful exit* (Oneshot). A post-hook failure is **logged but does not fail the service** — by the time it runs, the service is already up. (A Oneshot that *fails* never runs its post-hooks.)

Hooks run under [`HookIdentity`](/peios/using-peios/services-and-jobs/identity-and-privileges.md) if set, otherwise the service's own identity. Their output is captured and tagged like the main process's, labelled with the hook (e.g. `jellyfin/ExecStartPre[0]`).

### Command string parsing

The command fields — `ExecStartPre`, `ExecStartPost`, `ExecReload`, `HealthCheck` — are parsed into an argument list with simple, predictable rules. **peinit never invokes a shell.**

- Whitespace splits the command into arguments; double quotes group text containing spaces into one argument and are not retained (`--name="hello world"` → one argument `--name=hello world`).
- There is **no** shell expansion, variable substitution, or globbing. Backslash is literal; a single quote is an ordinary character.
- The first argument is the executable and **must be an absolute path** beginning with `/`. Relative names and PATH search are validation errors.
- An empty or whitespace-only command, or an unclosed quote, is a validation error.

If a command genuinely needs shell features, wrap it in a script and point the field at the script.

`ExecReload` can also be a signal, written `signal:<NAME>` — for example `signal:SIGHUP`. The name must be an exact, canonical Linux signal name (uppercase, no aliases, no numbers); `SIGKILL` and `SIGSTOP` are rejected because they cannot be handled as a reload. With no `ExecReload` at all, reload sends `SIGHUP`. The reload lifecycle is covered in [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md).

## Conditions and asserts

Before peinit runs any hook or the main process, it evaluates **conditions** and **asserts** — start-time checks in the same `type:argument` form. The difference is what failure *means*:

- A failed **Condition** *skips* the service — it transitions to [Skipped](/peios/using-peios/services-and-jobs/the-service-lifecycle.md), which **satisfies its dependents** (it succeeded by not needing to run). Conditions express "only run here if it makes sense."
- A failed **Assert** *fails* the service — it transitions to Failed with `AssertionError`. Asserts express "this was expected to run, and a precondition is missing."

Conditions are checked first; only if all pass are asserts checked. All entries are AND-ed. The check types:

| Type | Form | Passes when |
|---|---|---|
| `path` | `path:<path>` | the path exists (any type) |
| `file` | `file:<path>` | a regular file exists |
| `directory` | `directory:<path>` | a directory exists |
| `registry` | `registry:<key>` | the registry key exists |

Two constraints follow from peinit being single-threaded PID 1, which [must never block](/peios/using-peios/services-and-jobs/overview.md):

- **`registry:` checks are cache-only.** A `registry:` check may only name a key peinit already caches (under `Machine\System\Services\` or `Machine\System\Init\`); it is evaluated against the in-memory model, never a live read. Naming any other key is a validation error.
- **Filesystem checks run off the main loop.** `path:`/`file:`/`directory:` checks are performed by a short-lived forked helper, not a `stat()` on the event loop, because `stat()` can wedge on a hung mount.

That timeout is the `PreStartCheckTimeout` field, which defaults to **5 seconds**. A filesystem check that does not finish within it — say a `stat()` wedged on a hung mount — is treated as **not satisfied**, fail-safe: peinit kills the helper and treats the check as unmet, so a Condition skips the service and an Assert fails it, rather than letting the event loop hang.

## The fd store

The fd store lets a service preserve file descriptors **across restarts** — most usefully a listening socket, so a stateful daemon can restart without dropping connections. It is opt-in: `FdStoreMax` (default 0) sets the maximum number of fds peinit will hold; 0 disables it.

The mechanics ride on [sd_notify](/peios/using-peios/services-and-jobs/service-types.md):

- A service stores an fd by sending `FDSTORE=1` with the fd attached (and optionally `FDNAME=<name>`; the default name is `stored`). peinit holds it.
- It removes one with `FDSTOREREMOVE=1` and `FDNAME=<name>`.
- On the **next start**, peinit injects the stored fds starting at fd 3, sets `LISTEN_FDS` to the count and `LISTEN_FDNAMES` to the colon-separated names, then clears the store. (This is the same `LISTEN_FDS` convention systemd uses, so software with existing fd-passing support works unmodified.)

The store **survives an automatic restart** (crash → restart policy → new start) — that is the whole point. It is **cleared** on an *explicit* stop or shutdown (the service is not coming back), and when a [removed definition](/peios/using-peios/services-and-jobs/defining-a-service.md) is finally discarded.

## Where to start

For the identity half of the launch — how the token is chosen and installed — read [Service identity and privileges](/peios/using-peios/services-and-jobs/identity-and-privileges.md).

For what happens to the stdout/stderr pipes peinit holds, read [Service output and logging](/peios/using-peios/services-and-jobs/output-and-logging.md).

For how conditions, asserts, and hooks fit into the start sequence and its states, read [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md).

---

# Controlling services

_Peios / Using Peios / Services & jobs_

> peiosctl and the control socket — the verbs and their rights, wait semantics, the command-by-state matrix, status output, and error codes.

`peiosctl` is the command-line tool for driving **peinit** at runtime — starting and stopping services, querying their state, reloading configuration, and shutting the system down.

```
peiosctl <command> [service] [flags]
```

```
$ peiosctl status jellyfin        # current state of one service
$ peiosctl start jellyfin         # start it, wait until Active or Failed
$ peiosctl list                   # every service you can query
$ peiosctl shutdown reboot        # graceful reboot
```

Underneath, `peiosctl` is a thin client over peinit's **control socket** at `/run/services/peinit/control.sock`. The wire protocol — not the CLI — is the normative interface, so everything here (commands, rights, semantics) holds regardless of which front-end you use.

## How the control interface works

The socket speaks **newline-delimited JSON**: one request object per line, one response object per line. A request and its success response look like this:

```json
{"command": "start", "service": "jellyfin", "wait": true}
{"status": "ok", "operation_id": "a1b2c3d4-...", "service": "jellyfin", "state": "active", "cause": "explicit_start", "warnings": []}
```

Two properties are worth knowing even if you only ever use `peiosctl`:

- **Every command is access-controlled.** When you connect, peinit captures your [token](/peios/using-peios/services-and-jobs/identity-and-privileges.md) from the kernel and runs [AccessCheck](/peios/security-fundamentals/access-decisions/overview.md) against the target service's descriptor for *every* command. There is no "trust localhost," no override. Who may do what is the subject of [Who can manage a service](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md).
- **Lifecycle commands create [operations](/peios/using-peios/services-and-jobs/jobs-and-operations.md).** A `start`/`stop`/`restart`/`reload`/`reset` returns an `operation_id` — a GUID you can poll. Conflict resolution between concurrent commands happens at the operation layer, which is why two simultaneous `start`s merge instead of colliding.

## Service commands

| Command | Does | Required right |
|---|---|---|
| `start` | Run the service through its full start sequence. | `SERVICE_START` |
| `stop` | SIGTERM, then SIGKILL after `StopTimeout`. | `SERVICE_STOP` |
| `restart` | Stop then start, as one operation. | `SERVICE_STOP` + `SERVICE_START` |
| `reload` | Re-read configuration (`ExecReload`, or SIGHUP). | `SERVICE_INTERROGATE` |
| `reset` | Clear `Failed`/`Abandoned`/`Skipped` → `Inactive`. | `SERVICE_STOP` |
| `status` | Report state, cause, PID, uptime, health, current job and operation, warnings. | `SERVICE_QUERY_STATUS` |
| `list` | List services and states (filtered to what you can query). | (per-service `SERVICE_QUERY_STATUS`) |

```
$ peiosctl restart jellyfin
$ peiosctl reload nginx
$ peiosctl reset failed-migration     # clear a Failed state without starting
```

## System commands

| Command | Does | Required right |
|---|---|---|
| `shutdown <type>` | Graceful [shutdown](/peios/using-peios/services-and-jobs/shutdown.md). `type` is `poweroff`, `reboot`, or `halt`. | `SYSTEM_SHUTDOWN` |
| `reload-config` | Re-read *all* definitions and rebuild the graph (atomic). | `SYSTEM_RELOAD_CONFIG` |
| `operation-status <id>` | Report the state of an operation by GUID. | `SERVICE_QUERY_STATUS` on its target |

```
$ peiosctl shutdown poweroff
$ peiosctl reload-config
$ peiosctl operation-status a1b2c3d4-...
```

## Wait semantics

By default, a lifecycle command **blocks until its operation reaches a terminal state** — `start` waits for Active (or Failed), `stop` waits for Inactive, and so on. Pass `--no-wait` to get the `operation_id` back immediately and poll with `operation-status` instead.

| Command | Default | Waits for |
|---|---|---|
| `start` | wait | Active (Simple) / Completed or Inactive (Oneshot), or Failed |
| `stop` | wait | Inactive |
| `restart` | wait | the successful start target, or Failed |
| `reload` | **no-wait** | (with `--wait`) the Reloading state to resolve |
| `reset` | immediate | — |

`reload` is the exception — it returns immediately by default, because a reload may have no observable completion. With `--wait`, the response carries a `mode`:

| `mode` | Meaning |
|---|---|
| `confirmed` | The service signalled `READY=1` (and, for a command reload, the command exited 0). |
| `advisory` | The reload was issued and the detection window elapsed without explicit confirmation. |
| `failed` | The `ExecReload` command exited non-zero or timed out. **The service stays Active** — a failed reload never takes down a running service. |

The detection window is a **fixed 2 seconds** — it is built in and is *not* configurable via the registry. If the service says nothing within that window, the reload resolves as `advisory` and the service stays Active.

> [!NOTE]
> A connection blocked on a `--wait` operation is *not* counted as idle, so it is not closed by `ConnectionTimeout`. It stays open until the operation resolves, bounded by the operation's own timeout (e.g. `StartTimeout`).

## The command × state matrix

A command sent to a service in an unexpected state returns an **error**, not a silent no-op. This matrix is the authority on what each command does in each [state](/peios/using-peios/services-and-jobs/the-service-lifecycle.md):

| Command | Inactive | Starting | Active | Reloading | Stopping | Completed | Backoff | Failed | Abandoned | Skipped |
|---|---|---|---|---|---|---|---|---|---|---|
| **start** | Start | MERGE | ALREADY | ALREADY | QUEUE | Start | DEFER | Start | ERROR | Start |
| **stop** | NOOP | Cancel+Stop | Stop | Stop | MERGE | Clear | Cancel | NOOP | ERROR | NOOP |
| **restart** | Start | QUEUE | Restart | Restart | QUEUE | Start | Restart | Start | ERROR | Start |
| **reload** | ERROR | ERROR | Reload | MERGE | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR |
| **reset** | NOOP | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR | Clear | Clear | Clear |
| **status** | OK | OK | OK | OK | OK | OK | OK | OK | OK | OK |

Legend:

- **MERGE** — an operation of this type is already running; your command merges into it and you get its GUID.
- **DEFER** — your `start` is accepted and creates a Pending start operation, but it does *not* run yet: it waits out the service's existing backoff deadline and then starts. If a deferred start is already waiting, you merge into it and get its GUID.
- **ALREADY** — already in the target state; returns the current status, not an error.
- **QUEUE** — queued as Pending; runs after the current operation finishes.
- **NOOP** — no effect; returns the current status.
- **Clear** / **Cancel** — clear the state to Inactive / abort the current operation, then proceed.
- **ERROR** — invalid for this state; returns an error with an explanation.

The [Backoff](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) column is the subtle one: the service is down with an automatic restart pending, so `start` is *deferred* — it creates a Pending start that honours the remaining backoff delay and only runs once that deadline expires (a second `start` merges into the one already waiting), `stop` cancels the pending restart and any deferred start (the service goes Inactive), `restart` cancels the automatic one and does an admin restart, and `reload`/`reset` are invalid because no process exists.

## Reading status

`status` returns the full picture for one service:

```json
{
    "status": "ok",
    "service": "jellyfin",
    "state": "active",
    "cause": "explicit_start",
    "status_text": "Listening on port 8096",
    "current_job": {"id": "a1b2...", "type": "service_main", "pid": 1234, "started_at": "...", "identity": "jellyfin-svc"},
    "current_operation": {"id": "e5f6...", "type": "start", "source": "admin"},
    "health": "healthy",
    "uptime_seconds": 86400,
    "definition_removed": false,
    "warnings": []
}
```

- `state` and `cause` are the [lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) pair — read them together.
- `status_text` is the latest `STATUS=` string the service sent via sd_notify (`null` if never sent; cleared on each restart).
- `current_job` and `current_operation` are the [job and operation](/peios/using-peios/services-and-jobs/jobs-and-operations.md) GUIDs, or `null`.
- `health` is `healthy`, `unhealthy`, `unknown`, or `null` (no health check).
- `definition_removed` is `true` when the definition was deleted but an instance is still [draining](/peios/using-peios/services-and-jobs/defining-a-service.md).
- `warnings` lists leaked sub-cgroups and other operator-relevant notices.

`list` returns a compact summary of every service you can query — services you lack `SERVICE_QUERY_STATUS` on are simply **omitted**, not denied:

```json
{"status": "ok", "services": [
    {"service": "jellyfin", "state": "active", "cause": "explicit_start", "health": "healthy"},
    {"service": "registryd", "state": "active", "cause": "dependency_start", "health": null}
]}
```

`operation-status` returns one operation by GUID; an unknown or expired GUID is the `UNKNOWN_OPERATION` error. (Operations are dropped after a short retention grace once terminal — long enough for a polling client to read the result, not forever.)

## Error codes

An error response is `{"status": "error", "code": "...", "message": "..."}`. The `code` is one of:

| Code | Meaning |
|---|---|
| `ACCESS_DENIED` | AccessCheck denied the command against the target descriptor. |
| `UNKNOWN_SERVICE` | No such service definition (also returned for `start`/`restart`/`reload` on a [definition-removed](/peios/using-peios/services-and-jobs/defining-a-service.md) service). |
| `UNKNOWN_OPERATION` | No such operation GUID — never existed, or dropped after its retention grace. |
| `MALFORMED_REQUEST` | The request line is not a single valid JSON object. |
| `REQUEST_TOO_LARGE` | The request exceeds `MaxRequestSize`. |
| `INVALID_COMMAND` | The `command` field is missing or unknown. |
| `INVALID_ARGUMENTS` | A required field is missing or malformed (e.g. `shutdown` with no valid `type`). |
| `INVALID_STATE` | Not valid for the service's current state (an `ERROR` cell above), or rejected because the system is already shutting down. |
| `OPERATION_TIMEOUT` | A `--wait` operation did not reach a terminal state within its timeout. |
| `INTERNAL_ERROR` | peinit hit an internal failure executing the command. |

The `message` is human-readable and non-normative — read it for context, key off the `code`.

## Connection limits

peinit enforces hard limits on the control socket, all tunable under `Machine\System\Init\`:

| Key | Default | Limits |
|---|---|---|
| `MaxControlConnections` | 32 | Concurrent client connections (excess are refused at connect). |
| `MaxRequestSize` | 65536 | Bytes per request. |
| `ConnectionTimeout` | 30 | Seconds an *idle* connection may sit before it is closed. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The command succeeded. |
| `1` | A usage error. |
| non-zero | The command failed — an access denial, unknown service, invalid state, or timeout. |

## Where to start

To understand the states the matrix refers to, read [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md).

To understand the operations every lifecycle command creates and how to poll them, read [Jobs and operations](/peios/using-peios/services-and-jobs/jobs-and-operations.md).

To configure who may run each command, read [Who can manage a service](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md).

---

# Who can manage a service

_Peios / Using Peios / Services & jobs_

> Every control command runs AccessCheck against the service's ServiceSecurity descriptor. The rights, the defaults, and the system control descriptor.

A service is a *securable object*. Just as a [file](/peios/security-fundamentals/file-access/overview.md) or a [registry key](/peios/using-peios/registry-security/access-control.md) carries a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) that decides who may touch it, a service carries one that decides who may **start, stop, query, or reload** it. peinit enforces it on *every* [control command](/peios/using-peios/services-and-jobs/controlling-services.md) — there is no command that skips the check.

This is the second, independent half of the security model. The first half, [Service identity and privileges](/peios/using-peios/services-and-jobs/identity-and-privileges.md), is about what a service *can do* (its token). This page is about who can *manage the service* — a completely separate question, answered by a completely separate descriptor.

## Two descriptors, two questions

A service is associated with **two** descriptors that are easy to conflate but answer different questions and are enforced by different components:

| Descriptor | Question | Stored | Enforced by |
|---|---|---|---|
| **Registry key SD** | Who can *read or edit the definition*? | On the `Machine\System\Services\<name>` key | [LCS](/peios/using-peios/registry-security/access-control.md), at key-open time |
| **ServiceSecurity SD** | Who can *manage the running service*? | As the `ServiceSecurity` binary value on that key | **peinit**, on every control command |

The registry key SD is ordinary [registry access control](/peios/using-peios/registry-security/access-control.md) and not peinit's concern — peinit reads definitions as SYSTEM, which has full access. The **ServiceSecurity** SD is peinit's domain, and the rest of this page is about it.

They are genuinely independent. An administrator might be able to *query a service's status* (ServiceSecurity grants `SERVICE_QUERY_STATUS`) but not *read its configuration* (the registry key SD denies read) — or the reverse. Runtime control and configuration access are separate concerns, and both combinations are valid.

## Service access rights

The ServiceSecurity descriptor grants these rights:

| Right | Bit | Grants |
|---|---|---|
| `SERVICE_QUERY_STATUS` | 0x0001 | Query state, PID, cause, health, warnings. |
| `SERVICE_START` | 0x0002 | Start the service. |
| `SERVICE_STOP` | 0x0004 | Stop the service. |
| `SERVICE_INTERROGATE` | 0x0008 | Reload the service. |
| `SERVICE_ALL_ACCESS` | 0x000F | All of the above — the "full access" granted to SYSTEM by default. |

`restart` requires **both** `SERVICE_STOP` and `SERVICE_START`, since it is a stop followed by a start. `reset` requires `SERVICE_STOP`.

When peinit evaluates the descriptor it maps the generic rights as follows, so a descriptor written with generic rights behaves sensibly:

| Generic | Maps to |
|---|---|
| `GENERIC_READ` | `SERVICE_QUERY_STATUS` |
| `GENERIC_WRITE` | `SERVICE_START` \| `SERVICE_STOP` \| `SERVICE_INTERROGATE` |
| `GENERIC_EXECUTE` | `SERVICE_START` \| `SERVICE_STOP` \| `SERVICE_INTERROGATE` |
| `GENERIC_ALL` | `SERVICE_ALL_ACCESS` |

## How a command is authorised

Every control command runs the same gate:

1. peinit captures the caller's [token](/peios/using-peios/services-and-jobs/identity-and-privileges.md) from the kernel (`kacs_open_peer_token`) — the caller's *effective* identity at connection time, so if the caller is [impersonating](/peios/security-fundamentals/impersonation/overview.md), the impersonated identity is what is checked.
2. peinit resolves the target service and its ServiceSecurity descriptor.
3. peinit runs [AccessCheck](/peios/security-fundamentals/access-decisions/overview.md): the caller's token against the descriptor, for the right the command needs.
4. **Denied** → return `ACCESS_DENIED` and **log the attempt** (caller SID, target service, requested right).
5. **Granted** → execute the command.

> [!NOTE]
> Access denials are always logged — caller, target, and the right requested. Silent denial is a specification violation. If you are debugging a denial, the audit record has everything you need; see [Debugging a denial](/peios/security-fundamentals/access-decisions/debugging-a-denial.md).

## The default descriptor

If a service has no `ServiceSecurity` value, it **inherits** its parent key's. If no ancestor sets one either, peinit applies a built-in default:

- **SYSTEM** (`S-1-5-18`) — full access.
- **Administrators** (`S-1-5-32-544`) — query and stop only.

So out of the box, administrators can see and stop a service but not start or reload it unless a descriptor grants more — a conservative default that you widen deliberately.

ServiceSecurity is **hot-reloaded**: a change to the value in the registry takes effect on the **next control request**, with no service restart. peinit picks the change up through a [registry notification](/peios/using-peios/registry-concepts/watches.md). This is why ServiceSecurity is in its own [mutability class](/peios/using-peios/services-and-jobs/defining-a-service.md) — access policy should be able to change without disturbing a running service.

## The system control descriptor

Some operations are not about any one service — `shutdown` and `reload-config` act on the whole system. These are checked against **peinit's own** descriptor, stored at `Machine\System\Init\ControlSecurity`:

| Right | Bit | Grants |
|---|---|---|
| `SYSTEM_SHUTDOWN` | 0x0001 | Initiate poweroff, reboot, or halt. |
| `SYSTEM_RELOAD_CONFIG` | 0x0002 | Re-read all definitions and rebuild the graph. |

Its generic mapping deliberately gives `GENERIC_READ` *nothing* — there is no "read" of the system control object, only the two actions:

| Generic | Maps to |
|---|---|
| `GENERIC_READ` | (nothing) |
| `GENERIC_WRITE` | `SYSTEM_RELOAD_CONFIG` |
| `GENERIC_EXECUTE` | `SYSTEM_SHUTDOWN` |
| `GENERIC_ALL` | `SYSTEM_SHUTDOWN` \| `SYSTEM_RELOAD_CONFIG` |

The default grants **SYSTEM** full access and **Administrators** both rights. peinit loads this descriptor at boot and hot-reloads it on registry change, exactly like ServiceSecurity.

## The list command filters, it does not deny

`list` is access-control-aware in a quieter way: it returns only the services the caller has `SERVICE_QUERY_STATUS` on, and simply **omits** the rest. A caller with no query rights gets an empty list, not a denial. This means a low-privilege principal cannot even enumerate the services it cannot see — the existence of a service is itself information the descriptor controls.

## The boundaries that hold

A few invariants are worth stating outright, because they are what make this trustworthy:

- **peinit never bypasses AccessCheck for a control operation.** No backdoor, no override flag, no "trust localhost."
- **The descriptors are the only policy inputs.** peinit consults the ServiceSecurity and ControlSecurity descriptors and nothing else — not config files, not environment variables, not hardcoded lists.
- **One service's state is never exposed to another without a check.** `status` is per-service access-controlled; `list` filters.

## Where to start

For the *other* half of the security model — what a service can reach once it is running — read [Service identity and privileges](/peios/using-peios/services-and-jobs/identity-and-privileges.md).

For the commands these rights gate, read [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md).

For the descriptor and AccessCheck machinery itself, read [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md) and [Access decisions](/peios/security-fundamentals/access-decisions/overview.md).

---

# Jobs and operations

_Peios / Using Peios / Services & jobs_

> A job is one process execution; an operation is one requested action. Their lifecycles, types, and GUIDs — plus ad-hoc jobs submitted through JFS.

A [service](/peios/using-peios/services-and-jobs/defining-a-service.md) is a long-lived *definition*. But when you read a status output or query the [event log](/peios/security-fundamentals/auditing/overview.md), two shorter-lived objects show up alongside it: the **job** (one process execution) and the **operation** (one requested action). They are what make peinit observable — every run and every command carries a GUID you can follow. This page covers both, and the special case of **ad-hoc jobs**.

## Jobs: what actually ran

Every time peinit forks a process — a service's main binary, a [pre- or post-hook](/peios/using-peios/services-and-jobs/execution-environment.md), a [health check](/peios/using-peios/services-and-jobs/supervision.md), or an [ad-hoc run](#ad-hoc-jobs) — that single execution is a **job**, with its own GUID and exit result. If a service is the *what*, a job is *what actually ran*. A restart does not reuse a job; it creates a **new** one. The service always tracks its *current* job GUID, and a `status` query reports it.

Jobs come in five types:

| Type | Created for |
|---|---|
| `ServiceMain` | A service's main process. |
| `PreExecHook` | One `ExecStartPre` command. |
| `PostExecHook` | One `ExecStartPost` command. |
| `HealthCheck` | One health-check run. |
| `AdHoc` | A process submitted via [JFS](#ad-hoc-jobs). |

Their lifecycle is simpler than a service's, because a job only tracks a process — not policy:

| State | Meaning |
|---|---|
| `Created` | The job object exists but the process is not forked yet (pre-hooks may be running). |
| `Running` | The process is alive. |
| `Completed` | The process exited successfully. |
| `Failed` | The process failed — or peinit classified the job failed before fork (a parent-side setup error). |
| `Abandoned` | The process survived SIGKILL (D-state). |

A job record carries the things you would want for forensics: the resolved identity and a token summary, the image path and arguments, created/started/ended timestamps, the exit code or signal, the cgroup, the [failure cause](/peios/using-peios/services-and-jobs/the-service-lifecycle.md), and the `operation_id` that created it (`null` for ad-hoc jobs). The clean division of ownership is: the **service** owns policy (restart, dependencies, health schedule, current state); the **job** owns the execution facts (PID, exit, timestamps, identity, cgroup, log correlation).

### Retention and log correlation

peinit keeps only *active* jobs in memory. When a job reaches a terminal state it **emits a structured event** (through KMES) carrying the full record, then drops the job. peinit keeps **no** job history — [eventd](/peios/security-fundamentals/auditing/overview.md) is the historian, consuming those events from the kernel ring buffer. The lifecycle events are `job.created`, `job.started`, and `job.ended`.

Separately, all of a job's `stdout`/`stderr` is [forwarded to eventd](/peios/using-peios/services-and-jobs/output-and-logging.md) tagged with the job's GUID. That is what lets a query like "show me the logs for job X" return exactly that execution's output — not interleaved with the run before or after it.

## Ad-hoc jobs

An **ad-hoc job** is an arbitrary process a *service* asks peinit to run on behalf of one of its clients. It is not tied to a persistent definition — it runs once, reports its result, and is cleaned up. The motivating case: a service has [impersonated](/peios/security-fundamentals/impersonation/overview.md) a user and wants a process run *as that user*, but supervised centrally by peinit rather than by the service itself.

The obstacle is identity delegation: a service cannot simply forward an impersonation token over a socket. **JFS** (the Job Forwarding Subsystem) solves this in the kernel — it captures the caller's effective token and delivers it to peinit along with the job request. peinit forks, installs that captured token, and supervises the result like any other job.

> [!IMPORTANT]
> An ad-hoc job runs with **the submitter's own (possibly impersonated) token** — there is no identity field in an ad-hoc request. A caller cannot name an arbitrary identity; it can only pass through a token it already holds. This is what prevents escalation: a service cannot manufacture a job running as an identity it does not already possess.

An ad-hoc request carries a deliberately small subset of the service fields — `ImagePath`, `Arguments`, `Environment`, `WorkingDirectory`, `Timeout`, `Description`. A couple are worth pinning down: `Timeout` is in **seconds**, and `0` means *no limit* (the job runs for as long as it needs). `WorkingDirectory` defaults to `/` when you leave it out. The policy fields (restart, dependencies, health checks, `ErrorControl`, triggers) are **not** available — those belong to persistent definitions. An ad-hoc job runs once in its own cgroup, routes its output to eventd, and on exit emits `job.ended` and is dropped. If it overruns its `Timeout`, peinit escalates SIGTERM → SIGKILL exactly as a service stop does.

Ad-hoc jobs **bypass the operation model entirely** — there is no service to "start," so the request creates a job directly. The job *is* the whole lifecycle.

> [!NOTE]
> JFS is a kernel subsystem with its own byte-level interface, still being specified. peinit is its consumer; the *shape* of the protocol is settled, but the wire details are owned by JFS, so this path is not fully implementable from the peinit specification alone.

## Operations: what was requested

An **operation** is a first-class object representing a *requested state-machine action* on a service. Rather than letting [control commands](/peios/using-peios/services-and-jobs/controlling-services.md) mutate state directly, peinit turns each one into an operation that is validated, queued, and executed by its event loop. This is what gives concurrent callers — admin tools, automated triggers, dependency propagation — explicit, observable conflict resolution instead of races.

There are five operation types — `Start`, `Stop`, `Restart`, `Reload`, `Reset` — and each carries a **source** recording *why* peinit created it:

| Source | Created by |
|---|---|
| `Admin` | A control client. |
| `Boot` | The Phase 2 boot lifecycle. |
| `Shutdown` | The shutdown lifecycle. |
| `DependencyPropagation` | A start pulling in a [dependency](/peios/using-peios/services-and-jobs/dependencies.md). |
| `RestartPolicy` | An automatic [restart](/peios/using-peios/services-and-jobs/supervision.md). |
| `Timer` | A [timer](/peios/using-peios/services-and-jobs/triggers-and-timers.md) firing. |
| `BindsToRecovery` | A bound target returning to Active. |
| `BindsToPropagation` | A bound target stopping. |
| `ConflictResolution` | A `Conflicts` eviction. |
| `OnFailure` | A failed service's fallback handler. |

The source is why a `status` showing `current_operation.source: "restart_policy"` tells you the service is being auto-restarted, while `"admin"` tells you a person asked.

### Operation lifecycle

```mermaid
flowchart LR
    P["Pending"] --> R["Running"]
    R --> C["Completed"]
    R --> F["Failed"]
    R --> A["Aborted"]
    P --> M["Merged"]
    P --> X["Cancelled"]
    P --> F
```

| State | Meaning |
|---|---|
| `Pending` | Validated and queued, waiting on a precondition (e.g. a prior stop to finish). |
| `Running` | Executing — the service is transitioning. |
| `Completed` | Achieved its goal (start reached Active/Completed; stop reached Inactive/Failed). |
| `Failed` | Did not achieve its goal — including timing out while still Pending. |
| `Merged` | Folded into an identical operation already in flight (records the survivor's GUID). |
| `Cancelled` | Terminated while still Pending — never ran. |
| `Aborted` | Terminated while Running — interrupted in progress. |

`Cancelled` and `Aborted` are the same idea at two points: cancelled never ran, aborted was running. The *reason* (admin action, supersession) is a property of the event, not the state.

### Conflict resolution and merging

When a command arrives for a service that already has an operation in flight, peinit resolves the collision deterministically — the same logic the [command × state matrix](/peios/using-peios/services-and-jobs/controlling-services.md) summarises:

- **Same type** (start over start, stop over stop, reload over reload) → **merge**. The new caller transparently receives the existing operation's GUID; from their side, their request is in progress.
- **Stop wins over start.** An explicit stop cancels a pending start or aborts a running one. The admin said stop.
- **Later supersedes earlier**, and a start while a stop is in flight is **queued** to run after the stop.

### Timeout and retention

An operation inherits its target's timeout as its maximum lifetime — `StartTimeout` for start/reload/reset, `StopTimeout` for stop, and the sum of both legs for restart. Crucially, **the clock starts at operation creation, including queue time** — from the caller's perspective they have been waiting since they sent the command, so a long queue can time an operation out before it even runs.

Terminal operations are emitted as events (`operation.requested`, `.started`, `.completed`, `.failed`, `.cancelled`, `.merged`, `.aborted`) and dropped from memory after a short grace (default 60 s — long enough for a polling client to read the result). As with jobs, peinit keeps no operation history; eventd does.

## How they surface

You meet jobs and operations in three places:

- **`status`** — `current_job` and `current_operation` give the GUIDs of the service's active execution and active action (or `null`).
- **`operation-status <id>`** — polls one operation to a terminal state; a `--wait` lifecycle command does this for you under the hood.
- **[eventd](/peios/security-fundamentals/auditing/overview.md)** — the durable history. Every job and operation lifecycle transition lands there as a structured event, which is how you reconstruct what happened after the objects themselves are gone from peinit's memory.

## Where to start

To create and poll operations from the command line, read [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md).

To understand the service states an operation drives a service through, read [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md).

To query the durable job and operation history, read [Auditing](/peios/security-fundamentals/auditing/overview.md).

---

# Service output and logging

_Peios / Using Peios / Services & jobs_

> peinit captures every service's stdout and stderr, buffers before eventd exists, and forwards after — best-effort logs, guaranteed audit events.

peinit is **not** a logging system — storage, indexing, and queries belong to [eventd](/peios/security-fundamentals/auditing/overview.md). But peinit holds the `stdout`/`stderr` pipes of every service at the moment it forks, so it is unavoidably in charge of *where output goes*, and it has to cope with the window early in boot before eventd even exists. This page is about that responsibility.

## Capturing output

When peinit forks a service it creates pipes for `stdout` and `stderr`, redirects the child's streams to the write ends, and keeps the read ends — monitoring them in its event loop. (The child's `stdin` is `/dev/null`; see [The execution environment](/peios/using-peios/services-and-jobs/execution-environment.md).) It reads output **line by line** and tags each line with:

- the **service name**,
- the **stream** (stdout or stderr),
- a **timestamp**,
- the **[job](/peios/using-peios/services-and-jobs/jobs-and-operations.md) GUID**.

The job GUID is the important one: it is what lets a later query return exactly one execution's output, never interleaved with the run before or after it. Hook output is captured too, labelled with the hook (e.g. `jellyfin/ExecStartPre[0]`), and so is health-check output — invaluable for diagnosing *why* a check failed.

## Logs are best-effort; audit events are not

This is the distinction to internalise, because it explains why some things survive a crash and others do not. peinit handles **two different streams of information** with two different guarantees:

| | Service logs | Audit events |
|---|---|---|
| What | `stdout`/`stderr` from services and hooks | Access denials, critical failures, recovery-mode entry, graph errors, job/operation lifecycle |
| Path | Forwarded to eventd's log socket | Emitted into the **KMES** kernel ring buffer |
| Guarantee | **Best-effort** — may be dropped under load | **Durable** — survive eventd restarts and reboots |
| Available from | Once eventd is up (buffered before) | The moment PKM loads — before eventd, before the registry |

Service logs are a convenience: useful, but lossy by design. **Audit events are part of the security posture** and go through [KMES](/peios/security-fundamentals/auditing/events-and-transport.md), which persists them in the kernel from the instant the module loads. So peinit keeps a pre-eventd *buffer* for logs but needs none for events — eventd picks events up from the ring buffer whenever it attaches, no matter how late.

## The pre-eventd buffer

Before eventd is running there is nowhere to send logs — its socket does not exist yet. peinit therefore buffers captured output in a fixed-size in-memory **pre-eventd buffer** (default 1 MB); when it fills, the oldest entries are dropped.

In practice the only services that run before eventd are `registryd`, `lpsd`, `authd`, and `eudev`. The first three are Peios-owned with controlled output; `eudev` is the real overrun risk (verbose device enumeration), and the [restart budget](/peios/using-peios/services-and-jobs/supervision.md) naturally bounds output from a crash loop.

## Flood protection

A noisy service must never starve peinit's event loop. Several limits enforce that:

| Key (`Machine\System\Init\`) | Default | Limits |
|---|---|---|
| `MaxLogLineLength` | 8192 | Bytes per line; longer lines are truncated with a `[truncated]` marker. |
| `MaxLogBufferPerService` | 65536 | Bytes buffered per service pipe before back-pressure. |

Two mechanisms back these up:

- **A per-iteration read budget.** Each pass of the event loop reads at most a bounded number of bytes from service pipes before moving on to other events — so no single chatty service can monopolise a loop iteration.
- **Back-pressure, not dropping.** If a service writes faster than peinit reads, the kernel pipe buffer fills and the service's own `write()` calls block. This is deliberate: the service slows down. While reading the pipe, peinit does **not** drop output — the no-silent-drop guarantee covers *reading the pipe*. It is only *downstream* (the pre-eventd buffer, and eventd's datagram socket) that delivery becomes lossy.

> [!NOTE]
> Signals are never starved. In every loop iteration, child reaping (SIGCHLD) and shutdown handling take priority over all other event sources, including log reading. A flood of service output can never delay peinit noticing that a service died or that the system is shutting down.

## The eventd handoff

When eventd reaches Active, peinit switches from buffering to forwarding:

1. It begins sending to eventd's log datagram socket (path from `Machine\System\eventd\LogSocketPath`).
2. It **replays the pre-eventd buffer**, oldest first, preserving each line's timestamp and metadata. This replay is best-effort — the records are datagrams on a loss-tolerant socket and some may be dropped under load; peinit never blocks waiting to deliver them.
3. It switches to real-time forwarding — new output goes out as it arrives.
4. It clears the buffer.

From then on peinit is a pipe relay: read a line, tag it, forward it as a datagram. The log socket is **non-blocking and loss-tolerant** — if eventd cannot drain it fast enough the kernel drops further datagrams silently, because log ingestion must never exert back-pressure on the whole system through PID 1. peinit keeps no unbounded outbound buffer and never blocks on a send to eventd.

If eventd itself crashes after starting, peinit (which supervises it like any service) notices, **re-enables the pre-eventd buffer**, and repeats the handoff when eventd comes back. There is a log gap across the restart, bounded by the buffer size — but audit **events** are unaffected, because they were going to KMES the whole time and eventd resumes consuming them from the last persisted sequence.

## Console output

peinit writes its **own** operational messages to `/dev/console`:

- Phase 1 progress (mount results, registryd start),
- Phase 2 progress (service starts and failures, dependency errors),
- shutdown progress,
- recovery-mode entry,
- critical service failures.

Service `stdout`/`stderr` is **not** echoed to the console by default — the console is for peinit's status messages only. This keeps the console legible during boot and, crucially, usable in [Recovery mode](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md), where it may be the only interface an administrator has.

## Where to start

To see how the pipes are wired and what else the process inherits, read [The execution environment](/peios/using-peios/services-and-jobs/execution-environment.md).

To follow the job GUID that tags every log line, read [Jobs and operations](/peios/using-peios/services-and-jobs/jobs-and-operations.md).

For where the logs and events actually go — storage, indexing, and queries — read [Auditing](/peios/security-fundamentals/auditing/overview.md).

---

# Boot and boot modes

_Peios / Using Peios / Services & jobs_

> The two-phase boot — hardcoded bootstrap, then registry-driven — and the Full, Safe, and Recovery modes with the boot-attempt counter between them.

peinit's boot has two phases and a chicken-and-egg problem to solve. The problem: peinit reads everything it does from the [registry](/peios/using-peios/registry-concepts/overview.md), but the registry is served by a daemon that something has to start first — and the identity authority that would mint that daemon's token does not exist yet either. The solution is to split boot into a **hardcoded** bootstrap that needs no registry, and a **registry-driven** phase that starts once the registry is up. The boundary between them is `registryd`.

## Where peinit takes over

peinit is PID 1, but it is not the *first* thing that runs. The [initramfs](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) assembles and mounts the real root — decryption, RAID/LVM, the root filesystem itself — and then hands control to peinit. The contract peinit relies on is narrow:

- The real root is already mounted **read-write**, and `/proc`, `/sys`, `/dev` are mounted and moved into it.
- peinit is exec'd as PID 1 from a fixed path on the real root.

peinit **does not** assemble, decrypt, repair, or even re-mount the root — those need tools and configuration that belong to the initramfs. It also does not `fsck` the root or mount non-root storage (a data partition is mounted by an ordinary Oneshot service, not by peinit). For the trust and identity side of this handoff — signatures, the SYSTEM token peinit inherits — see [peinit at PID 1](/peios/security-fundamentals/boot-and-trust-establishment/peinit-pid-1.md).

## Phase 1: the hardcoded bootstrap

Phase 1 is compiled into peinit. It cannot change at runtime and touches no registry. It does the minimum to make Phase 2 possible:

1. **Confirm the root is writable** with a single probe write. registryd's storage needs a writable root even for reads, so a read-only root cannot support Phase 2 → Recovery.
2. **Mount the remaining virtual filesystems** — `/dev/pts`, `/dev/shm`, `/run`, `/sys/fs/cgroup` — mounting each only if absent. A failure here → Recovery.
3. **Restore the persisted random seed** from `/var/state/peinit/random-seed`, mixing it into the kernel's entropy pool early so anything that needs randomness during boot gets it. A missing seed is normal — first boots and stateless live boots have none — so peinit just carries on; a seed problem is never fatal and never sends boot to Recovery.
4. **Establish the machine-id** from `/lcl/etc/machine-id` — a stable, opaque identifier for this install (used for log correlation, instance identity, and software compatibility). It is *not* a security principal: it is not a SID, an account, or a credential, and no authorisation decision depends on it. If the file is missing, empty, or malformed, peinit generates a fresh 128-bit ID and writes it before continuing.
5. **Set the clock from the hardware RTC**, so early timestamps and the boot counter are meaningful. A failure here → Recovery.
6. **Start registryd** and wait for it to signal readiness, then **probe-read** the schema-version key to confirm it is actually serving reads. Any failure → Recovery — there is no Phase 2 without a registry.
7. **Provision boot-time paths.** With registryd up and before Phase 2 starts, peinit applies the entries under `Machine\System\Init\ProvisionedPaths\` — the registry-driven equivalent of tmpfiles.d, creating directories and files (with Peios security descriptors) that no single service owns. Best-effort entries that fail are logged and skipped, but an entry marked `Required=1` that cannot be provisioned sends boot → Recovery. The individual keys are cataloged in the [registry key reference](/peios/using-peios/services-and-jobs/registry-key-reference.md).
8. **Infrastructure setup** — create the [control socket](/peios/using-peios/services-and-jobs/controlling-services.md), open the JFS device for [ad-hoc jobs](/peios/using-peios/services-and-jobs/jobs-and-operations.md), bring up the loopback interface. Control-socket failure → Recovery; the other two are logged as warnings and boot continues.

Most Phase 1 failures are fatal to a normal boot, because none of the later machinery can run without this foundation — the only outcome is [Recovery mode](#recovery-mode). The exceptions are the fail-soft steps called out above: a missing or unusable random seed, a regenerated machine-id, and best-effort provisioned paths all let boot continue.

> [!CAUTION]
> Packaged images, VM templates, and live ISOs must not ship a populated `/lcl/etc/machine-id` or a `/var/state/peinit/random-seed` file. A shipped machine-id gives every clone the same identity, and a shipped seed is a public value that is not acceptable entropy. Clone and reset tooling should remove or truncate `/lcl/etc/machine-id` so peinit generates a fresh ID on the next boot, and should never bake a seed into the image — if you need strong first-boot randomness for a stateless image, provide a real kernel entropy source (hardware RNG or virtio-rng) instead.

### registryd and loregd

`registryd` is an **interface**, not a specific program. It is the path peinit execs to get a registry source daemon — the component that implements the registry's persistent storage and answers peinit's reads. The *implementation* behind that interface can vary; by default it is **loregd**.

This split matters in exactly one place: **Recovery mode**. In normal operation you only ever deal with the `registryd` abstraction — peinit starts it, treats it as opaque, and reads the registry through it. But when the registry *itself* is what broke, you need tools that work *without* a running registry, and those tools talk to the implementation directly. That is why the recovery tooling is named `loregd` (`loregd --inspector`, `--recover-from-backup`, …): in recovery you are working with the storage implementation, not the registry abstraction. It is the one context where the distinction is visible to an administrator.

## Phase 2: the registry-driven boot

With registryd serving reads, peinit boots the rest of the system from the registry:

1. **Read all definitions** under `Machine\System\Services\`. (A registry read timing out here → Recovery.)
2. **Build and validate the dependency graph** from the boot-triggered services and their transitive [dependency closure](/peios/using-peios/services-and-jobs/dependencies.md). Validation runs *before* anything starts.
3. **Start services in dependency order**, in [parallel](/peios/using-peios/services-and-jobs/dependencies.md) up to `MaxParallelStarts`, with [readiness gating](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) releasing each service's dependents as it becomes satisfied.

Only services with a `boot` trigger are start candidates; demand-only services are pulled in only if something boot-triggered depends on them, and [Disabled](/peios/using-peios/services-and-jobs/triggers-and-timers.md) services are excluded from the graph (but kept in the model for on-demand start). The whole boot runs against one [snapshot](/peios/using-peios/services-and-jobs/defining-a-service.md) — mid-boot registry edits do not perturb it.

The platform daemons come up first because everything rests on them. They are all SYSTEM, all minted by peinit (no authd yet), and all `ErrorControl=Critical`:

| Service | Phase | Identity source | Readiness | ErrorControl |
|---|---|---|---|---|
| registryd | 1 | minted by peinit | sd_notify | Critical |
| eudev | 2 | minted by peinit (privileges stripped) | process alive | Normal |
| lpsd | 2 | minted by peinit | sd_notify | Critical |
| authd | 2 | minted by peinit | sd_notify | Critical |
| eventd | 2 | minted by peinit | sd_notify | Critical |
| networking | 2 | authd | sd_notify | Normal |
| sshd | 2 | authd | process alive | Normal |
| application services | 2 | authd | per-service | Normal |

Once authd is up, every subsequent service gets its token through the [normal authd flow](/peios/using-peios/services-and-jobs/identity-and-privileges.md). The order above is *emergent* from the standard role definitions' dependencies, not hardcoded — change the dependencies and the order changes. In practice login services such as `sshd` come up last, so the system is fully operational before it starts accepting user sessions.

## The three boot modes

The three modes form an escalation path from normal operation to last-resort maintenance.

```mermaid
flowchart TD
    F["Full boot"] -->|success| OK["operational<br/>counter reset"]
    F -->|cycle/conflict w/ Critical| S["Safe mode<br/>(no reboot)"]
    F -->|Critical failure| RB["sync + reboot"]
    S -->|success| OKS["operational (reduced)<br/>counter reset"]
    S -->|Critical failure| RB
    RB --> CNT["counter increments"]
    CNT -->|counter ≥ N| REC["Recovery mode"]
    F -.->|Phase 1 failure| REC
```

### Full mode

The default. All boot-triggered services start in dependency order. A boot is **successful** — and the [boot-attempt counter](#the-boot-attempt-counter) resets to 0 — when every Critical service has reached and *held* a dependent-satisfying state (Active, Completed, or Skipped) for a grace period (`BootSuccessGrace`, default 30 s). The criterion is "satisfying," not "Active," so that a Critical Oneshot that reaches Completed can still mark the boot good.

### Safe mode

Safe mode starts a **reduced** set of boot-triggered services and rebuilds the graph from scratch using only those, dropping dependencies on excluded services. Two categories are eligible:

- **Critical services** (`ErrorControl=Critical`) — must start; a Critical failure here still follows the normal reboot path.
- **SafeMode services** (`SafeMode=1`) — best-effort; if they fail, Safe mode carries on without them.

Everything else is skipped. Safe mode is entered when:

- graph validation finds a **cycle involving a Critical service**, or
- an **unresolvable conflict involving a Critical service**, or
- the kernel command line says `peios.safemode=1`.

The first two are *configuration* errors — rebooting would just hit them again — so peinit downgrades **without** rebooting. Safe mode is **not** entered by a Critical service *crashing at runtime*; that follows the reboot path. And it is purely a boot-sequencing concern: once booted, you can start any service by hand exactly as in Full mode. It is the mode for "the configuration is broken but the TCB is healthy."

> [!NOTE]
> `SafeMode=1` and `ErrorControl=Critical` are *filters within the boot-triggered set*. They do not make a demand-only service auto-start in Safe mode — a service with no `boot` trigger stays demand-only regardless.

### Recovery mode

Recovery mode is the last resort, and it offers **no TCB guarantee** — the administrator gets an unrestricted SYSTEM shell on the console and must treat it with corresponding care. It is a maintenance *environment*, not a degraded boot.

In Recovery, peinit completes the Phase 1 basics, *tries* to start registryd (ignoring failure — the shell must appear regardless), skips all Phase 2 services, and execs a SYSTEM shell on `/dev/console` (`/bin/recsh` if present, else `/bin/sh`), respawning it if it exits. It is entered when:

- the **boot-attempt counter reaches N** (default 3),
- the kernel command line says `peios.recovery=1`, or
- **registryd fails during Phase 1** — entered immediately, with no reboot and no counter increment, because there is no Phase 2 to attempt.

Because the registry itself may be what broke, recovery provides tools that work without it — talking to the [loregd implementation](#registryd-and-loregd) directly:

| Tool | Does |
|---|---|
| `loregd --inspector` | Read the storage database directly for diagnosis. |
| `loregd --recover-from-backup` | Restore from an automatic backup taken on every registryd startup. |
| `loregd --dangerously-clear-database` | Wipe the registry entirely. Recoverable, because role definitions are the source of truth for service config. |

> [!IMPORTANT]
> Recovery mode requires console access — physical, IPMI, or serial. There is no remote recovery in the current design (emergency SSH and registry historical reversion are noted as post-v1 work). Plan console access for any machine you need to be able to recover.

## The boot-attempt counter

The counter is what turns a crash-looping Critical service into an eventual Recovery shell instead of an infinite reboot loop. It is a plain integer in a file at `/.peinit/boot-attempts` — *not* in the registry, because the registry may be the very thing that is broken.

- peinit **reads** it at startup, before choosing a mode. The Recovery threshold (`counter ≥ N`) is checked against this pre-increment value, so the default N of 3 admits exactly three attempts before Recovery. A missing file counts as 0; a corrupt or unreadable one → Recovery. Override N with `peios.bootattempts=N` on the kernel command line, or set it to `0` to disable the check when the counter is itself the fault.
- peinit **increments** it once per boot, right after confirming the root is writable and before Phase 2 — never before the root is known writable, or a read-only root would silently lose the increment and defeat escalation.
- The counter is **reset to 0** on a successful Full or Safe boot (after the grace period).
- If the counter file cannot be *written* (disk full), peinit treats it as 0 and continues — a write failure must not by itself trigger Recovery.

A [Critical service](/peios/using-peios/services-and-jobs/supervision.md) exhausting its restart budget — at boot or at runtime — triggers a sync and reboot, which increments the counter on the next boot. Repeat that enough and the counter crosses N, and peinit stops trying and hands you a Recovery shell. The counter deliberately does *not* try to catch a peinit too broken to reach its own increment; that is a binary-integrity problem, not a boot-loop problem.

## Boot configuration

| Key | Default | Controls |
|---|---|---|
| `Machine\System\Boot\MaxParallelStarts` | 10 | Services starting concurrently (must be > 0; invalid → Recovery). |
| `Machine\System\Boot\BootSuccessGrace` | 30 | Seconds a Critical service must hold a satisfying state before boot counts as successful. |
| `Machine\System\Boot\ShutdownTimeout` | 90 | Maximum seconds for the whole [shutdown](/peios/using-peios/services-and-jobs/shutdown.md) sequence. |
| `Machine\System\Boot\PostKillTimeout` | 5 | Seconds a service cgroup may take to drain after SIGKILL before it counts as stuck. |

## The kernel command line

peinit reads four `peios.*` tokens. They are deliberately few: everything peinit can read *after* registryd is serving belongs in the registry instead, where it can be inspected, secured and changed without editing a boot entry. What is left is either a per-boot mode decision or a Phase 1 value — one peinit needs before there is a registry to ask.

| Token | Effect |
|---|---|
| `peios.safemode=1` | Force [Safe mode](#safe-mode). |
| `peios.recovery=1` | Force [Recovery mode](#recovery-mode), whatever the counter says. |
| `peios.bootattempts=N` | Override the boot-attempt threshold; `0` disables the check. |
| `peios.notifysocket=PATH` | Move the sd_notify socket. Phase 1, because registryd is the first service to use it and it must be bound before registryd starts. |
| `peios.quiet=N` | How much peinit may write to the console: `0` write everything, `1` (default) stay out of a terminal a service owns, `2` also drop ordinary progress. See below. |

### Console noise: `peios.quiet`

peinit reports its progress to `/dev/console`, and so does any service that claims that terminal with [`TTYPath`](/peios/using-peios/services-and-jobs/execution-environment.md). Two writers, one device: a login prompt started while peinit is still narrating gets written over, and the reader cannot tell input from log. `peios.quiet` decides who yields.

It sets **two independent rules**, which is why it is a level and not a flag:

| | Terminal a service owns | Everywhere else |
|---|---|---|
| `peios.quiet=0` | peinit writes anyway | everything |
| `peios.quiet=1` *(default)* | only messages that mean the machine is about to be lost | everything |
| `peios.quiet=2` | only messages that mean the machine is about to be lost | errors only |

The rules **stack rather than scale**: errors are never less visible at `2` than at `1`. An error overrides a requested blackout — silence was a preference, and an error is news — but it does not override terminal ownership, which is not peinit's to override. Only losing the machine (dropping to [Recovery](#recovery-mode), halting with no shell, a Critical service about to force a reboot) is worth one corrupted line of somebody else's session.

[prelude](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) honours `peios.quiet=2` as well, since it writes to the same console. The other two levels mean nothing there — prelude exits before the first service starts, so no terminal has an owner yet.

> [!WARNING]
> Suppressed messages are **dropped, not stored**. Until peinit can forward them to [eventd](/peios/security-fundamentals/auditing/overview.md), `peios.quiet=1` means you lose peinit's narrative from the moment a login prompt appears. On a machine you are debugging, boot with `peios.quiet=0`.

A few lines escape all of this: whatever peinit and prelude print *before* they have read the command line. Neither can honour a preference it has not seen yet, and those lines are also the only evidence either started at all.

Unknown `peios.*` tokens are ignored, as is a malformed value on either of the two valued tokens — this parser runs before anything exists to report a diagnostic to, and refusing to boot over a typo in a tuning knob is the worse outcome.

> [!NOTE]
> There is no token that selects services. Which services start is decided entirely by what is defined under `Machine\System\Services` — see [Defining a service](/peios/using-peios/services-and-jobs/defining-a-service.md). A console shell or a login prompt is an ordinary service definition with a [`TTYPath`](/peios/using-peios/services-and-jobs/execution-environment.md), not a boot flag.

## Where to start

To understand the graph validation and parallel start that drive Phase 2, read [Dependencies and ordering](/peios/using-peios/services-and-jobs/dependencies.md).

To understand the Critical-failure reboot path that feeds the boot counter, read [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

For the trust and token side of early boot, read [peinit at PID 1](/peios/security-fundamentals/boot-and-trust-establishment/peinit-pid-1.md).

---

# Shutdown

_Peios / Using Peios / Services & jobs_

> Shutdown is boot in reverse — SIGTERM to SIGKILL in reverse dependency order, bounded by a global timeout, then seed, unmount, sync, and power off.

Shutdown is [boot](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) run in reverse. peinit stops services in **reverse [dependency](/peios/using-peios/services-and-jobs/dependencies.md) order** — dependents before the things they depend on — escalating from a polite SIGTERM to a forced SIGKILL, all bounded by a global timeout, and then unmounts, syncs, and performs the final power action. This page covers how it is triggered and how it runs.

## How shutdown is triggered

There are four paths into shutdown, and they are not equal — three are graceful, one is not.

**A control command.** An administrator with `SYSTEM_SHUTDOWN` runs `peiosctl shutdown <type>`:

| Type | Result |
|---|---|
| `poweroff` | Stop everything, unmount, power off. |
| `reboot` | Stop everything, unmount, reboot. |
| `halt` | Stop everything, unmount, halt (CPU stopped, power stays on). |

**Signals.** As PID 1, peinit treats a handful of signals as shutdown requests:

| Signal | Meaning |
|---|---|
| SIGINT | Reboot — the kernel sends this on Ctrl-Alt-Del. |
| SIGTERM | Poweroff. |
| SIGPWR | Poweroff — a compatibility path for environments that surface power failure or power-button policy as a signal. |

**The power button.** On Linux, a physical power-button press is a graceful `poweroff` — peinit watches the machine's input devices under `/dev/input` and treats a `KEY_POWER` press (the key going down, not its release or auto-repeat) as a shutdown request. This path is deliberately minimal and **fail-soft**: if `/dev/input` is missing, an input device cannot be opened, or a device stops responding, peinit just keeps running and drops that device — the button quietly stops working, but the machine stays up and you can still shut down over the control socket or with a signal. It is not a full power-management policy engine; a future acpid/logind-style daemon can layer richer policy on top by sending control-socket commands.

**A Critical service failure** — the ungraceful path. When an `ErrorControl=Critical` service exhausts its [restart budget](/peios/using-peios/services-and-jobs/supervision.md), peinit **syncs and reboots immediately**. There is no service-stop ordering: the system is in an undefined state and the fastest route to a known one is a reboot. This is what feeds the [boot-attempt counter](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md), and a Critical service that fails identically every boot becomes a reboot loop that the counter eventually breaks by escalating to Recovery.

## The graceful sequence

For a `poweroff`, `reboot`, or `halt`, peinit runs an ordered sequence:

1. **Enter the shutdown state.** A flag is set, and while it holds: **no new services may start**, timer triggers are disarmed, and new control commands are rejected with `INVALID_STATE` — *except* `status` queries, which keep working so you can watch progress.
2. **Suspend Critical-failure semantics.** If a Critical service fails *during* shutdown, it is logged but does **not** trigger a reboot — the system is already going down, and rebooting would loop.
3. **Clear Completed services.** Oneshot services sitting in [Completed](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) (with `RemainAfterExit`) have no process; peinit moves them to Inactive so their dependents can be stopped cleanly.
4. **Stop services in reverse dependency order**, in waves. Each graceful-stop-eligible service (those in Active or Reloading) gets SIGTERM and `StopTimeout` seconds to exit; if it does not, peinit SIGKILLs its whole cgroup. A service is never stopped until everything that `Requires` or `BindsTo` it has already stopped. Services that are Starting are not stopped gracefully — their startup is cancelled and the cgroup SIGKILLed.
5. **Enforce the global timeout.** The whole sequence is bounded by `ShutdownTimeout` (default 90 s). When it expires, all remaining services are SIGKILLed; any whose cgroups do not empty within the post-kill timeout (default 5 s) become [Abandoned](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) (leaked), and shutdown continues regardless.
6. **Save the random seed.** With every service stopped and before any filesystem is touched, peinit writes a fresh seed drawn from the kernel's CSPRNG to `/var/state/peinit/random-seed` — this is the entropy the *next* boot starts from. The write is crash-conscious (written to a temporary file, flushed, then atomically swapped in), and it is best-effort: if it fails, peinit logs it and carries on. Shutdown never stalls on it, and the forced and Critical-reboot paths skip it entirely in favour of getting the machine down fast.
7. **Unmount filesystems.** peinit snapshots the mount table and tries to unmount **every** remaining non-root mount in its namespace — not just the ones it mounted itself — working from the deepest mount points outward. That includes the Phase 1 set (`/proc`, `/sys`, `/dev`, `/dev/pts`, `/dev/shm`, `/run`, `/sys/fs/cgroup`) wherever those are still mounted. A mount that is already gone counts as done. If one won't unmount because it is busy, peinit falls back to remounting it read-only; anything it still can't clean up is logged and left for the final `sync()` and the kernel, never blocking shutdown. The root filesystem (`/`) is never unmounted — once the rest are handled, peinit remounts root read-only.
8. **Sync and finish.** `sync()` to flush pending writes, then the final action — power off, reboot, or halt.

The reverse ordering falls straight out of the [dependency graph](/peios/using-peios/services-and-jobs/dependencies.md); there is no separate stop-order configuration. The last services to stop are the TCB daemons everything rests on — `eventd`, then `authd`, then `lpsd`, then `registryd` dead last, mirroring its position as the first ever started.

## STOPPING=1 and timeout extension

A service that begins shutting down on its own — say, in response to an internal error — can tell peinit by sending `STOPPING=1` via [sd_notify](/peios/using-peios/services-and-jobs/service-types.md). peinit acknowledges it and, importantly, **stops sending SIGTERM** to that service: it is already on its way down, and a redundant signal could interfere.

`STOPPING=1` is a courtesy, **not** a timeout extension. The `StopTimeout` keeps running; if the process has not exited when it expires, peinit escalates to SIGKILL regardless. A service that genuinely needs longer must say so with `EXTEND_TIMEOUT_USEC` (see [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md)) — and during shutdown those extensions are capped not only by the per-service `StopTimeout` × 4 but also by the remaining global `ShutdownTimeout`, whichever is smaller.

## Forced shutdown

When something is wedged and you need the machine down *now*, **repeated SIGINT** forces it: three Ctrl-Alt-Del presses within five seconds skip the graceful sequence entirely — SIGKILL everything, sync, reboot. It is the escape hatch for a graceful shutdown that is itself stuck behind an unresponsive service.

## Shutdown during boot

A shutdown requested while peinit is still in Phase 2 boot takes effect immediately: services still Starting are SIGKILLed (they never reached Active, so there is nothing to stop gracefully), services that did reach Active are stopped per the normal sequence, and the boot is abandoned.

## How peinit handles signals

peinit handles **all** signals through a `signalfd` read from its event loop — every signal is blocked and read as data, so there are no async signal handlers and no async-safety hazards. PID 1 cannot be killed by any signal; the kernel protects it.

| Signal | Behaviour |
|---|---|
| SIGCHLD | Reap children; match exits to services and jobs. Also reaps orphaned processes the system reparented to PID 1. |
| SIGINT | Reboot request. Repeated within the window → forced reboot. |
| SIGTERM | Poweroff request. |
| SIGPWR | Poweroff request — compatibility path for power-button/power-failure policy. |
| SIGHUP | Ignored — PID 1 has no controlling terminal. |
| SIGPIPE | Ignored — a broken control-socket pipe must never crash PID 1. |

All other signals are ignored.

> [!NOTE]
> Reaping orphans matters: when any process anywhere on the system dies leaving children, those children are reparented to PID 1. peinit reaps them so they do not accumulate as zombies — even though they were never its services. This is part of the job of being PID 1, distinct from supervising services.

## Where to start

To understand the reverse ordering and which relationships gate it, read [Dependencies and ordering](/peios/using-peios/services-and-jobs/dependencies.md).

To understand the Critical-failure path that bypasses graceful shutdown, read [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

For the boot side of the lifecycle and the reboot/recovery escalation, read [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md).

---

# Troubleshooting peinit

_Peios / Using Peios / Services & jobs_

> A symptom-first guide — a service that won't start or keeps restarting, a change that didn't land, an access denial, and a machine stuck rebooting.

Almost every peinit problem is diagnosed the same way: **read the state and the cause.** A `status` query gives you both, and the [cause taxonomy](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) tells you what the cause means. This page works backward from common symptoms to the cause and the fix.

```
$ peiosctl status <service>      # state + cause + health + warnings
$ peiosctl list                  # everything you can see, at a glance
```

For history beyond what peinit holds in memory — past jobs, prior failures, the exact log line a service died on — query [eventd](/peios/security-fundamentals/auditing/overview.md); peinit emits every [job and operation](/peios/using-peios/services-and-jobs/jobs-and-operations.md) transition there.

## A service won't start

`status` shows it `Failed` or `Skipped`. The **cause** says why:

| Cause | What happened | What to do |
|---|---|---|
| `ValidationError` | The definition is malformed — a bad field, an unresolvable conflict, an illegal health-check timing. | Check the logs for the specific field. Fix the definition; the message names what is wrong. |
| `DependencyFailure` | A `Requires`/`BindsTo` target failed or does not exist. | Fix the *target* first — `status` it. The dependent recovers once the target can start. |
| `ConditionSkipped` (→ Skipped) | A start-time [condition](/peios/using-peios/services-and-jobs/execution-environment.md) was not met. **Not a failure** — the service decided it does not apply here. | If it *should* run, the condition's premise is false (a missing path/file/key). This is often correct behaviour. |
| `AssertionError` | A start-time [assert](/peios/using-peios/services-and-jobs/execution-environment.md) failed — a required precondition is missing. | Create the missing precondition (path, file, directory, registry key), then start again. |
| `PreHookFailure` | An `ExecStartPre` hook exited non-zero. | Run the hook command by hand as the [hook identity](/peios/using-peios/services-and-jobs/identity-and-privileges.md) to see why. |
| `ParentSetupFailure` | peinit could not even fork — fd/PID exhaustion, a cgroup error. No process was created. | A system-resource problem, not the service's fault. Check for fd/PID limits and cgroup health. |
| `PreExecFailure` | Setup after fork failed — token install, rlimits, environment. | Usually identity: check `Identity`, that authd is up, and that `RequiredPrivileges` names real privileges. |
| `ReadinessTimeout` | The process started but never became [ready](/peios/using-peios/services-and-jobs/service-types.md) within `StartTimeout`. | Either the service is genuinely slow (raise `StartTimeout`, or have it send `EXTEND_TIMEOUT_USEC`) or it never sends `READY=1` (wrong `Readiness`, or a bug). |

> [!TIP]
> A startup failure (`ReadinessTimeout`, `PreHookFailure`, `PreExecFailure`, `ParentSetupFailure`) is **restart-eligible** — peinit will have retried it through [Backoff](/peios/using-peios/services-and-jobs/supervision.md) before landing in Failed. So by the time you see `Failed`, it has already failed repeatedly. The logs show each attempt.

## A service keeps restarting

The service flaps — up, down, up, down. This is [restart policy](/peios/using-peios/services-and-jobs/supervision.md) doing its job, but it points at an unstable service.

- **It eventually settles in `Failed` with `RestartBudgetExhausted`.** It crashed `RestartMaxRetries` times faster than it could stay healthy for `RestartWindow`. The fix is the *service*, not the policy — read its logs for the crash. Raising the budget only delays the inevitable.
- **It flaps forever and never exhausts the budget.** It is briefly reaching Active each cycle and resetting the counter. Either genuinely stabilise it, or — if a [health check](/peios/using-peios/services-and-jobs/supervision.md) is failing it after it starts — check the health command; a flaky check restarts a perfectly good service.
- **It restarts on a clean exit you did not expect.** `RestartPolicy=Always` restarts even successful exits (cause `CleanExitRestart`). If the service is *meant* to exit, use `OnFailure` instead.

## The machine keeps rebooting

A reboot loop almost always means a **Critical service is failing**. A [`ErrorControl=Critical`](/peios/using-peios/services-and-jobs/supervision.md) service that exhausts its budget makes peinit sync and reboot; if it fails the same way every boot, you loop.

The [boot-attempt counter](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) is designed to break this: after N attempts (default 3) peinit stops and drops you into [Recovery mode](#booted-into-recovery-mode). From the Recovery shell, find the failing Critical service (its logs are in [eventd](/peios/security-fundamentals/auditing/overview.md), which persists across reboots), fix or disable it, and reboot. If you need to intervene before the counter trips, boot with `peios.recovery=1` on the kernel command line.

## A service is Abandoned

`Abandoned` means peinit SIGKILLed the process but it survived — stuck in uninterruptible kernel sleep (**D-state**), the signature of hung I/O (a dead NFS mount, a failing disk). Nothing in userspace can kill a D-state process.

- The underlying **I/O fault is the real problem** — investigate the storage or mount, not peinit.
- The service's cgroup is **leaked** and shows in `warnings`. A later start uses a fresh [generational cgroup](/peios/using-peios/services-and-jobs/execution-environment.md), so the new instance is unaffected.
- `peiosctl reset <service>` clears the Abandoned state and re-checks the cgroup: if the stuck process finally died, peinit cleans up; if not, it stays leaked and warns you. Leaked cgroups clear fully only on reboot.

## I changed the config and nothing happened

peinit works from an [in-memory snapshot](/peios/using-peios/services-and-jobs/defining-a-service.md), and each field has a **mutability class** that decides when a change applies:

- **Immutable at runtime** (`ImagePath`, `Type`, `Identity`, `RequiredPrivileges`, `ErrorControl`) → takes effect only on `restart`.
- **Apply on next start** (dependencies, conditions, `OnFailure`) → next start or graph reload.
- **Reloadable at runtime** (timeouts, restart policy, health checks, environment, hooks…) → next time peinit acts on the service.
- **Hot-reloaded** (`ServiceSecurity`) → next control request.

If a change is not landing, check its class first. For a wholesale re-read of every definition, run `peiosctl reload-config` — it rebuilds and validates the whole graph atomically and swaps it in only if validation passes (running services are untouched). And remember peinit *pulls* changes from [registry notifications](/peios/using-peios/registry-concepts/watches.md) rather than having them pushed, so there can be a brief lag.

## ACCESS_DENIED

A control command returns `ACCESS_DENIED`. peinit ran [AccessCheck](/peios/security-fundamentals/access-decisions/overview.md) on your token against the target's descriptor and the right was not granted:

- A *service* command needs the matching right in the service's [ServiceSecurity descriptor](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) (`start` needs `SERVICE_START`, `restart` needs both stop and start, …).
- `shutdown` and `reload-config` need `SYSTEM_SHUTDOWN` / `SYSTEM_RELOAD_CONFIG` in peinit's control descriptor.
- The check uses your **effective** identity at connect time — if you are [impersonating](/peios/security-fundamentals/impersonation/overview.md), that is what is checked.

Every denial is logged with the caller SID, target, and requested right. Walk it through [Debugging a denial](/peios/security-fundamentals/access-decisions/debugging-a-denial.md).

> [!NOTE]
> If `list` shows fewer services than you expect, that is not a bug — `list` **omits** services you lack `SERVICE_QUERY_STATUS` on rather than denying them. You are seeing exactly what your token can see.

## A dependent never started

You started (or booted) a service, but something that depends on it never came up:

- A **`Requires`/`BindsTo` dependent stays blocked** until its target reaches a [satisfying state](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) (Active, Completed, or Skipped). If the target is stuck in Starting or Failed, so is the dependent — fix the target.
- A dependent that connected to its target on boot and got *connection refused* usually means the target uses **`Readiness=Alive`** — peinit only waited for the process to exist, not to be serving. Switch the target to `Readiness=Notify` so it signals `READY=1` when actually ready. (peinit logs this as a validation warning at boot.)
- A **`Wants` dependent starts regardless** of its target — if you needed it to wait, `Wants` was the wrong relationship; use `Requires`.

## Booted into Safe mode

[Safe mode](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) starts only Critical and `SafeMode=1` services. You land here when graph validation finds a **cycle or unresolvable conflict involving a Critical service**, or when `peios.safemode=1` is on the kernel command line. The TCB is healthy; the *configuration* is broken.

Fix the graph: the logs name the cycle path (`A → B → C → A`) or the conflicting pair. Once the cycle is broken or the conflict resolved, a normal boot returns. You can start any service by hand from Safe mode in the meantime — it is only auto-start that is restricted.

## Booted into Recovery mode

[Recovery mode](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) gives you a SYSTEM shell on the console and starts nothing else. You reach it when the boot counter hits N, when `peios.recovery=1` is set, or when **registryd failed in Phase 1** (entered immediately, no counter).

If the registry itself is the problem, use the offline tools that bypass it — they talk to the [loregd implementation](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) directly:

```
$ loregd --inspector               # read the storage directly to diagnose
$ loregd --recover-from-backup     # restore the backup taken each registryd start
$ loregd --dangerously-clear-database   # last resort — wipe; roles re-supply config
```

Recovery has **no TCB protections** — it is a SYSTEM shell, full stop. Treat it accordingly, and remember it needs console access (physical, IPMI, or serial); there is no remote recovery yet.

## Where to start

To read states and causes fluently, keep [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) handy.

For restart, backoff, and Critical-reboot behaviour, see [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md).

For the commands and their outputs, see [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md).

---

# Registry key reference

_Peios / Using Peios / Services & jobs_

> The complete catalog — every service-definition field with its registry type and default, and every key peinit reads or writes.

This page is the reference catalog for everything peinit reads from or writes to the [registry](/peios/using-peios/registry-concepts/overview.md): the full service-definition schema with types and defaults, and every key peinit touches outside the service definitions. For the *meaning* of each item, follow the link in its row; this page is for looking up a type, a default, or a path.

> [!NOTE]
> The registry stores a value's type but not its meaning — that is what [`regman`](/peios/using-peios/registry-administration/regman.md) and these docs are for. A value peinit accepts at write time can still be rejected when peinit reads and validates it; this page lists the schema, not the validation rules (those live on each field's page).

## Registry value types

Service definition fields map onto registry [value types](/peios/using-peios/registry-concepts/keys-values-and-types.md) as follows:

| Schema type | Registry type | Is |
|---|---|---|
| string | `REG_SZ` | A UTF-8 string. |
| multi_string | `REG_MULTI_SZ` | An ordered list of strings. |
| dword | `REG_DWORD` | A 32-bit unsigned integer. |
| binary | `REG_BINARY` | A raw byte sequence. |
| (timestamp) | `REG_QWORD` | A 64-bit value — used for timer last-run timestamps. |

## The service definition schema

Every service is a key under `Machine\System\Services\<name>`; these are the values inside it. Only `ImagePath` is required. Fields are grouped by purpose; for full semantics see [Defining a service](/peios/using-peios/services-and-jobs/defining-a-service.md) and the linked pages.

**Execution** — see [The execution environment](/peios/using-peios/services-and-jobs/execution-environment.md)

| Field | Type | Default | Meaning |
|---|---|---|---|
| `ImagePath` | string | *(required)* | Absolute path to the service binary. |
| `Arguments` | multi_string | — | Command-line arguments. |
| `WorkingDirectory` | string | `/` | Working directory (non-empty absolute path). |
| `TTYPath` | string | — | Absolute path to a terminal to attach as the service's stdio and controlling terminal, e.g. `/dev/console` or `/dev/tty1`. Absent or empty means the daemon default (`/dev/null` stdin, captured output). Attaching a terminal suppresses log capture. |
| `Environment` | multi_string | — | `KEY=VALUE` pairs added to the environment. |
| `RuntimeDirectories` | multi_string | — | Directories created directly under `/run` just before the main process starts, each secured for SYSTEM, Administrators, and the service's own SID. |
| `LimitNOFILE` | dword | — | `RLIMIT_NOFILE`. |
| `LimitCORE` | dword | — | `RLIMIT_CORE`, in bytes. |

> [!NOTE]
> Each `RuntimeDirectories` entry is a plain relative name — just `foo`, never a path — and must not contain `/`, `\`, `.`, `..`, NUL, or control characters. For an entry `foo`, peinit creates `/run/foo` immediately before launching the main process and applies a Peios file security descriptor granting full access to SYSTEM, Administrators, and this service's own SID. If the directory can't be created or the descriptor can't be applied, the start fails with `ParentSetupFailure`. peinit does *not* remove these directories when the service stops — `/run` is a boot-scoped tmpfs that the next boot clears for you.

**Type and readiness** — see [Simple and Oneshot services](/peios/using-peios/services-and-jobs/service-types.md)

| Field | Type | Default | Meaning |
|---|---|---|---|
| `Type` | dword | 0 (Simple) | 0 = Simple, 1 = Oneshot. |
| `Readiness` | dword | 0 (Notify) | 0 = Notify (`READY=1`), 1 = Alive. Ignored for Oneshot. |
| `RemainAfterExit` | dword | 0 | Oneshot only — stay Completed after a successful exit. |
| `SuccessExitCodes` | multi_string | — | Non-zero exit codes treated as success (each 0–255). |

**Activation** — see [Triggers and timers](/peios/using-peios/services-and-jobs/triggers-and-timers.md)

| Field | Type | Default | Meaning |
|---|---|---|---|
| `Triggers` | multi_string | — | `boot` and/or `timer:<schedule>`. Absent = demand-only. |
| `Disabled` | dword | 0 | If 1, triggers must not activate the service. |
| `SafeMode` | dword | 0 | If 1, attempt to start in Safe mode. Critical implies SafeMode. |
| `Conditions` | multi_string | — | Start-time checks; a failure *skips* the service. |
| `Asserts` | multi_string | — | Start-time checks; a failure *fails* the service. |
| `PreStartCheckTimeout` | dword | 5 | Seconds allowed for the helper that evaluates filesystem `Conditions`/`Asserts`; if it overruns, it is killed and the check counts as *not satisfied* (a Condition skips, an Assert fails). |
| `TimerPersistent` | dword | 1 | Catch up a missed timer run after a reboot. |
| `TimerJitter` | dword | 0 | Maximum random delay (seconds) added to each firing. |

**Identity** — see [Service identity and privileges](/peios/using-peios/services-and-jobs/identity-and-privileges.md)

| Field | Type | Default | Meaning |
|---|---|---|---|
| `Identity` | string | `LocalService` | Principal name or SID for the service token. |
| `RequiredPrivileges` | multi_string | — | Privilege allow-list; all others are removed from the token. |
| `HookIdentity` | string | (service's `Identity`) | Identity for `ExecStartPre`/`ExecStartPost`. |

**Dependencies** — see [Dependencies and ordering](/peios/using-peios/services-and-jobs/dependencies.md)

| Field | Type | Default | Meaning |
|---|---|---|---|
| `Requires` | multi_string | — | Hard dependencies. |
| `Wants` | multi_string | — | Soft dependencies. |
| `BindsTo` | multi_string | — | Runtime coupling. |
| `Conflicts` | multi_string | — | Mutual exclusion. |
| `OnFailure` | string | — | Service to start when this one enters Failed. |

**Supervision and health** — see [Keeping services running](/peios/using-peios/services-and-jobs/supervision.md)

| Field | Type | Default | Meaning |
|---|---|---|---|
| `ErrorControl` | dword | 0 (Normal) | 0 = Normal, 1 = Critical (sync + reboot on irrecoverable failure). |
| `RestartPolicy` | dword | 1 (OnFailure) | 0 = Never, 1 = OnFailure, 2 = Always. |
| `RestartMaxRetries` | dword | 5 | Consecutive restarts before Failed. |
| `RestartWindow` | dword | 120 | Seconds of Active health that resets the restart counter. |
| `RestartDelay` | dword | 1 | Backoff base (doubles per failure, capped at 60). |
| `HealthCheck` | string | — | Periodic health-check command. |
| `HealthCheckInterval` | dword | 30 | Seconds between health checks. |
| `HealthCheckTimeout` | dword | 5 | Seconds before a check is killed and failed. |
| `HealthCheckRetries` | dword | 3 | Consecutive failures before unhealthy. |
| `WatchdogTimeout` | dword | 0 | Seconds between expected `WATCHDOG=1` pings; 0 disables. |

**Transition phases** — see [The service lifecycle](/peios/using-peios/services-and-jobs/the-service-lifecycle.md) and [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md)

| Field | Type | Default | Meaning |
|---|---|---|---|
| `ExecStartPre` | multi_string | — | Commands run before the binary (sequential; any failure aborts start). |
| `ExecStartPost` | multi_string | — | Commands run after readiness / successful exit (failure logged, not fatal). |
| `ExecReload` | string | — (SIGHUP) | Reload command, or `signal:<NAME>`. |
| `StartTimeout` | dword | 30 | Seconds for the whole start sequence. |
| `StopTimeout` | dword | 10 | Seconds between SIGTERM and SIGKILL. |

**Notify, fds, security, metadata**

| Field | Type | Default | Meaning |
|---|---|---|---|
| `NotifyAccess` | dword | 0 (Main) | Who may send sd_notify (only Main is supported). |
| `FdStoreMax` | dword | 0 | Max fds in the [fd store](/peios/using-peios/services-and-jobs/execution-environment.md); 0 disables it. |
| `ServiceSecurity` | binary | inherit parent | [Descriptor](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) controlling who may manage the service. |
| `DisplayName` | string | — | Human-readable name for status display. |
| `Description` | string | — | Description of the service. |

## Service definition keys

| Key | Type | Purpose | See |
|---|---|---|---|
| `Machine\System\Services\` | (parent key) | Parent of all service definitions; each child key is a service. | [Defining a service](/peios/using-peios/services-and-jobs/defining-a-service.md) |
| `Machine\System\Services\SchemaVersion` | dword | Schema version guard (currently 1). | [Defining a service](/peios/using-peios/services-and-jobs/defining-a-service.md) |
| `Machine\System\Services\<name>\LastTimerRun` | `REG_QWORD` | Last-run timestamp for a single-trigger persistent timer. Written by peinit. | [Triggers and timers](/peios/using-peios/services-and-jobs/triggers-and-timers.md) |
| `Machine\System\Services\<name>\TimerState\` | (subkey) | Per-trigger last-run timestamps for multi-trigger services. Each value is named by the percent-encoded schedule string and holds a `REG_QWORD`. | [Triggers and timers](/peios/using-peios/services-and-jobs/triggers-and-timers.md) |

## Boot configuration

Under `Machine\System\Boot\`:

| Key | Type | Default | Purpose | See |
|---|---|---|---|---|
| `MaxParallelStarts` | dword (> 0) | 10 | Maximum services starting concurrently. Missing → default; zero/invalid → Recovery. | [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) |
| `BootSuccessGrace` | dword | 30 | Seconds a Critical service must hold a satisfying state before boot is successful. | [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) |
| `ShutdownTimeout` | dword | 90 | Maximum seconds for the whole graceful shutdown. | [Shutdown](/peios/using-peios/services-and-jobs/shutdown.md) |
| `SettleTimeout` | dword | 5 | Seconds to wait for the boot set to settle before starting [`boot:settled`](/peios/using-peios/services-and-jobs/triggers-and-timers.md) services anyway. | [Triggers and timers](/peios/using-peios/services-and-jobs/triggers-and-timers.md) |
| `PostKillTimeout` | dword | 5 | Seconds peinit waits for a service cgroup to drain after SIGKILL before treating it as stuck. `ShutdownTimeout` bounds the shutdown as a whole; this bounds one service's last stage of it. | [Shutdown](/peios/using-peios/services-and-jobs/shutdown.md) |

## Operational parameters

Under `Machine\System\Init\`:

| Key | Type | Default | Purpose | See |
|---|---|---|---|---|
| `ControlSecurity` | binary | SYSTEM full; Administrators shutdown + reload-config | Descriptor for system-level control operations. | [Who can manage a service](/peios/using-peios/services-and-jobs/who-can-manage-a-service.md) |
| `MaxControlConnections` | dword | 32 | Maximum concurrent control-socket connections. | [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md) |
| `MaxRequestSize` | dword | 65536 | Maximum control-socket request size (bytes). | [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md) |
| `ConnectionTimeout` | dword | 30 | Seconds before an idle control connection is closed. | [Controlling services](/peios/using-peios/services-and-jobs/controlling-services.md) |
| `MaxLogLineLength` | dword | 8192 | Maximum bytes per service output line before truncation. | [Service output and logging](/peios/using-peios/services-and-jobs/output-and-logging.md) |
| `MaxLogBufferPerService` | dword | 65536 | Maximum bytes buffered per service pipe before back-pressure. | [Service output and logging](/peios/using-peios/services-and-jobs/output-and-logging.md) |
| `LogReadBytesPerEvent` | dword | 16384 | Bytes drained from a service's output pipe per readable event. Larger favours throughput on chatty services, smaller favours fairness between them. | [Service output and logging](/peios/using-peios/services-and-jobs/output-and-logging.md) |
| `PreEventdBuffer` | dword | 1048576 | Total bytes of service output held in memory before eventd is up to receive it. Overflow drops the oldest output; it never blocks a service. | [Service output and logging](/peios/using-peios/services-and-jobs/output-and-logging.md) |
| `EnvVars\` | (parent key) | empty | Default environment variables injected into Phase 2 services (value name = variable, `REG_SZ` data = value). The descriptor here is security-critical. | [The execution environment](/peios/using-peios/services-and-jobs/execution-environment.md) |
| `ProvisionedPaths\` | (parent key) | empty | Boot-time path provisioning: each child key names one directory or file that peinit creates and secures after registryd starts and before Phase 2 — the registry-backed equivalent of tmpfiles.d. | [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) |
| `ProvisionedPaths\<name>\Kind` | string | *(required)* | `directory` or `file` — what to create at `Path`. | [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) |
| `ProvisionedPaths\<name>\Path` | string | *(required)* | Absolute path to create or verify. | [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) |
| `ProvisionedPaths\<name>\Security` | binary | SYSTEM + Administrators full, users read/traverse | Peios file security descriptor to apply to the object. | [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) |
| `ProvisionedPaths\<name>\Required` | dword | 0 | If 1, a failure to provision this entry sends boot to [Recovery mode](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) before Phase 2 starts. | [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) |

> [!NOTE]
> A few `ProvisionedPaths` rules worth knowing before you rely on them: `Kind=file` never truncates an existing file (it only makes sure the file is present), and peinit does *not* create missing parent directories for you — define each directory you need as its own entry, or depend on the package that owns the parent. A malformed entry is logged and skipped rather than blocking boot; only an entry you mark `Required=1` can send boot to [Recovery mode](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md), and only when it genuinely can't be created or secured.

## Keys peinit reads from other subsystems

| Key | Type | Purpose | See |
|---|---|---|---|
| `Machine\System\eventd\LogSocketPath` | string | Path of eventd's log datagram socket, where peinit forwards service output. | [Service output and logging](/peios/using-peios/services-and-jobs/output-and-logging.md) |

## Where to start

For how these fields combine into a working definition and when changes take effect, read [Defining a service](/peios/using-peios/services-and-jobs/defining-a-service.md).

To look up the meaning, valid range, and effect-timing of any key on a live system, use [`regman`](/peios/using-peios/registry-administration/regman.md).

---

# Mount policies

_Peios / Using Peios / Mount policies_

> Every mounted filesystem carries a mount policy — one per-superblock setting deciding whether FACS applies and how missing SDs are handled.

A **mount policy** is the per-superblock setting that controls how FACS treats a filesystem. The policy tells the kernel: does FACS apply here at all? When a file has no SD, what should happen? When the kernel synthesises a default SD because the file doesn't have one, where does the template come from? These questions are uniform within a filesystem — every file on a single mount gets the same treatment — and the answer is the mount policy.

There are four policy classes in v0.20. Three are FACS-managed (the kernel applies KACS access control); one is unmanaged (FACS doesn't apply at all). The class is decided when the filesystem is mounted, and changes apply lazily — existing handles are unaffected, but the next access against any file on the mount sees the new policy.

This page introduces the four classes at a glance. [Policy classes](/peios/using-peios/mount-policies/policy-classes.md) covers each in detail with the missing-SD and corrupt-SD rules; [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md) covers how each filesystem type physically stores the SD; [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md) covers the syscalls and the generation counter for cache invalidation.

## Why mount policies exist

A FACS-managed file needs a security descriptor. The DACL on it is the access decision input. The kernel needs to be able to look at any FACS-managed file and find an SD.

But not every filesystem can store an SD on every file. ext4 stores SDs in xattrs, but only up to 4 KB without special features; XFS supports xattrs natively up to 64 KB; tmpfs has no persistent storage at all; FAT and exFAT have no xattr support; remote filesystems like NFS may not surface SDs to the client. The kernel needs different behaviours for these cases.

That's what mount policies provide. The policy is the kernel's per-filesystem answer to "what to do when a file does not have an SD I can read":

- For some filesystems, refuse access (the file should have an SD and doesn't, so something is wrong).
- For some, synthesise an SD on the fly in memory (and don't try to write it back).
- For some, synthesise an SD and try to write it back so future accesses don't need to re-synthesise.
- For some (the kernel's own pseudo-filesystems), don't apply FACS at all.

Each of these is one of the four classes. The class is a property of the mount, not the file.

## The four classes at a glance

| Class | What it does | Typical use |
|---|---|---|
| **`facs_deny_missing`** | FACS-managed. A file with no SD is unreachable — every access fails. | System mounts (root, `/home`, `/var`) where every file should have an SD. |
| **`facs_synthesize_ephemeral`** | FACS-managed. A missing SD is synthesised in memory but not written back. The next access re-synthesises. | Removable media, FAT/exFAT, NFS client mounts — filesystems that may not preserve xattrs cleanly or where modifying SDs is not appropriate. |
| **`facs_synthesize_persistent`** | FACS-managed. A missing SD is synthesised and immediately written back. Subsequent accesses use the stored SD. | Filesystems being adopted into Peios — a previously-unmanaged filesystem getting an SD on first access. |
| **`unmanaged`** | FACS does not apply. The kernel uses its own per-operation access rules for files under this mount. | `/proc`, `/sys` — kernel pseudo-filesystems with their own access semantics. |

`unmanaged` is not settable via the public ABI — only the kernel itself sets this class, at boot time, for the pseudo-filesystems it manages. The other three are administratively settable.

A mounted filesystem has exactly one of these classes at any time. Changing the class is a single operation that affects every file on the mount going forward (subject to the lazy-invalidation behaviour).

## Per-superblock, not per-path

A mount policy is per-**superblock**, not per-path or per-bind-mount. All paths visible through the same underlying filesystem instance share the same policy.

The implications:

- A bind mount of `/etc/peios` to `/old/etc/peios` does not let you set different policies for the two paths. They are the same superblock; one policy.
- A read-only bind mount and the underlying read-write mount of the same filesystem share the policy.
- Two separate mounts of the same filesystem (uncommon but possible in some namespacing setups) each have their own superblock instance and can have different policies.

The per-superblock rule keeps mount policy simple. The alternative — per-mount or per-path — would mean a single inode could be subject to different policies depending on how it was reached, which would be operationally confusing.

## What the policy decides

For a FACS-managed mount (any of the three managed classes), the policy decides three things:

1. **What happens when an inode has no SD attached.** Different classes handle this differently — deny, synthesise-in-memory, or synthesise-and-persist.
2. **The mount-level SD template.** Each FACS-managed mount can carry a default SD template, used when synthesising an SD for a file that has no parent-derivable SD (root of the filesystem, typically).
3. **Whether mutations to synthesised SDs are persisted.** Ephemeral synthesises in memory only; persistent writes back to the filesystem.

For the unmanaged class, the policy decides one thing: FACS doesn't apply. Files under this mount are governed by whatever per-operation rules the kernel has for that pseudo-filesystem.

## What the policy does not decide

A few clarifications:

- **The policy does not change what filesystem operations are available.** Read, write, mmap, all the usual operations work normally on every class. The policy affects how the *access check* runs, not what operations exist.
- **The policy does not control persistence of file data.** A file's contents are persisted (or not) by the underlying filesystem; the policy is only about SD persistence.
- **The policy does not affect already-open handles.** The handle model caches access masks on fds; changing the policy does not retroactively change cached masks. Future opens see the new policy.
- **The policy does not propagate inheritably.** Changing a parent's mount policy doesn't affect mounted-elsewhere child directories — each mount carries its own policy on its own superblock.

## Where to start

If you want each class in detail — what `facs_deny_missing` actually does at runtime, how `synthesize_ephemeral` works for FAT, what happens when a synthesised SD turns out to be problematic — read [Policy classes](/peios/using-peios/mount-policies/policy-classes.md).

If you want to know how each filesystem stores SDs — ext4's `ea_inode`, XFS's native xattr support, NTFS via `system.ntfs_security`, the no-xattr-support filesystems — read [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

If you want the operational story — `kacs_set_mount_policy`, `kacs_get_mount_policy`, the generation counter that makes invalidation lazy, why mount policy changes don't walk the filesystem — read [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md).

## The commands

The concept pages above describe the model; three command-line tools put it to work. [`mount`](/peios/using-peios/mount-policies/mount.md) attaches a filesystem to the mount tree and is where a KACS mount policy (and its optional SD template) is chosen at attach time — the one place the policy classes above meet a command line. [`umount`](/peios/using-peios/mount-policies/umount.md) detaches a filesystem again, by mount point or by source, with lazy, forced, recursive and all-targets variants. [`lsblk`](/peios/using-peios/mount-policies/lsblk.md) lists the block devices available to mount, with their filesystems and, like `ls -l`, their SD-derived owner and mode. Use these when you want the operational reference for a specific flag rather than the model behind it.

---

# Policy classes

_Peios / Using Peios / Mount policies_

> The four policy classes, the synthesis chain that produces an SD when one is missing (parent → mount template → fallback), and the universal corrupt-SD rule.

The four policy classes are the answer to one question: when FACS needs an SD for a file and the filesystem doesn't have one, what should happen?

The three managed classes — `facs_deny_missing`, `facs_synthesize_ephemeral`, `facs_synthesize_persistent` — answer the question in three different ways. The fourth, `unmanaged`, answers it by stepping out of the question entirely: FACS doesn't apply, so the question doesn't arise.

This page covers each class in detail, the synthesis chain that produces an SD when one is needed, and the universal corrupt-SD rule.

## facs_deny_missing

A FACS-managed mount where missing SDs are treated as an error. Every file on a `facs_deny_missing` mount must have a valid SD; one that does not is unreachable.

The runtime behaviour:

1. The kernel reads the file's SD from the filesystem (typically an xattr).
2. If no SD is present, the access fails. The kernel returns `-EACCES` for any operation that would require an SD-based access check.
3. The file is effectively unreadable, unwritable, undeletable, unmodifiable. Every access fails because there is no policy to evaluate against.

This is the strict mode. It is appropriate for filesystems where every file should have been provisioned with an SD — system mounts, application storage, anywhere the operator has set up the filesystem deliberately.

The defaults for Peios system mounts (the root filesystem, `/home`, `/var`) are `facs_deny_missing`. The image-build process ensures every file in the base image has an SD; subsequent file creation always inherits an SD from the parent. There is no path by which a file without an SD legitimately ends up on these filesystems.

If somehow a file does end up without an SD — a backup-restore tool that skipped xattrs, a misbehaving filesystem driver — that file is unreachable. The fix is to give it an SD (`kacs_set_sd`, requires WRITE_DAC, which the caller may not have if they cannot read the file).

Practical upshot: a `facs_deny_missing` mount is strict. Files without SDs stay unreachable until given one. This is the right setting for filesystems where you trust the provisioning.

## facs_synthesize_ephemeral

A FACS-managed mount where missing SDs are synthesised in memory, but **not written back** to the filesystem. The synthesised SD is used for the access check; it does not become part of the file's persistent state.

The runtime behaviour:

1. The kernel reads the file's SD from the filesystem.
2. If no SD is present, the kernel **synthesises** one using the synthesis chain (covered below).
3. The synthesised SD is used for the access check.
4. The synthesised SD is **not** written back to the filesystem. It exists only in the kernel's cache for the duration this inode is in memory.
5. On a subsequent open of the same file (after the inode has been evicted from cache, say), the SD is synthesised again. The same inputs produce the same output, so the same SD comes out — but the synthesis is repeated.

This class is appropriate for filesystems where you can't or don't want to write SDs:

- **Removable media** (USB drives, optical media). You don't want to modify the media just because you read a file on it.
- **FAT and exFAT.** These filesystems have no xattr support; the SD has nowhere to go even if you wanted to write it.
- **NFS client mounts.** The actual file lives on a remote server; modifying its SD via xattr would have unpredictable effects.
- **tmpfs and devtmpfs.** Per-instance pseudo-filesystems with no real persistence.

The synthesis-only-in-memory pattern lets FACS apply access control to these filesystems without changing their on-disk content. The trade-off is that the cost of synthesising is paid every time the inode is cold.

## facs_synthesize_persistent

A FACS-managed mount where missing SDs are synthesised **and** written back. Each file is synthesised once; from then on it has a real SD.

The runtime behaviour:

1. The kernel reads the file's SD from the filesystem.
2. If no SD is present, the kernel synthesises one using the synthesis chain.
3. The synthesised SD is used for the access check — immediately, from the kernel's cache.
4. The synthesised SD is **also** written back to the filesystem (typically as the SD xattr). The file now has a persistent SD.
5. On a subsequent open, the SD is read from storage — no re-synthesis needed.

The write-back in step 4 does not happen *during* the access — it is **deferred** until just after the triggering operation finishes (when the kernel can safely write without holding the locks the access is using). In practice the SD is on disk by the time the syscall that first touched the file returns to userspace, so for an observer it is effectively immediate.

Because the synthesised SD is a deterministic function of its inputs (the parent's SD, or the mount template), the on-disk copy is a *cache* of a value the kernel can always recompute. If the write-back is interrupted — the inode is evicted first, or the process exits in the gap — nothing is lost: the next access re-synthesises the identical SD and tries the write-back again. A file is never left with the *wrong* SD, only occasionally with the SD still in memory rather than on disk. (A mount-template change in that gap simply discards the pending SD and re-synthesises against the new template, so a superseded SD is never frozen onto the disk.)

This class is for filesystems being adopted into Peios. A previously-unmanaged filesystem (say, an ext4 volume from a Linux system without KACS) can be mounted with `facs_synthesize_persistent`; every file accessed gets an SD on first access; over time, every file ends up with an SD.

The use case: migrating an existing filesystem to KACS-managed without a single-pass conversion tool. The synthesis-on-access pattern lets the conversion happen incrementally as files are touched. After enough time, every regularly-accessed file has been adopted; rarely-touched files get adopted the next time they are read.

After all files have SDs, the mount can be switched to `facs_deny_missing` for strict mode. The transition is a single `kacs_set_mount_policy` call; existing SDs are unaffected, and any straggler files without SDs become unreachable (which is the desired effect of switching to strict).

## unmanaged

The unmanaged class is the special case for filesystems FACS shouldn't apply to. The kernel uses its own per-operation access rules for files under this mount; no SD-based access check runs at FACS's level.

Specifically:

- `/proc` is `unmanaged`. Access to `/proc/<pid>/*` files is governed by the process-level checks (process SD + PIP, per the two-check rule), not by FACS.
- `/sys` is `unmanaged`. Writes are restricted to `BUILTIN\Administrators` and `SYSTEM` by hardcoded rule.
- `/sys/kernel/security/kacs/*` has explicit SDs set by the kernel; reading these uses the kernel's own logic.

The `unmanaged` class cannot be set via the public ABI. The `kacs_set_mount_policy` syscall rejects attempts to set this class with `-EINVAL`. Only the kernel itself sets this class — at boot, for the pseudo-filesystems it manages.

The reason for restricting this: making a regular filesystem `unmanaged` would mean FACS has no say over it at all, which would be a meaningful operational decision but also a security-relevant one. The kernel reserves the class for its own use.

If you mount a regular filesystem and don't want FACS to apply, the closest you can get is `facs_synthesize_ephemeral` with a permissive mount template — files get a permissive synthesised SD that effectively grants access. This is not the same as no FACS, but it is the closest path available through the public ABI.

## The synthesis chain

For the two synthesising classes, when the kernel needs to produce an SD for a file with none, it uses a chain of sources. The first source that yields a usable SD wins.

```mermaid
flowchart LR
    A["File has no SD"] --> B["Inherit from parent directory"]
    B -->|parent has inheritable ACEs| F["Use computed SD"]
    B -->|parent has none / no parent| C["Mount-level SD template"]
    C -->|template is set| F
    C -->|no template| D["Hardcoded fallback SD"]
    D --> F
```

In order:

1. **Parent directory inheritance.** The kernel reads the parent directory's SD and computes what a newly-created child would inherit (per the [Inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) rules). If this produces a usable SD (with inheritable ACEs from the parent), that SD is used.
2. **Mount-level SD template.** Each FACS-managed mount can carry a default SD template — set via `kacs_set_mount_policy` along with the policy class. The template is a complete self-relative SD (max 64 KB). If the parent did not yield an SD, the template is used.
3. **Hardcoded fallback.** If neither the parent nor the template produces an SD, the kernel uses a hardcoded fallback: `GENERIC_ALL` to SYSTEM and `BUILTIN\Administrators`; `GENERIC_READ | GENERIC_EXECUTE` to Everyone. The owner is set to SYSTEM, group to SYSTEM.

The fallback is conservative: administrators get full control, others get read-and-execute. It is what every filesystem mounted with `facs_synthesize_*` and no specific configuration falls back to, and what every root-of-mount file ends up with on a freshly-installed Peios system.

For most mounts, the fallback is the safety net — typical files have parents with inheritable ACEs, and synthesis lands on step 1. The template is used for files at the root of the mount (where there's no parent on this filesystem) or in unusual cases where parent-inheritance doesn't apply.

## Corrupt SD handling

A universal rule across all FACS-managed classes: **a corrupt SD is treated as a denial**. If the SD on a file exists but fails structural validation — bad header, malformed ACL, invalid SID, exceeds the size limit, anything that prevents the parser from making sense of it — the access check fails with `-EACCES`.

The kernel does not fall back to synthesis when an existing SD is corrupt. The synthesis path is for files with no SD; a file with a *broken* SD is different. The kernel:

1. Detects the corruption when reading the SD.
2. Returns `-EACCES` for the access.
3. Emits an audit event (one per inode per cache population — the same corrupt SD encountered repeatedly during one mount's lifetime produces one audit event for that inode, not one per access).

The corruption audit event is part of the audit stream and useful for diagnostics. A misbehaving backup-restore tool that produced corrupt SDs on a restored set of files generates a flurry of these events when the files are first accessed.

The corrupt-SD rule is the same across all three FACS-managed classes. `facs_deny_missing` denies missing; `facs_synthesize_*` synthesises missing; all three deny corrupt. The "missing" and "corrupt" cases are different — missing is "no SD was attached" and is recoverable through synthesis or operator intervention; corrupt is "the SD that was attached is broken" and requires either fixing the SD or accepting denial.

## Class transitions

Changing a mount's policy class is allowed (subject to access rules covered in [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md)). The interesting transitions:

- **`facs_synthesize_persistent` → `facs_deny_missing`.** Common during migration. As files are adopted by `synthesize_persistent`, they accumulate SDs. Once enough have been adopted, switch to `deny_missing` to enforce strictness. Any file still missing an SD becomes unreachable; an administrator can either accept that or set SDs on the holdouts.
- **`facs_synthesize_ephemeral` → `facs_synthesize_persistent`.** Less common. Would convert an ephemeral mount to a persistent one — the next access of any file without a stored SD would write the synthesised SD back. This effectively "snapshots" the synthesis on first access.
- **`facs_deny_missing` → `facs_synthesize_*`.** Unusual; would relax strictness. The kernel allows it but the operational reasoning is rarely good — synthesis is a recovery mechanism, not an everyday setting.

A class transition is just a policy update; no files are touched at the transition. The new policy applies to future accesses. Existing cached state (synthesised SDs in memory) may need to be re-evaluated — see the generation counter in [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md).

## Where to go next

For how each filesystem physically stores the SD, read [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

For the syscalls that set and read a mount's policy, read [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md).

For choosing a policy from the command line at attach time, read [mount](/peios/using-peios/mount-policies/mount.md).

---

# SD storage by filesystem

_Peios / Using Peios / Mount policies_

> How each filesystem stores SDs — ext4 xattrs and ea_inode, NTFS round-tripping via system.ntfs_security, FAT's in-memory synthesis — and the implications.

A security descriptor is structurally a single piece of metadata that has to live somewhere. Different filesystems have different ways to hold metadata; some have built-in security-attribute support (NTFS), some have generic extended attribute support that fits (ext4, XFS, Btrfs), some have neither (FAT, exFAT). The mount-policy class you choose for a filesystem depends partly on what the filesystem can store and partly on what behaviour you want.

This page covers the storage mechanism each filesystem type uses and the implications.

## The canonical xattr

For filesystems that support extended attributes, Peios stores the SD in the **`security.peios.sd`** xattr. The name is in the `security.*` namespace, which requires privileged access to read or write at the filesystem layer — which doesn't matter for FACS-managed access (FACS denies direct xattr operations on the SD xattr unconditionally; access goes through `kacs_get_sd` / `kacs_set_sd`), but does mean the xattr survives operations that respect security xattrs.

For NTFS, the xattr name is **`system.ntfs_security`** — the native NTFS security attribute, accessed through the same xattr interface. Using the NTFS-native name means SDs round-trip cleanly between Peios and other operating systems that read NTFS.

The choice of xattr name is filesystem-specific:

| Filesystem | xattr name |
|---|---|
| ext4, XFS, Btrfs, tmpfs, others with generic xattr support | `security.peios.sd` |
| NTFS (via the `ntfs3` driver) | `system.ntfs_security` |

For other filesystems (with no xattr support or no convention), the SD is held in memory and re-synthesised on each cold open. See the per-filesystem sections below.

## ext4

ext4 supports xattrs natively. SDs up to about 4 KB fit in the inline xattr space; larger SDs require the **`ea_inode`** feature.

`ea_inode` is an ext4 feature that lets a single xattr value spill into a dedicated inode rather than being limited to the size of the inode's inline xattr area. With `ea_inode` enabled at filesystem creation time (or enabled later via `tune2fs`), an SD can be up to the standard 65,535-byte SD limit.

`mke2fs` on Peios enables `ea_inode` by default for ext4, and raises the inode size to 512 bytes so that a typical SD fits inline in the first place — see [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md). Real-world SDs rarely exceed the inline xattr space, but pathological cases (deeply-nested ACEs, lots of inherited ACEs from a complex parent) can push past 4 KB; `ea_inode` accommodates them.

If an ext4 filesystem without `ea_inode` is mounted on Peios and a file has an SD larger than its inline xattr space could hold, the filesystem's xattr write would fail. The kernel handles this by either:

- Refusing the `kacs_set_sd` call with an error indicating the filesystem cannot hold the SD.
- Or, in the `facs_synthesize_persistent` case, refusing the write-back and treating the file as if it has no SD (re-synthesising next time).

The cleanest fix is to enable `ea_inode`. For everyday use, the inline xattr space is sufficient.

## XFS

XFS supports xattrs natively, up to 64 KB per attribute by default. This is comfortably more than the SD size limit (65,535 bytes), so SDs fit without any special configuration.

XFS is a fine choice for FACS-managed filesystems. No `ea_inode`-style consideration is needed; SDs just work.

## Btrfs

Btrfs supports xattrs natively. Small xattrs are stored inline; larger ones get their own extent. SDs of any practical size fit without special handling.

Btrfs's snapshot and clone behaviour is interesting for SDs: a Btrfs snapshot of a directory includes the SDs of every file within. A snapshot is a point-in-time view; the SDs in the snapshot reflect what they were when the snapshot was taken. If the original files' SDs are subsequently modified, the snapshot still has the old SDs. This is the expected behaviour but worth noting for backup/snapshot workflows.

## tmpfs and devtmpfs

tmpfs is an in-memory filesystem. It supports xattrs, but everything it stores lives in RAM and is lost on unmount or reboot. SDs on tmpfs are present while the filesystem is mounted; they vanish when it is unmounted.

tmpfs is typically mounted with `facs_synthesize_ephemeral`. The synthesis happens in memory anyway (the tmpfs storage *is* memory), so there's no operational difference between "store an SD in the tmpfs xattr" and "synthesise an SD into the kernel's per-inode cache".

`devtmpfs` is the kernel-managed filesystem for device nodes. SDs on devtmpfs are applied by **udev rules**, not by FACS-driven synthesis. The udev daemon (or its Peios equivalent) sets the SD on each device node as the node is created. This is a different model from the synthesis-based pattern other filesystems use; it works because the device-node population is controlled centrally and the SDs can be set deterministically.

devtmpfs uses the `facs_synthesize_ephemeral` class, but in practice the synthesis path is rarely hit because udev provides the SDs.

## NTFS — round-trip via ntfs3

NTFS is the Windows-native filesystem. Peios mounts it through the **`ntfs3`** kernel driver, which exposes the NTFS security attribute via the standard xattr interface under the name `system.ntfs_security`.

The implications:

- SDs written by Peios to an NTFS volume use the same on-disk format as Windows. A Windows system reading the same volume sees the SD natively.
- SDs written by Windows to an NTFS volume are readable by Peios. Round-tripping a volume between the two operating systems preserves SDs.

This is the "binary-compatible" property the SD format gives. The wire format is the same; the filesystem driver translates between the xattr interface and the on-disk security attribute.

NTFS volumes are typically mounted `facs_synthesize_ephemeral` rather than `facs_deny_missing`. The reasoning: an NTFS volume from Windows may have files without Peios-recognised SDs (the SDs are Windows-native and may use principals that don't exist on the Peios system). Ephemeral synthesis lets such files be accessible without modifying the volume's stored SDs.

## FAT and exFAT

FAT and exFAT have **no xattr support at all**. There is no place to store an SD; the on-disk format simply doesn't have the metadata channel.

FACS handles this by using `facs_synthesize_ephemeral` for FAT/exFAT mounts. Every file gets a synthesised SD in memory; no SD is ever written back. The synthesised SD applies for the file's time in the inode cache; when the file is evicted, the SD is gone and will be re-synthesised on next access.

This means FAT/exFAT files cannot be given persistent KACS-style permissions. The synthesised SD is the same every time (assuming the same inputs — parent SD, mount template), so the access decision is deterministic, but there is no way to customise per-file.

For most FAT use cases this is fine — FAT is typically used for removable media or boot partitions where uniform-permissions semantics is acceptable. The mount-level SD template can be configured to grant whatever access pattern is appropriate for the mount as a whole.

## NFS — synthesise locally, enforce remotely

NFS client mounts are a unique case. The actual files live on a remote server; the local filesystem driver is a network protocol implementation, not a real filesystem.

The mount class is `facs_synthesize_ephemeral`. FACS synthesises a local SD per the synthesis chain (typically yielding a sensible default from the mount template) and uses it for local access control. The local FACS check decides whether to forward the operation to the server.

If the local check passes, the operation goes to the server. The server has its own access control (potentially also Peios with FACS, potentially another OS with different rules); the server's access control decides whether the operation actually proceeds. If the server denies, the operation fails with whatever error the protocol returns (typically `-EACCES` or `-EIO`).

This is "dual authority" — both client and server have a say. The client's denial is local-final; the server's denial is remote-final. There is no single source of truth for the access decision.

The implications were covered in [Special cases](/peios/security-fundamentals/file-access/special-cases.md) under "NFS — dual authority": don't trust local FACS results for security, expect I/O errors from server-side denial, the local synthesised SD is not the server's actual SD.

## /proc and /sys

`/proc` and `/sys` are kernel pseudo-filesystems. Their mount-policy class is `unmanaged` — set by the kernel at boot, not changeable via the public ABI.

`/proc` doesn't have an SD per-file in the FACS sense. Access to `/proc/<pid>/*` is gated by the per-process rules (process SD + PIP, from the two-check rule). The kernel implements these checks directly when serving `/proc` file operations.

`/sys` similarly has its own per-file rules. The `/sys/kernel/security/kacs/*` entries have explicit SDs the kernel maintains; other `/sys` files have hardcoded rules ("writes restricted to `BUILTIN\Administrators` and `SYSTEM`").

The `unmanaged` class is what tells FACS to not interfere with these. The kernel knows what it is doing with its own pseudo-filesystems; FACS stays out of the way.

## Stacked filesystems — overlayfs and StrataFS

A stacking filesystem presents files that physically live somewhere else. overlayfs merges a read-only lower layer with a writable upper one; [StrataFS](/peios/using-peios/disks-and-filesystems/stratafs/overview.md) composes several directories into one view. Neither stores a security descriptor of its own, and neither needs to: the descriptor belongs to the file, and the file is on the layer underneath.

So a stack forwards. When the kernel needs the SD of a file in the merged view, it asks the layer that actually holds it, and what comes back is that layer's **effective** descriptor — which is to say, whatever an access check on the underlying file would have used. If the layer underneath *stores* an SD, the stack sees the stored one. If the layer underneath *synthesises*, the stack sees the synthesised one. Synthesis composes upward through the stack; it is not a private arrangement between the kernel and the bottom layer.

That is what makes the common live-boot arrangement work:

```
overlay          <- the merged root, facs_deny_missing
  upper: tmpfs   <- writable scratch, stores real SDs
  lower: squashfs <- read-only image with no SDs, facs_synthesize_ephemeral
```

The overlay stores nothing and can still be `facs_deny_missing`, because neither layer beneath it ever answers "missing": the tmpfs has stored descriptors, and the squashfs synthesises one for every inode.

Two consequences worth holding onto:

**The stack's own policy class governs only what happens when every layer beneath it has nothing to offer.** Pick it for that case. Over a synthesising lower, `facs_deny_missing` is the strict-and-correct choice; over an `unmanaged` lower it would lock the whole view.

**A copy-up does not carry the lower's descriptor up with it.** When overlayfs copies a file from the lower layer to the upper to make it writable, it creates a genuinely new inode in the upper, and that inode gets its descriptor by ordinary [inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) from its parent in the upper — not by duplicating the lower's. This is deliberate: `security.peios.sd` is not userspace-writable (SD mutation is `kacs_set_sd`'s job), so a verbatim copy could not be performed even in principle, and the inherited answer is the right one anyway. Every other extended attribute *is* copied up normally.

## Summary

| Filesystem | SD storage | Typical mount class |
|---|---|---|
| ext4 | `security.peios.sd` xattr; `ea_inode` for SDs > 4 KB | `facs_deny_missing` (for system mounts) |
| XFS | `security.peios.sd` xattr, native large support | `facs_deny_missing` |
| Btrfs | `security.peios.sd` xattr, native | `facs_deny_missing` |
| tmpfs | `security.peios.sd` xattr in memory | `facs_synthesize_ephemeral` |
| devtmpfs | xattr in memory, populated by udev | `facs_synthesize_ephemeral` |
| NTFS (ntfs3) | `system.ntfs_security` xattr, NTFS-native | `facs_synthesize_ephemeral` (typical) |
| FAT / exFAT | No SD storage; in-memory only | `facs_synthesize_ephemeral` |
| NFS client | No client-side storage; synthesise locally | `facs_synthesize_ephemeral` |
| squashfs | No SD storage unless the image was built with one | `facs_synthesize_ephemeral` |
| overlayfs | None of its own — reads the real layer | `facs_deny_missing` |
| StrataFS | None of its own — reads the provider | `facs_deny_missing`, not settable |
| /proc | n/a — no FACS | `unmanaged` |
| /sys | n/a — kernel-managed SDs | `unmanaged` |

The pattern: real on-disk filesystems with xattr support get one of the `facs_*` classes depending on policy needs. Pseudo-filesystems and the kernel's own filesystems are `unmanaged`. Filesystems without xattr support default to ephemeral synthesis as the only viable mode. Stacking filesystems store nothing and defer to the layer beneath.

## Where to go next

For what each policy class does with a missing SD, read [Policy classes](/peios/using-peios/mount-policies/policy-classes.md).

For setting and reading a mount's policy at runtime, read [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md).

For the structure of the SD being stored, read [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md).

---

# Managing mounts

_Peios / Using Peios / Mount policies_

> kacs_set_mount_policy and kacs_get_mount_policy, the generation counter behind lazy invalidation, and the SeTcbPrivilege requirement.

A mount's policy is set via a syscall, read via a syscall, and otherwise sits as kernel-internal state on the filesystem's superblock. Mount-policy changes are administrative — they require `SeTcbPrivilege` — and they take effect lazily, without the kernel walking the filesystem to update anything.

This page covers the two syscalls (`kacs_set_mount_policy`, `kacs_get_mount_policy`), the generation counter that makes lazy invalidation work, and what does and does not happen at a policy change.

## kacs_set_mount_policy

The write syscall:

```
result = kacs_set_mount_policy(fd, args)
```

Where `fd` is a file descriptor referring to any object on the target superblock (the kernel uses the fd to identify which superblock — the file itself does not need to be the root of the mount). `args` is a `kacs_mount_policy_args` struct:

| Field | Meaning |
|---|---|
| `policy` | The new policy class (one of `KACS_MOUNT_POLICY_DENY_MISSING`, `KACS_MOUNT_POLICY_SYNTHESIZE_EPHEMERAL`, `KACS_MOUNT_POLICY_SYNTHESIZE_PERSISTENT`). |
| `flags` | Reserved; must be zero. |
| `generation` | (Output) The new generation counter value after the change. |
| `template_sd_ptr`, `template_sd_len` | Optional mount-level SD template. Max 64 KiB. |

The kernel:

1. Validates the fd and identifies the superblock.
2. Validates the policy value. Attempts to set `KACS_MOUNT_POLICY_UNMANAGED` (which is not settable via this ABI) are rejected with `-EINVAL`.
3. Validates the template SD if provided — must parse, must be within size limits.
4. Checks the caller's privileges. `SeTcbPrivilege` is required.
5. Atomically updates the superblock's policy class, the template SD, and increments the generation counter.
6. Writes the new generation to `args.generation`.
7. Returns 0.

The change is **atomic at the superblock level** — every future access against any inode on this superblock sees the new policy. There is no transitional state where some inodes are under the old policy and others under the new.

### Privilege requirement

> [!IMPORTANT]
> `SeTcbPrivilege` is the privilege gating this call. The privilege is held by peinit and a handful of other TCB components; not by ordinary services or administrators. In practice this means mount policy is configured at boot (by peinit, applying mount-time configuration) or by a privileged management daemon. Ad-hoc policy changes are rare.

The reasoning: changing a mount's policy is an administrative decision that affects the entire filesystem's access semantics. It belongs in the TCB tier, not in the regular-administrator tier. An ordinary administrator who wants to change a mount's policy goes through a tool that itself has the privilege.

### The template SD

The optional template SD is the default SD used during synthesis when the parent's inheritance does not yield one. The kernel stores the template on the superblock; subsequent synthesis-on-missing calls use it.

The template is a complete self-relative SD — owner, primary group, DACL, optional SACL. The kernel validates the template at policy-set time:

- Must be in self-relative format.
- Must include an owner (an SD without one is malformed and rejected).
- Must fit within 65,535 bytes total; the template specifically is capped at 64 KiB.
- ACLs and ACEs must parse.

A bad template at policy-set time is rejected with `-EINVAL`; the current policy and template are unchanged.

The template can be omitted entirely (zero pointers) for mounts that don't need one. The synthesis chain then falls through to the hardcoded fallback for any file whose parent doesn't yield an SD.

## kacs_get_mount_policy

The read syscall:

```
result = kacs_get_mount_policy(fd, args)
```

Same `fd` and `args` semantics, returning the current policy class, current template (if buffer provided), and current generation counter.

The kernel:

1. Validates the fd, identifies the superblock.
2. Checks the caller's privileges. `SeTcbPrivilege` is required to read.
3. Writes the policy class, generation, and template to `args` (template only if a buffer was provided).
4. Returns 0.

Like the write syscall, this is `SeTcbPrivilege`-gated. The mount policy is system-level state, exposed only to TCB-tier callers.

This is the kernel's read-back-the-current-policy interface. A management tool reads the policy, perhaps applies a transformation, and writes the new value back. The read-modify-write pattern is what most policy management code uses.

## The generation counter

A subtle but important feature: the kernel maintains a **monotonic generation counter** per superblock. Each successful `kacs_set_mount_policy` (or template change) increments it.

The purpose is **lazy invalidation**.

Without the counter, a policy change would either need to immediately propagate to every cached SD on the filesystem (expensive — could be millions of inodes) or accept that cached SDs from before the change remain valid (incorrect — the new policy might say different things).

With the counter, the kernel can do something smarter:

1. Each in-kernel cached SD or synthesised entry records the generation it was derived from.
2. At access time, the kernel compares the cached entry's generation against the superblock's current generation.
3. If they match, the cached entry is current; use it.
4. If they differ, the cached entry is stale; discard it and re-derive.

The kernel does **not** walk the filesystem at the policy change. The walk happens implicitly, one inode at a time, as accesses arrive. An access that arrives 30 seconds after the policy change is the first to invalidate that specific inode's cached state; the access pays a small cost to re-derive, but every subsequent access uses the new cached state.

This is why mount policy changes are cheap to apply. The expensive work (walking inodes) never happens; what happens instead is per-inode re-derivation on first access after the change.

### What the generation counter affects

The counter affects in-kernel **cached** state derived from the mount policy:

- **Missing-SD synthesised entries.** If a file had a missing SD and was synthesised on-the-fly, the synthesised SD is cached. A policy change invalidates this cache; the next access re-synthesises.
- **Ephemeral-synthesis caches for `facs_synthesize_ephemeral` mounts.** Same.
- **Template-derived information** — the kernel's representation of the mount template is regenerated on next access.

The counter does **not** affect:

- **On-disk SDs.** A file that has a stored SD continues to have that SD. The policy change doesn't rewrite stored SDs.
- **Already-open file descriptors.** The cached granted mask on an fd is independent of the mount-policy generation; it's the result of the access check at open time. Policy changes after open don't affect open fds. (This is the standard check-at-open model.)
- **Inodes not currently in cache.** They have nothing to invalidate; the next access just goes through the normal path with the current policy.

### Reading the counter

The generation counter is exposed in the output of `kacs_get_mount_policy`. A management tool can read the counter to know when policy was last changed, or compare against a previously-read value to detect external changes.

The counter is per-superblock; different filesystems have unrelated counter values. The counter starts at some implementation-defined value at mount time and increases monotonically with each policy change.

## What happens at a policy change

To summarise, when `kacs_set_mount_policy` succeeds:

1. The superblock's policy class field is updated.
2. The superblock's template SD is updated.
3. The superblock's generation counter is incremented.
4. Open file descriptors against files on this mount are unaffected (cached granted masks intact).
5. Cached in-memory state derived from the old generation will be invalidated on first access after the change.
6. Files with stored SDs continue to have those SDs.
7. The filesystem itself is not walked, not modified, not reorganised.

The change is essentially a small superblock update plus a lazy invalidation marker. The work happens at future accesses, distributed across actual usage.

## What policy changes do not do

A few clarifications:

- **They don't rewrite SDs on disk.** A change from `facs_synthesize_ephemeral` to `facs_synthesize_persistent` doesn't go back and write synthesised SDs to disk. Files that were synthesised under the old policy continue to have no on-disk SD; the next access re-synthesises (now writes back, per the new persistent policy).
- **They don't close open files.** Processes with handles to files on the mount continue to have those handles. The cached granted masks are unchanged.
- **They don't propagate across mounts.** A policy change on one mount doesn't affect any other mount, even if they reference the same underlying device (per the per-superblock rule).
- **They don't change the FACS handle model.** The new policy applies to future access checks; existing handles use their cached masks per usual.

## Use patterns

A handful of common patterns for managing mount policy:

**Boot-time policy.** peinit applies the policy class and template for each mounted filesystem at boot. The configuration source is typically registry-based; peinit reads the configuration and calls `kacs_set_mount_policy` for each mount.

**Migration from a non-Peios filesystem.** Mount with `facs_synthesize_persistent` initially. Let usage gradually adopt files into having stored SDs. Once enough have been adopted, switch to `facs_deny_missing` (or leave it as `synthesize_persistent` if you want the synthesis-on-missing safety net).

**Removable media insertion.** When a removable filesystem is mounted (a USB drive, an optical disc), the mount setup chooses `facs_synthesize_ephemeral`. The volume's contents become reachable without modifying the on-disk metadata; ejection cleanly leaves the media untouched.

**Policy uplift on a hardened deployment.** A deployment starting with `facs_synthesize_ephemeral` everywhere (permissive) can migrate to `facs_synthesize_persistent` (filling in stored SDs) and then to `facs_deny_missing` (strict mode). Each transition is one `kacs_set_mount_policy` call per mount.

## Errors

Possible failures from `kacs_set_mount_policy`:

| Error | Cause |
|---|---|
| `-EBADF` | The fd is invalid. |
| `-EPERM` | The caller does not hold `SeTcbPrivilege`. |
| `-EINVAL` | Setting an `unmanaged` policy via the public ABI; setting an unknown policy value; non-zero reserved flags; malformed template SD; template size exceeded. |

Possible failures from `kacs_get_mount_policy`:

| Error | Cause |
|---|---|
| `-EBADF` | The fd is invalid. |
| `-EPERM` | The caller does not hold `SeTcbPrivilege`. |
| `-ERANGE` | A template buffer was provided but is smaller than the template. The required size is written to the output. |

In normal operation both calls succeed. Failures are typically privilege issues or malformed inputs.

## See also

- [Policy classes](/peios/using-peios/mount-policies/policy-classes.md) — what each settable class does.
- [mount](/peios/using-peios/mount-policies/mount.md) — setting the policy from the command line at attach time.
- [Privileges](/peios/security-fundamentals/privileges/overview.md) — the privilege model behind the SeTcbPrivilege gate.

---

# mount

_Peios / Using Peios / Mount policies_

> Attach a filesystem to the Peios mount tree — new mounts, binds, moves, remounts and propagation changes — and apply a KACS mount policy at mount time.

`mount` attaches a filesystem to the Peios mount tree. With no operands it instead *lists* what is currently mounted. It is a faithful reworking of the util-linux `mount(8)` surface for Peios, with two structural differences: Peios has **no `/etc/fstab` and no `/etc/mtab`**, so every mount is described entirely on the command line and live mount state is read from the kernel; and a `mount` can apply a **KACS mount policy** to the new filesystem at attach time (see [Mount policies](/peios/using-peios/mount-policies/overview.md)).

```
mount [-t TYPE] [-o OPTIONS] SOURCE TARGET
mount [-o remount,OPTIONS] TARGET
mount --bind|--rbind|--move SOURCE TARGET
mount --make-shared|--make-private|... TARGET
mount [-l] [-t TYPE]
```

Internally `mount` uses the fd-based mount API (`fsopen` / `fsconfig` / `fsmount` / `move_mount`, plus `open_tree` and `mount_setattr`), never the classic single-shot `mount(2)`. This matters in one place you can see: when a mount fails, the kernel's `fs_context` message log is drained and printed as the reason, even without `-v`.

## Operands and argument shapes

Because there is no fstab to consult, operand resolution is strict:

- **No operands, no verb** — list mode (see [below](#list-mode)).
- **Two operands** — `SOURCE TARGET`.
- **One operand** — an error. In util-linux a lone operand is resolved through fstab; Peios has none, so a single operand cannot be turned into a `SOURCE TARGET` pair. Supply both, or use `--source` / `--target`.
- **A lone target** is valid only for `-o remount` and for a standalone propagation change (`--make-*`), which act on an existing mount point.

`--source SRC` and `--target DIR` name the operands explicitly and may be combined with a single positional. `--target-prefix DIR` prepends `DIR/` to the target after it is chosen. Options may be interspersed with operands (`mount SRC -o ro TGT`), matching util-linux.

Paths, `-o key=value` values, labels and UUIDs are handled as **opaque byte strings**, never assumed to be UTF-8; an embedded NUL is a usage error.

### Canonicalisation

By default source and target paths are canonicalised (made absolute, symlinks resolved). `-c` / `--no-canonicalize` disables that; `X-mount.nocanonicalize[=source|target]` is the `-o` form and can disable it for just one side. The final path handed to the kernel is protected against a TOCTOU symlink swap on its last component.

## Operation modes (verbs)

`mount` performs one of several operations. The **structural verbs** — bind, rbind, move, beneath and remount — are mutually exclusive with one another. A **propagation** change is *not* a structural verb: it may stand alone on a target, or trail another verb or a new mount in the same command (applied afterwards as one or more `mount_setattr` steps), and several may be combined.

| Verb | How to request it | What it does |
|---|---|---|
| **New mount** | `mount [-t T] SRC TGT` | Attach a fresh instance of the filesystem at `SRC` onto `TGT`. |
| **Bind** | `-B` / `--bind`, or `-o bind` | Make an existing subtree visible at a second location. |
| **Recursive bind** | `-R` / `--rbind`, or `-o rbind` | Bind a subtree together with every mount underneath it. |
| **Move** | `-M` / `--move`, or `-o move` | Relocate an existing mount to a new mount point. |
| **Move beneath** | `--beneath SRC TGT` | Attach `SRC` *beneath* the mount currently at `TGT` (the kernel enforces several constraints; violations surface as exit 32). |
| **Remount** | `-o remount[,...] TGT` | Change the options of an existing mount (see [Remounting](#remounting)). |
| **Propagation** | `--make-*` / `--make-r*`, or the `-o` tokens below | Change how mount/unmount events propagate across a subtree. |
| **List** | `mount` / `mount -l` | Print the current mounts. |

### Propagation flags

Each of these sets the propagation type of the mount at the target. The `--make-r*` (and `r`-prefixed `-o`) forms apply recursively to the whole subtree; the recursive form is all-or-nothing (it either applies to the entire subtree or to none of it).

| Flag | `-o` token | Recursive flag | Recursive `-o` token | Meaning |
|---|---|---|---|---|
| `--make-shared` | `shared` | `--make-rshared` | `rshared` | Events propagate to and from peer mounts. |
| `--make-slave` | `slave` | `--make-rslave` | `rslave` | Events propagate *in* from the master but not back out. |
| `--make-private` | `private` | `--make-rprivate` | `rprivate` | No propagation either way. |
| `--make-unbindable` | `unbindable` | `--make-runbindable` | `runbindable` | Private, and cannot be bind-mounted. |

A freshly created mount, bind or move is **private** by default unless its destination's parent is shared, in which case it joins that peer group — the standard kernel rule.

## Source specification

| Form | Resolution |
|---|---|
| Device path (`/dev/sda1`) | Used directly. |
| `-L LABEL` / `LABEL=`, `-U UUID` / `UUID=`, `PARTLABEL=`, `PARTUUID=` | Resolved to a device via libblkid. No match is a mount failure (exit 32). |
| A directory or regular file | Used directly (a bind source or target; a file may be a bind target). |
| A pseudo source (`tmpfs`, `proc`, `sysfs`, `none`, …) | Passed through; `-t` is required because it cannot be probed. |
| An image file | Attached through a loop device (see [Loop devices](#loop-devices)). |

## Filesystem type

`-t TYPE` names the type explicitly. `-t auto`, or omitting `-t` entirely, asks libblkid to **safely probe** the source; an ambiguous or multiply-signed device is refused (exit 32) rather than guessed at. `X-mount.auto-fstypes=LIST` constrains the probe candidates. Type *lists* and `no<type>` negation are not accepted in the mounting path (they remain meaningful only as a [listing filter](#list-mode)).

## The `-o` option language

`-o` takes a comma-separated list of `key` or `key=value` tokens and is repeatable (all occurrences are joined). A `key="..."` double-quoted value protects embedded commas; the `key=value` split is on the **first** `=`. Every token is sorted into one of the following categories.

### Per-mount attributes

Applied to the mount itself. Each accepts its negation; `ro`/`rw` additionally accept a `=vfs` / `=fs` / `=recursive` scope qualifier (bare is `=vfs`, non-recursive; `=fs` targets the superblock read-only flag; `=recursive` applies to the whole subtree).

| Option | Effect |
|---|---|
| `ro` / `rw` | Read-only / read-write. |
| `suid` / `nosuid` | Honour / ignore set-user-ID bits. |
| `dev` / `nodev` | Allow / disallow device nodes. |
| `exec` / `noexec` | Allow / disallow execution. |
| `atime` / `noatime` | Update / never update access times. |
| `relatime` / `norelatime` | Relative-atime updates on or off. |
| `strictatime` / `nostrictatime` | Strict-atime updates on or off. |
| `diratime` / `nodiratime` | Directory access-time updates on or off. |
| `nosymfollow` | Do not follow symlinks on this mount. There is no positive `symfollow` token — the attribute is cleared only via a remount mask. |

The atime tokens share a single mode field; the last one specified wins.

### Superblock flags

| Option | Effect |
|---|---|
| `sync` / `async` | Synchronous / asynchronous writes. |
| `dirsync` | Synchronous directory updates. |
| `lazytime` / `nolazytime` | Lazy on-disk timestamp updates on or off. |
| `iversion` / `noiversion` | Inode version counting on or off. |
| `silent` / `loud` | Suppress or emit certain kernel messages. |
| `mand` / `nomand` | Obsolete (removed from the kernel). Accepted and ignored with a note under `-v`. |

### Filesystem-specific parameters

Any token not recognised above is forwarded verbatim to the filesystem: `key=value` as a string parameter, a bare `key` as a flag. There is no built-in allow-list and no length limit; a rejection by the filesystem is reported with the offending key and the kernel's message.

For example, StrataFS takes its precedence-ordered directory stack through
`strata=`:

```sh
mount -t stratafs none /bin -o 'strata=/lcl/bin+create:/usr/bin+ro'
```

See [StrataFS](/peios/using-peios/disks-and-filesystems/stratafs/overview.md) for the stack flags, merge
and write-routing model, and the `stratafs` inspection command.

### Userspace-only tokens

These never reach the filesystem. They include the meta-verbs `remount`, `bind`, `rbind`, `move`; the loop controls `loop` / `loop=/dev/loopN`, `offset=`, `sizelimit=` (numeric values accept `K`/`M`/`G`/`T` and `KiB`/`MiB`/… suffixes); the propagation tokens listed [above](#propagation-flags); and `defaults`, which expands to `rw,suid,dev,exec,async` (later tokens override it).

### Functional `X-mount.*` options

| Option | Effect |
|---|---|
| `X-mount.mkdir[=mode]` | Create the target directory if missing (default mode `0755`; the alias of `-m`). |
| `X-mount.subdir=DIR` | Attach subdirectory `DIR` of a freshly mounted filesystem at the target. Effective only for a new-instance mount; silently ignored (noted under `-v`) for bind/move/remount/propagation. |
| `X-mount.noloop` | Suppress the implicit loop device for a regular-file source. |
| `X-mount.auto-fstypes=LIST` | Constrain the `-t auto` probe to these types. |
| `X-mount.nocanonicalize[=source\|target]` | The `-o` form of `-c`; with `=source` or `=target` it disables canonicalisation for just that path. |

`X-mount.idmap` and `X-mount.owner` / `group` / `mode` are **not supported** and are rejected with a usage error: Peios ownership is SID/security-descriptor based, so the fix is to add a SID/ACE to the descriptor rather than remap or chown.

### KACS mount policy

This is the genuinely Peios-specific part of `mount`. A mount policy is a per-superblock setting that governs how FACS treats the filesystem; the full model is described in [Mount policies](/peios/using-peios/mount-policies/overview.md) and its detail pages. `mount` can set that policy at attach time:

| Option | Policy class |
|---|---|
| `-o policy=deny-missing` | `facs_deny_missing` — a file with no SD is unreachable. |
| `-o policy=synth-ephemeral` | `facs_synthesize_ephemeral` — a missing SD is synthesised in memory only. |
| `-o policy=synth-persist` | `facs_synthesize_persistent` — a missing SD is synthesised and written back. |
| `--synth-sddl SDDL` | Provide the mount-level SD template used during synthesis (only valid with a `synth-*` policy). |

`policy=unmanaged` is **not** user-settable; only the kernel sets the unmanaged class, for its own pseudo-filesystems. `policy=` is valid only on a **new mount** of a real filesystem — combining it with bind/move/remount/propagation, or with list mode, is a usage error. See [Policy classes](/peios/using-peios/mount-policies/policy-classes.md) for what each class does and [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md) for how the SD is physically stored.

The policy is applied to the *detached* filesystem before it is attached: if setting it fails, nothing is ever published with an unintended policy (no rollback needed). Because setting a mount policy is a `SeTcbPrivilege`-gated operation (see [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md)), a caller without that privilege gets a clean `EPERM` (exit 1) and no mount. `--synth-sddl` is validated client-side first — it must be well-formed SDDL and must include an owner.

Peios applies no coarse `uid==0` check anywhere: the `mount` applet is not installed set-user-ID, and every privileged action is authorised per-operation by KACS.

### Remounting

`-o remount` changes the options of an existing mount without detaching it. Peios does **not** perform the util-linux "read the old flags and re-supply them" dance — the fd-based API applies deltas rather than resetting, so each remount touches only what you name. Per-mount attributes (`ro`/`rw`, `nosuid`, atime, …) are changed via `mount_setattr`; superblock flags, filesystem parameters and `ro=fs`/`rw=fs` go through the superblock reconfigure path. `=recursive` remounts the whole subtree, all-or-nothing.

A **bind** mount shares its source's superblock, so any superblock-level option on a bind remount (a Category-B flag, a filesystem parameter, or `ro=fs`/`rw=fs`) is refused as a usage error rather than silently mutating the shared superblock for every mount of that filesystem. Only per-mount VFS attributes are valid on a bind remount.

## Loop devices

A regular-file source is backed by a loop device automatically **when the type is unspecified or the filesystem is recognised by libblkid**; `X-mount.noloop` suppresses this. `-o loop` forces auto-allocation of a free device, `loop=/dev/loopN` names one, and `offset=` / `sizelimit=` select a region of the file (they imply loop and are an error against a real block device). A backing file already attached at the same offset and size is *reused* rather than doubly attached, to avoid corruption. Loops created by `mount` are auto-cleared by the kernel on unmount, so they do not leak; `umount -d` force-clears the rest (see [`umount`](/peios/using-peios/mount-policies/umount.md)).

## Other flags

| Flag | Effect |
|---|---|
| `-t`, `--types TYPE` | Filesystem type, or `auto`. |
| `-o`, `--options LIST` | Mount options (above); repeatable. |
| `-r`, `--ro`, `--read-only` | Mount read-only (`-o ro`). |
| `-w`, `--rw`, `--read-write` | Mount read-write and **forbid** the automatic read-only fallback on a write-protected device (`-o rw`). |
| `--source SRC` / `--target DIR` | Name the operands explicitly. |
| `--target-prefix DIR` | Prepend `DIR/` to the target. |
| `-B`, `--bind` / `-R`, `--rbind` / `-M`, `--move` / `--beneath` | The structural verbs. |
| `--make-*`, `--make-r*` | Propagation changes. |
| `--exclusive` | Force a unique superblock instance (no reuse). Meaningful only for multi-instance filesystems (e.g. `tmpfs`) or read-only block mounts; on an already-mounted writable block device it fails with `EBUSY` (exit 32). |
| `-m`, `--mkdir[=MODE]` | Create the target directory if missing (default mode `0755`). |
| `-L`, `--label LABEL` / `-U`, `--uuid UUID` | Select the source by filesystem label / UUID. |
| `-c`, `--no-canonicalize` | Do not canonicalise paths. |
| `-f`, `--fake` | Dry run: parse, resolve and plan everything but skip the mount syscalls and the policy step. |
| `-v`, `--verbose` | Narrate resolved values and each syscall; drain the kernel `fs_context` log. Repeatable but `-vv` is the same as `-v`. |
| `-l`, `--show-labels` | In list mode, append each filesystem's label. |
| `--onlyonce` | Skip the mount if it is already present (a driver-aware check against live mount state, not a naive string match). |
| `-N`, `--namespace NS` | Operate inside mount namespace `NS` (a PID, an ns file path, or a named namespace). Source resolution happens in the caller's namespace; the mount lands in the target namespace. |
| `-i`, `--internal-only` | Do not invoke a `mount.<type>` helper. |
| `-n`, `--no-mtab` | Accepted and ignored (Peios has no mtab). |
| `--synth-sddl SDDL` | KACS synth-policy template SD (above). |
| `-h`, `--help` / `-V`, `--version` | Standard. |

No external mount helpers ship in this version, so a network or FUSE type fails with "no helper for type" (exit 1) — a clean deferral, not a crash.

## List mode

With no operands, `mount` prints one line per mount, read from `/proc/self/mountinfo`:

```
SOURCE on TARGET type FSTYPE (OPTIONS)
```

`mount -t TYPE` with no operands *filters* the list by filesystem type instead of mounting; here type lists (`-t ext4,xfs`) and `no<type>` negation (`-t nosysfs`) are honoured. `-l` appends ` [LABEL]` to each line when a label is known.

Details of the rendering: the option field is the positional VFS-then-superblock merge with a coalesced `ro`/`rw`, not sorted; mountinfo octal escapes are decoded; control characters in the mount point become `?`; and the source is shown as the kernel recorded it, with `/dev/loopN` resolved to its backing file and `/dev/dm-N` to `/dev/mapper/<name>`. The listing shows only mounts the caller's token may observe; a partial or empty view is correct, not an error, and the paths of filtered-out parents are never reconstructed.

For a device-oriented view of what is available to mount, use [`lsblk`](/peios/using-peios/mount-policies/lsblk.md).

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Incorrect invocation or a permission denial — bad flags, mutually-exclusive verbs, a lone operand, an invalid `policy=` or `--synth-sddl`, an authorisation failure (`EPERM`/`EACCES`), an embedded NUL, or "no helper for type". |
| `2` | System error (out of memory, no free loop device, cannot fork). |
| `4` | Internal error (an invariant failure). |
| `8` | Interrupted (SIGINT), after signal-safe cleanup. |
| `32` | Mount failure — a syscall in the flow failed, a label/UUID had no match, a probe was undetermined, or a `--beneath`/move/`--exclusive` kernel refusal. |
| `126` | An external `mount.<type>` helper was found but failed to execute (moot until a helper ships). |

Code `16` (mtab) is never produced. Code `64` (some succeeded, some failed) only arises when several source arguments are given and at least one succeeds while another fails.

---

# umount

_Peios / Using Peios / Mount policies_

> Detach a filesystem from the Peios mount tree by mount point or by source, with lazy, forced, recursive and all-targets variants.

`umount` detaches a filesystem from the Peios mount tree. It is the counterpart of [`mount`](/peios/using-peios/mount-policies/mount.md) and, like it, reads live mount state from the kernel (`/proc/self/mountinfo`) rather than any `/etc/mtab` — Peios has none. Each operand is resolved against that live state and unmounted with `umount2(2)`.

```
umount [-lfRA] [-dr] TARGET|SOURCE...
```

## Operands and how they are resolved

Each operand is either a **mount point** or a **source**:

- If the (canonicalised) operand is itself a mount point in the current mount table, it is unmounted directly. This is always unambiguous.
- Otherwise the operand is treated as a **source** and matched against the sources in the mount table. If it is mounted in exactly one place, that place is unmounted. If it is mounted in **several** places, `umount` refuses with an error naming the candidates — resolve it by naming a specific mount point, or use `-A` to unmount all of them.

If an operand matches nothing, it is "not mounted": normally an error (exit 32), but `-g` makes that a success and `-q` suppresses the message.

Several operands may be given in one invocation, and options may be interspersed with them (`umount /a -f /b`). Paths are handled as opaque bytes; an embedded NUL is a usage error. By default each operand is canonicalised; `-c` disables that and additionally selects `UMOUNT_NOFOLLOW` so the kernel does not follow a symlink in the final component.

## Recursive and all-targets unmounts

Two flags expand a single operand into multiple unmounts. Within one operand the expansion **stops on the first failure**.

- **`-R` / `--recursive`** unmounts the target and everything mounted underneath it, including any over-mount stack at a single point, ordered deepest-first.
- **`-A` / `--all-targets`** unmounts *every* mount point of the given source in the current mount namespace. This is the live-mountinfo "unmount everywhere" complement to source resolution; it is not an fstab feature.
- **`-A -R` together** compose: for each mount point of the source, recurse underneath it.

`-R` and `-r` are mutually exclusive (combining them is a usage error, exit 1).

## Flags

| Flag | Effect |
|---|---|
| `-l`, `--lazy` | Detach the filesystem now and clean up references later (`MNT_DETACH`). |
| `-f`, `--force` | Force the unmount (`MNT_FORCE`), e.g. for an unreachable server. |
| `-R`, `--recursive` | Unmount the target and everything under it (see above). |
| `-A`, `--all-targets` | Unmount every mount point of the given source (see above). |
| `-d`, `--detach-loop` | After unmounting, free the backing loop device. Best-effort and verified: it clears the device only if it still backs the mount just removed, so a recycled loop number is never clobbered. Usually redundant, since `mount`-created loops auto-clear on unmount. |
| `-r`, `--read-only` | If the unmount fails, remount the filesystem read-only instead (via `mount_setattr`). Mutually exclusive with `-R`. |
| `-g`, `--graceful` | Exit `0` when the target is absent or not mounted (rather than failing). Applied unconditionally. |
| `-q`, `--quiet` | Suppress "not mounted" messages. |
| `-c`, `--no-canonicalize` | Do not canonicalise paths; selects `UMOUNT_NOFOLLOW`. Applied unconditionally. |
| `-v`, `--verbose` | Say what is being unmounted. Repeatable. |
| `-N`, `--namespace NS` | Enter mount namespace `NS` (a PID, an ns file path, or a named namespace) before reading its mount table and unmounting. All work happens inside that namespace. |
| `-n`, `--no-mtab` | Accepted and ignored (no mtab on Peios). |
| `-i`, `--internal-only` | Do not invoke a `umount.<type>` helper (none ship). |
| `--fake` | Dry run: resolve operands and report, but skip the unmount syscalls. |
| `-h`, `--help` / `-V`, `--version` | Standard. |

The `umount2` flag mapping is direct: `-l` → `MNT_DETACH`, `-f` → `MNT_FORCE`, `-c` (or a symlinked final component) → `UMOUNT_NOFOLLOW`. `MNT_EXPIRE` is deliberately not exposed, matching util-linux.

### On privilege

As with `mount`, there is no coarse `uid==0` check: the applet is not set-user-ID and every unmount is authorised per-operation by KACS. Where util-linux silently suppresses an option for non-root users (`-c`, and `-g`'s effectiveness), Peios applies it unconditionally.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success (or a `-g` no-op on an absent target). |
| `1` | Incorrect invocation or a permission denial — including `-R` with `-r`, a missing operand, an ambiguous source, an embedded NUL, or an authorisation failure. |
| `2` | System error (e.g. cannot read the mount table). |
| `8` | Interrupted (SIGINT). |
| `32` | Unmount failed, or the target is not mounted (unless `-g`). |
| `64` | Several source/target arguments were given and at least one succeeded while another failed. |
| `126` | An external `umount.<type>` helper was found but failed to execute (moot until a helper ships). |

A single `-R` / `-A` / `-A -R` invocation stops on the first failure and yields `32`; the aggregate `64` only appears across multiple top-level arguments. To see what is currently mounted before unmounting, use [`mount`](/peios/using-peios/mount-policies/mount.md) with no arguments or [`lsblk`](/peios/using-peios/mount-policies/lsblk.md).

---

# lsblk

_Peios / Using Peios / Mount policies_

> List block devices as a tree, flat list, JSON or key=value pairs — with filesystem identity, topology, and SD-derived owner and mode columns.

`lsblk` lists the block devices on the system and their relationships — disks, their partitions, and the device-mapper and loop devices layered on top. It is the device-oriented companion to [`mount`](/peios/using-peios/mount-policies/mount.md): where `mount` with no arguments shows what is *mounted*, `lsblk` shows what is *available to mount* and what filesystem sits on each device.

```
lsblk [options] [DEVICE...]
```

By default it prints a tree, one row per device with children indented beneath their parent. Given one or more `DEVICE` operands (a name like `sda`, a full path like `/dev/sda`, or anything resolving to a device node), it re-roots the output at those devices.

## Where the data comes from

Each column is sourced from one of four places, and any that cannot be read degrades to an empty cell rather than aborting the run:

- **sysfs** (`/sys/block`) — the device tree, sizes, and the topology/hardware columns. Peios leaves `/sys/block` unpatched, so this is the standard no-udev path.
- **libblkid** — filesystem identity: `FSTYPE`, `FSVER`, `UUID`, `LABEL`, and the partition-table columns. libblkid is opened at runtime.
- **The device node's security descriptor** — `OWNER` and `MODE`, read the same way [`ls -l`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) reads them.
- **The `/dev/disk/by-*` symlink farm** — `ID-LINK`. Populated by the device manager once it has run; there is deliberately no `/run/udev/data` parser, so the column reads the links themselves and is empty in the initramfs, where no device manager runs.

## Output modes

The mode selects how the rows are formatted; it does not change which columns are shown.

| Flag | Mode |
|---|---|
| *(default)* | Indented tree. |
| `-l`, `--list` | Flat list — the same columns, no tree glyphs. |
| `-J`, `--json` | JSON. Flag columns render as bare booleans and `MOUNTPOINTS` as an array; children nest under a `children` key. |
| `-P`, `--pairs` | `KEY="value"` pairs, one device per line. |
| `-r`, `--raw` | Raw, space-separated. Values that could contain a space or control character are hex-escaped (`\xNN`) so fields stay parseable. |
| `-T`, `--tree[=COLUMN]` | Force tree output even alongside `-l`, optionally attaching the tree glyphs to `COLUMN` instead of `NAME`. |

## Columns

With no column flag, `lsblk` prints the default set: `NAME`, `MAJ:MIN`, `RM`, `SIZE`, `RO`, `TYPE`, `MOUNTPOINTS`. Three flags swap in a preset, and `-o` names an explicit list (which wins over all of them):

| Flag | Column set |
|---|---|
| `-o`, `--output LIST` | Exactly the comma-separated columns named (case-insensitive; an unknown name is a usage error). |
| `-O`, `--output-all` | Every available column. |
| `-f`, `--fs` | Filesystem view: `NAME`, `FSTYPE`, `FSVER`, `LABEL`, `UUID`, `MOUNTPOINTS`. |
| `-m`, `--perms` | Permissions view: `NAME`, `SIZE`, `OWNER`, `MODE`. |

The available columns are `NAME`, `KNAME`, `PATH`, `MAJ:MIN`, `FSTYPE`, `FSVER`, `LABEL`, `UUID`, `PTUUID`, `PTTYPE`, `PARTTYPE`, `PARTLABEL`, `PARTUUID`, `MOUNTPOINT`, `MOUNTPOINTS`, `SIZE`, `RO`, `RM`, `HOTPLUG`, `TYPE`, `OWNER`, `MODE`, `MODEL`, `VENDOR`, `REV`, `SERIAL`, `TRAN`, `HCTL`, `ALIGNMENT`, `MIN-IO`, `OPT-IO`, `PHY-SEC`, `LOG-SEC`, `STATE`, `ROTA`, `SCHED`, `PKNAME`, and `ID-LINK`.

### The `OWNER` and `MODE` columns

`OWNER` and `MODE` describe the *device node*, not the filesystem on it, and they mirror [`ls -l`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) exactly — there is no `GROUP` column and no POSIX permission bits, because access to a device node is governed by its security descriptor, not by a mode. `OWNER` is the owner SID (`S-1-…`); `MODE` is a three-character `[type][x][+]`: the device-type character (`b` for a block device, `c` for a character device), an `x` slot (never set for a block device), and a `+` when the DACL is inheritance-protected. When the SD cannot be read — for instance on a non-Peios host with no KACS syscalls — `OWNER` shows `?` and `MODE` degrades honestly.

## Filtering, sorting and shaping

| Flag | Effect |
|---|---|
| `-a`, `--all` | Include empty (zero-size) devices, which are hidden by default. |
| `-d`, `--nodeps` | Do not print a device's holders or slaves (drop the children). |
| `-I`, `--include LIST` | Show only devices with these major numbers (comma-separated). |
| `-e`, `--exclude LIST` | Exclude devices by major number. The default is `1` (RAM disks); an explicit `-e` replaces that default, and `-a` clears exclusions entirely. |
| `-s`, `--inverse` | Print dependencies in inverse order (holders above the devices they depend on). |
| `-M`, `--merge` | Collapse a subtree shared by several parents (a multipath device) to a single occurrence. |
| `-E`, `--dedup COLUMN` | Drop rows whose `COLUMN` value duplicates an earlier one. |
| `-x`, `--sort COLUMN` | Sort siblings by `COLUMN` (numeric columns sort by value, not text). |

## Formatting

| Flag | Effect |
|---|---|
| `-b`, `--bytes` | Print `SIZE` as an exact byte count instead of a human-readable value. |
| `-p`, `--paths` | Print full `/dev` paths in the `NAME` column. |
| `-n`, `--noheadings` | Omit the header row. |
| `-i`, `--ascii` | Draw the tree with ASCII characters instead of box-drawing glyphs. |
| `-y`, `--shell` | Render column keys shell-safe (`MAJ:MIN` becomes `MAJ_MIN`). |
| `-w`, `--width NUM` | Truncate each table row to `NUM` columns wide. |
| `--sysroot DIR` | Read sysfs, the mount table and `/dev` from `DIR` instead of `/` (chiefly for testing against a fixture). |
| `-h`, `--help` / `-V`, `--version` | Standard. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Incorrect invocation (an unknown column, a malformed major-number or width value) or a failed run (for example, `/sys/block` is unreadable). |

`lsblk` uses this single non-zero code, matching util-linux. A per-device read failure is not fatal: it degrades the affected columns to empty or `?` and the run continues. A broken output pipe (piping into `head`, for instance) is treated as success.

---

# Disks and filesystems

_Peios / Using Peios / Disks and filesystems_

> The tools Peios ships for creating, checking and inspecting ext2/3/4 and FAT filesystems, and why they are packaged from upstream rather than rewritten.

Creating a filesystem is the one operation that happens *before* Peios has any say over it. A block device holds bytes; a filesystem is a structure imposed on those bytes; only when that structure is mounted does the kernel begin making access decisions about what it contains. Everything on this page happens on the early side of that line.

Peios ships two upstream families: **e2fsprogs** for ext2/3/4, and **dosfstools** for FAT. Both are the tools you know from any Linux system. e2fsprogs carries a small Peios patchset, and the one place it diverges is security descriptors, which [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md) covers. dosfstools carries no functional patch at all, for a reason worth stating up front: a FAT filesystem has no extended attributes, so it has nowhere to keep a security descriptor and there is nothing for that work to extend.

## Why these tools are packaged, not rewritten

Most user-facing commands on Peios come from peiosutils, which reworks each tool for the Peios security model — `mount` grew a `policy=` option, `lsblk` reports SD-derived owner and mode, `ls` reads SDs rather than POSIX modes. `mkfs` and `fsck` are deliberately not in that set.

The reason is where the security seam falls. **`mount` is the seam**: it is the operation that takes a filesystem and places it under KACS, choosing the mount policy that governs every access from that point on. `mkfs` and `fsck` sit below the seam. They write and repair a Linux-compatible on-disk format that Peios deliberately mirrors byte for byte — an ext4 filesystem made on Peios is an ext4 filesystem, readable anywhere. Rewriting them would mean reimplementing a format Peios does not want to change, and taking on the correctness burden of a filesystem checker for no security benefit.

So Peios packages e2fsprogs from upstream and carries a patch series for the one thing upstream has no concept of.

## What ships

| Package | Contents |
|---|---|
| `e2fsprogs` | The tools below. |
| `e2fsprogs-devel` | Headers, linker symlinks and pkg-config files for building against the libraries. |
| `e2fsprogs-static` | Static archives. |
| `libext2fs`, `libe2p`, `libcom-err`, `libss`, `libuuid` | The runtime shared libraries, packaged separately so a consumer can depend on one without pulling the tools. |

The tools themselves:

| Tool | Purpose |
|---|---|
| `mke2fs`, `mkfs.ext2`, `mkfs.ext3`, `mkfs.ext4` | Create a filesystem. This is where Peios security descriptors enter. |
| `e2fsck`, `fsck.ext2`, `fsck.ext3`, `fsck.ext4`, `fsck` | Check and repair a filesystem. |
| `tune2fs` | Change parameters on an existing filesystem, including enabling features after the fact. |
| `resize2fs` | Grow or shrink a filesystem. |
| `dumpe2fs` | Print superblock and block-group information. |
| `debugfs` | Interactive low-level access to a filesystem image, including reading and writing extended attributes directly. |
| `blkid`, `findfs` | Identify filesystems by label, UUID or type. |
| `e2label`, `e2image`, `e2undo`, `e2freefrag`, `filefrag`, `badblocks`, `logsave` | Labelling, imaging, undo, fragmentation and block-scanning utilities. |
| `chattr`, `lsattr` | Read and set ext2/3/4 inode attributes. |
| `uuidgen` | Generate a UUID. |

`libuuid` comes from e2fsprogs; `libblkid` comes from the util-linux libraries. The split is arbitrary but fixed — each library has exactly one owning package, so the two sources never both ship the same file.

From `dosfstools`:

| Tool | Purpose |
|---|---|
| `mkfs.fat`, `mkfs.vfat`, `mkfs.msdos` | Create a FAT12/16/32 filesystem. |
| `fsck.fat`, `fsck.vfat`, `fsck.msdos` | Check and repair a FAT filesystem. |
| `fatlabel` | Read or set a FAT volume label. |

The pre-4.0 aliases (`mkdosfs`, `dosfsck`, `dosfslabel`) are deliberately not shipped — they exist for compatibility with a command-line history Peios does not have.

## Where these tools live

Everything above is in **`/usr/bin`**, reached as `/bin` through the runtime view. None of it is in `/sbin`: that is for daemons, which appear in service definitions, and a filesystem tool is a privileged binary a person runs.

The `fsck.<type>` backends are the exception, and they sit in **`/usr/libexec/fsck/`**:

```
/libexec/fsck/fsck.ext2   fsck.ext3   fsck.ext4
                fsck.fat    fsck.vfat   fsck.msdos
```

These are not commands you type. `fsck` picks a checker by exec'ing `fsck.<type>` for the filesystem type it detects or is given, so they are a private interface between the front-end and its backends — which is exactly what `libexec` distinguishes. `fsck` searches `/libexec/fsck` first, then the directories on your `PATH`, so a third-party checker installed elsewhere on `PATH` is still found.

The `mkfs.<type>` names stay in `/usr/bin` for the opposite reason: Peios ships no `mkfs` front-end, so nothing ever dispatches on them and they are only ever typed.

## FAT and the EFI system partition

The reason Peios ships FAT tooling at all is the **EFI system partition**. UEFI requires the ESP to be FAT, so a system that cannot format FAT cannot create its own boot partition.

An ESP is also the clearest case of a filesystem that holds no access control of its own. There is no extended-attribute channel, so no security descriptor is ever written to it, and none can be. Its entire access policy comes from the mount — necessarily one of the synthesising classes, since `facs_deny_missing` on a filesystem where every file is permanently missing an SD would make the whole partition unreachable. See [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

The practical consequence: the protection on your boot partition is the mount policy and the physical security of the disk, not a descriptor on the files. Treat the contents accordingly.

## Where this sits in a running system

`debugfs` is the tool to reach for when you want to inspect an image *offline* — without mounting it, and therefore without KACS being involved at all. It reads and writes extended attributes directly, which makes it the way to confirm that a security descriptor really landed on disk:

```
debugfs -R "ea_list <2>" /dev/vda2
```

Inode 2 is always the root directory of an ext filesystem, so this asks what extended attributes the filesystem's root carries.

`e2fsck` has a specific place in boot. Checking and repairing the root filesystem is the initramfs's job, not peinit's — by the time peinit runs, the root is already mounted, and a filesystem checker cannot repair a filesystem that is in use. See [The initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md).

## What is not here yet

**Partition tables other than GPT.** `part` writes GPT and only GPT. Peios boots through UEFI with no bootloader, so MBR has nothing to do on a Peios system — but a disk that already carries one is recognised and named rather than silently overwritten. Resizing, moving and MBR↔GPT conversion do not exist either. See [Partitioning](/peios/using-peios/disks-and-filesystems/partitioning.md).

**Filesystems other than ext2/3/4 and FAT.** The mount side understands XFS, Btrfs and NTFS as well — see [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md) — but Peios ships creation tools only for the ext and FAT families.

## Where to start

For the security-descriptor model — why a filesystem you intend to keep should carry a real SD from the moment it is created, and how the tree gets one — read [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md).

For the exact command surface, including the extended options and the Peios defaults baked into the binary, read [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md).

For how these tools are put to work copying a live system onto a disk that then boots itself, read [Installing to disk](/peios/using-peios/disks-and-filesystems/installing-to-disk.md).

For the names a disk keeps across reboots — `/dev/disk/by-uuid` and its siblings, and the device manager that maintains them — read [Stable device names](/peios/using-peios/disks-and-filesystems/stable-device-names.md).

For what happens to a filesystem once it is attached to the mount tree, read [Mount policies](/peios/using-peios/mount-policies/overview.md).

---

# Stable device names

_Peios / Using Peios / Disks and filesystems_

> Why /dev/vda is not a name to keep, what /dev/disk/by-uuid and its siblings are, who creates them, and where they are not available.

The kernel names a disk by the order it found it in: `/dev/vda`, `/dev/sda`, `/dev/nvme0n1`. That order is whatever the hardware produced this time — a second controller that probed faster, a USB stick that was plugged in, a disk that moved to another port — so the same disk is not guaranteed the same name on the next boot. A name of that kind is fine for a command you type now and wrong for anything written down: a boot cmdline, a mount reference, a script.

Peios keeps kernel names for the moment and gives every disk a set of **stable names** for everything else. They live under `/dev/disk/`, as symlinks to whatever kernel name the device happens to have:

| Directory | Keyed by | Example |
|---|---|---|
| `by-uuid` | the filesystem's UUID, written when it was formatted | `4ED7-A6AC -> ../../vda2` |
| `by-label` | the filesystem's label | `PEIOS -> ../../vda` |
| `by-partuuid` | the GPT partition entry's UUID | `2036ae68-…-f118299d5446 -> ../../vda2` |
| `by-partlabel` | the GPT partition entry's name | |
| `by-id` | the hardware's own identity: model and serial, or WWN | `nvme-QEMU_NVMe_Ctrl_peiosnvme1 -> ../../nvme0n1` |
| `by-path` | the bus position the device sits at | |
| `by-diskseq` | the kernel's monotonic disk sequence number, for this boot only | |

Which one to use depends on what you mean. A *filesystem* is `by-uuid` — it follows the data if the disk is cloned, and it is what `root=UUID=` on the kernel command line names. A *partition* independent of what is on it is `by-partuuid`. A *physical disk* whatever is written to it is `by-id`. `by-label` is the human-friendly one and the only one that is not unique by construction.

Partitions appear under the same keys with the parent's identity plus `-partN`, so `by-id/nvme-…-part2` is the second partition of that NVMe disk.

## Who creates them

The names are made by the **device manager**, eudev, which peinit starts first in phase 2 of boot. As each block device appears — at boot, when it replays the kernel's enumeration, and afterwards whenever a device is plugged in — the device manager probes it (its partition table, and the filesystem signature on each partition) and creates the matching links. Unplug the device and the links go away.

That is the same daemon that loads a driver for each device the kernel reports, so a disk behind a modular controller gets its driver, its kernel name and its stable names from one pass. See [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) for where the service sits in the boot order; a service that opens a device by stable name should `Requires` it, because peinit does not release eudev's dependents until the boot-time replay has finished.

The same idea applies to network interfaces. The kernel names them `eth0`, `eth1` in probe order; the device manager renames each one by where it sits — `enp0s3` for the card in PCI slot 3 on bus 0, `eno1` for an onboard port the firmware numbers — so a configuration written against the name survives a second card or a driver that probes faster next time. `net.ifnames=0` on the kernel command line turns the renaming off.

The device manager does not decide who may open a device. That is the security descriptor on the node, seeded on `/dev` before any of this runs — see [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

## Where they are not available

**The initramfs has none of them.** No device manager runs there — only a single pass that loads drivers, described in [The initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md). The initramfs still honours `root=UUID=`, but it resolves the UUID by probing each block device directly rather than by looking in `/dev/disk/by-uuid`, and `lsblk` does the same. That is why the installer's cmdline works on a machine the initramfs has never seen: it never depended on the links.

**`by-diskseq` does not survive a reboot.** The sequence number is assigned in the order devices appear during *this* boot, which is precisely the property the other directories exist to avoid depending on. It is there for tools that need to tell a re-plugged device from the one it replaced.

## Where to go next

For writing a partition table that gives every partition a `by-partuuid` name, read [Partitioning](/peios/using-peios/disks-and-filesystems/partitioning.md).

For how the installer records the root filesystem's UUID into the kernel command line, read [Installing to disk](/peios/using-peios/disks-and-filesystems/installing-to-disk.md).

---

# Partitioning

_Peios / Using Peios / Disks and filesystems_

> part, the Peios partitioning tool: creating GPTs, adding and removing partitions, and what it refuses to touch.

A disk arrives from the factory as one undifferentiated run of sectors. Before a filesystem can live on it, something has to write down where each one begins and ends. That is a **partition table**, and on Peios the tool that writes one is `part`.

`part` manages **GPT** and nothing else. That is not an omission waiting to be filled: Peios boots through UEFI with no bootloader and no boot manager, so the MBR layout it would otherwise support has nothing to do on a Peios system.

## The shape of the tool

If you have used Windows, `part` occupies the slot `diskpart` occupies — but it is not the same shape, and the difference is deliberate.

| | `diskpart` | `part` |
|---|---|---|
| how you use it | an interactive shell: `select disk 0`, then act on it | one command, one device, one job |
| what it covers | partitions, formatting, drive letters, dynamic disks | the partition table |

Everything else `diskpart` bundles already has a home here: [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md) and `mkfs.vfat` make filesystems, `mount` mounts them, and Peios has no drive letters. And a `select`-then-act model is a hazard in a script, which is what usually calls `part` — a command that acts on "whatever was selected earlier" is one stray line away from acting on the wrong disk.

## Listing disks

Run `part list` with no arguments to see every disk on the machine:

```
# part list
DEVICE             SIZE  CONTENTS
/dev/vda           8.0G  gpt, 2 partitions
  vda1             512M  esp    EFI system partition
  vda2             7.5G  linux  Peios root
/dev/vdb           8.0G  no partition table
```

This is the view to start from, because it answers the question that precedes every other one: which disk did you mean? The partition names are the ones the kernel actually created — `vda1` on a virtio or SATA disk, `nvme0n1p1` on an NVMe one — so they are what you can pass straight to `mkfs` or to the installer.

The structure here comes from the kernel, not from reading a partition table, so a disk carrying a format `part` cannot manage still shows its partitions. A disk it cannot read at all is still listed, with `?` for its contents — the disk you cannot read is exactly the one worth knowing about.

Name a disk to see it in full:

```
# part list /dev/vda
/dev/vda: 16777216 sectors of 512 bytes (8.0G)
Label:     gpt
Disk GUID: DE942295-427C-411D-8023-B689D8BEE907
Usable:    34 .. 16777182

  #         START           END        SIZE  TYPE      NAME
  1          2048       1050623        512M  esp       EFI system partition
  2       1050624      16777182        7.5G  linux     Peios root

Free (aligned): 0B  in 0 extent(s), largest 0B
```

`Free (aligned)` counts space a new partition could actually occupy, which is not the same as unallocated sectors — the run below the first alignment boundary can never hold one. Reporting raw free space would promise room that `add` would then refuse to use.

`part verify` checks the same table's structure: both checksums, the two headers agreeing with each other, no overlaps, everything aligned and inside the usable range.

## Creating a table

```
# part create /dev/vda --yes
/dev/vda: wrote a new GPT (DE942295-427C-411D-8023-B689D8BEE907)

# part add /dev/vda --size 512M --type esp   --name "EFI system partition" --yes
/dev/vda: partition 1 at 2048..1050623 (512M)

# part add /dev/vda --size max  --type linux --name "Peios root" --yes
/dev/vda: partition 2 at 1050624..16777182 (7.5G)
```

`add` puts each partition in the first free run that fits, aligned to 1 MiB. `--size max` takes the largest free run rather than merely the last one, so it still does the obvious thing on a disk with a gap in the middle.

`part del /dev/vda 2 --yes` removes a partition by number. It frees the space and the slot; it does not touch the data that was in it.

### Sizes are sectors unless you say otherwise

`--size` takes `K`, `M`, `G`, `T` — powers of 1024 — or `max`. **A bare number is a sector count, not bytes.** `--size 2048` is 1 MiB on a 512-byte-sector disk, and you can write `2048s` to say so explicitly. Reading it as bytes would silently produce a partition a thousand times smaller than intended.

### Types

`--type` takes a short alias or a raw GUID:

| Alias | Meaning |
|---|---|
| `esp` | EFI system partition |
| `linux` | Linux filesystem data |
| `swap` | Linux swap |
| `msdata` | Microsoft basic data |

The list is short on purpose. Every type GUID in circulation would be a catalogue to keep current, and since a raw GUID is always accepted, nothing is unreachable for want of an alias.

### Names

Up to 36 UTF-16 code units — fewer if you use characters outside the Basic Multilingual Plane, which cost two units each. A longer name is **refused rather than shortened**. A partition name is how you identify the thing you are about to format, and a tool that quietly truncates it makes the label on your screen disagree with the label on the disk.

## What it will not do

`part` is the one tool on the system whose mistakes cannot be undone, so it is deliberately hard to point at the wrong thing.

**It requires `--yes`.** There is no interactive "are you sure": the usual caller is a script, and a prompt nobody can answer is worse than no prompt at all.

**It refuses a partition.** Naming `/dev/vda1` where you meant `/dev/vda` would write a GPT *inside* a partition — a table that looks valid to anything reading that partition directly, and is invisible to everything else.

```
# part create /dev/vda1 --yes
part: /dev/vda1 is a partition, not a whole disk; did you mean /dev/vda?
```

**It refuses a disk with anything mounted on it.** Not just the disk itself — any partition of it.

**It refuses a table it did not create.** This is the important one:

```
# part create /dev/vdb --yes
part: this disk carries an MBR (dos) partition table, which part cannot manage;
      pass --force to replace it — every partition on it will be lost
```

`part list` says what it *found*, not merely that it found no GPT — because "no GPT" is ambiguous between a blank disk and a disk holding somebody's data, and those deserve opposite treatment:

| What is there | What `part` says |
|---|---|
| an MBR | "an MBR (dos) partition table, which part cannot manage" |
| an Apple, BSD, Sun or SGI label | names it |
| a GPT whose header is corrupt | "the table may be damaged" |
| a filesystem written straight to the disk | "a `<type>` filesystem … with no partition table" |
| genuinely nothing | "no partition table" |

`--force` is the way through, and it is a *second* confirmation, separate from `--yes`. `--yes` means "I mean this destructive operation"; `--force` means "and I know it destroys a table that was already there". Requiring both is proportionate for an operation with no undo.

## Alignment, and why 1 MiB

Every partition starts on a 1 MiB boundary — 2048 sectors on a 512-byte-sector disk, 256 on a 4096-byte one. The number is not arbitrary: 1 MiB divides every erase block and RAID stripe width in practical use, so an aligned partition never straddles one. A misaligned filesystem pays a read-modify-write cycle on every boundary-crossing write, for the life of the filesystem.

`part` reads the logical sector size from the kernel rather than assuming it, so a 4Kn disk gets a correct table rather than one whose every structure is in the wrong place.

## Exit status

| Code | Meaning |
|---|---|
| 0 | success |
| 1 | usage error, or the operation failed |
| 2 | could not read or write the device |
| 3 | refused by a safety check |

**3 is separated from 1 on purpose.** A refusal is `part` working correctly, not malfunctioning, and a script should treat "this disk is not what you said it was" differently from "partitioning broke". `peios-install` relies on exactly this distinction.

## Where to go next

To put a filesystem on what you just created, read [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md) and [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md).

To have the installer do all of this for you, read [Installing to disk](/peios/using-peios/disks-and-filesystems/installing-to-disk.md) — `peios-install --whole-disk` runs exactly the three commands above before it formats anything.

---

# Formatting with security descriptors

_Peios / Using Peios / Disks and filesystems_

> How a filesystem gets real security descriptors at creation time, rather than synthesised ones at mount time, and why that distinction matters.

A freshly created filesystem contains one directory — its root — and that directory has no security descriptor. Nothing in the ext4 on-disk format has any concept of one. This is a problem the moment the filesystem is mounted under a FACS-managed policy, because [`facs_deny_missing`](/peios/using-peios/mount-policies/policy-classes.md) means exactly what it says: a file with no SD is unreachable, and that includes the root directory you are trying to enter.

There are two ways out. One is to let the kernel invent an SD at mount time. The other is to put a real one on the filesystem when it is created. Peios can do both, and which is appropriate depends on whether the filesystem is something you are passing through or something you intend to keep.

## Synthesised versus stamped

Mount-time synthesis is the right answer for media you do not own. A USB stick formatted on another system, a read-only image, an NTFS volume from Windows — these have no Peios SDs and should not acquire any. `facs_synthesize_ephemeral` gives every inode an SD in memory, derived from the mount's template, and never writes it back.

It is the wrong answer for a filesystem that is going to *be* a Peios system. A synthesised SD is a property of how the filesystem was mounted, not of the filesystem. Mount it with a different template and the whole tree's access policy changes; mount it somewhere that does not set a template and you get whatever the default is. The access policy of a system you own should live on that system, not in the command that attached it.

**Stamping** puts it there. `mke2fs` accepts a security descriptor at format time and writes it to the filesystem's root directory, so the filesystem carries its own policy from the moment it exists and mounts cleanly under `facs_deny_missing` with no template required.

## The root SD, and what it reaches

You give the descriptor as [SDDL](/peios/security-fundamentals/security-descriptors/overview.md), and `mke2fs` writes it to the `security.peios.sd` extended attribute on the root directory and on `lost+found`:

```
mke2fs -t ext4 -E root_sddl="O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)" /dev/vda2
```

That descriptor makes SYSTEM the owner and grants both SYSTEM and `BUILTIN\Administrators` full control. The `OICI` flags on each ACE — object-inherit and container-inherit — are what make it reach further than the root directory. Every file and directory subsequently created anywhere on the filesystem derives its own SD from that one through ordinary [inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md).

That is worth stating plainly, because it cuts both ways. A single inheritable ACE on the root is, in practice, the access policy of the entire filesystem. It is a complete answer for a system tree where everything should be administrator-owned. It is not a way to express "readable system tree, private home directories" — one inherited ACL cannot say two different things, and the ACEs that would make `/home/alice` private have to come from somewhere else.

When you read an SD, treat the `ID` flag as a diagnostic: `ID` is `INHERITED_ACE`, so that descriptor was derived rather than stored. When you see it, the file's policy is not on the file — go and look at the ancestor it came from.

## Populating a tree with per-node descriptors

Inheritance from the root covers files created *on* the filesystem. It does not cover a tree copied *onto* it, where the nodes already have descriptors of their own that need preserving.

`mke2fs -d` populates a new filesystem from a directory on the build host, and on Peios it is security-descriptor aware. For each node it copies, it computes the SD the node should have in its new home, by combining two inputs:

- The **explicit descriptor** the source node carries — what the creator of that node wanted for it.
- The **parent's descriptor in the new filesystem**, which supplies the inheritable ACEs.

The two are merged by the same rules that govern inheritance on a live system: explicit ACEs first, inherited ACEs appended after them. A node whose source carries no explicit descriptor is left alone, and inherits normally on first access instead. A node whose parent has no descriptor keeps its explicit one verbatim.

The walk is top-down, and directories are stamped before their contents are visited, so every child reinherits against a parent whose descriptor has already been written.

### Why the source uses a different xattr

The explicit descriptor on the source tree is read from **`user.peios.sd`**, not `security.peios.sd`. This looks like an inconsistency and is not.

`security.peios.sd` is the canonical, on-disk home for a descriptor — and precisely because it is canonical, it is protected. On a live Peios filesystem the kernel seals it: direct extended-attribute operations on it are refused unconditionally, and access goes through `kacs_get_sd` and `kacs_set_sd` instead. On a Linux build host, writing anything in the `security.*` namespace needs `CAP_SYS_ADMIN`.

Neither is available to a tool staging a tree. `user.peios.sd` is subject to neither restriction, which makes it the portable carrier: any user can attach it, on any host, and it travels with the tree through ordinary archive and copy operations. `mke2fs -d` reads it, computes the result, and writes the answer to the canonical `security.peios.sd` on the new filesystem. It also skips both names when copying the node's other extended attributes across, so neither the carrier nor a stale canonical value is propagated verbatim.

## This all works offline

None of the above needs a running Peios kernel. The reinheritance computation is pure userspace, and `mke2fs` writes the extended attributes through the ext2 library, addressing the image directly rather than going through the host's filesystem layer. That bypasses the host's own security module and the Peios seal alike, for the simple reason that neither is in the path.

The practical consequence is that a Peios filesystem, with its full security policy in place, can be built on a machine that is not running Peios.

## Where to go next

For the exact syntax of the extended options and the defaults `mke2fs` applies on Peios, read [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md).

For how a descriptor is physically stored once written, including when an oversized one needs its own inode, read [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

For what the mount policy does with the descriptor you stamped — and what happens on a filesystem that has none — read [Policy classes](/peios/using-peios/mount-policies/policy-classes.md).

---

# mke2fs

_Peios / Using Peios / Disks and filesystems_

> The Peios additions to mke2fs: the root_sddl and root_sd_file extended options, security-descriptor-aware population, and the compiled-in Peios defaults.

`mke2fs` creates an ext2, ext3 or ext4 filesystem. Peios packages it from upstream e2fsprogs, so the generic surface — `-t`, `-b`, `-L`, `-O`, `-i`, `-m`, the full extended-option list — is exactly the upstream one and its canonical documentation is the `mke2fs(8)` man page shipped with the package.

This page documents only what Peios adds, which is security descriptors and a changed set of defaults.

```
mke2fs [-t ext4] [-E root_sddl=SDDL | root_sd_file=PATH] [-d DIRECTORY] DEVICE
```

## Extended options

Peios adds two options to `-E`. Both set the security descriptor written to the new filesystem's root directory and to `lost+found`, in the `security.peios.sd` extended attribute.

| Option | Meaning |
|---|---|
| `root_sddl=SDDL` | The descriptor as an inline SDDL string. |
| `root_sd_file=PATH` | The descriptor as SDDL read from `PATH`. |

`root_sd_file` exists because `-E` takes a comma-separated list and SDDL contains commas. Any descriptor with more than one ACE, or with a conditional expression, is easier to pass in a file than to quote on a command line.

The file holds **SDDL text, not binary** — it is the same string `root_sddl` would take, in a file. Trailing newlines are stripped, so an ordinary one-line text file works. Anything else in the file is part of the descriptor: there is no comment syntax and no blank-line handling.

Both options are parsed in the order they appear. If you give both, or the same one twice, the last one wins.

If the SDDL does not parse, `mke2fs` prints `Invalid root SDDL:` followed by the string it was given, and exits without creating a filesystem.

Neither option has a default. Omit both and the filesystem is created with no security descriptor on its root, which is upstream behaviour — mount it under a synthesising policy or it will be unreachable.

### Example

```
mke2fs -q -t ext4 -E root_sddl="O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)" /dev/vda2
```

Confirm the result without mounting the filesystem:

```
debugfs -R "ea_list <2>" /dev/vda2
```

```
Extended attributes:
  security.peios.sd (96)
```

Inode 2 is the root directory. To read the descriptor back as bytes rather than just confirm its presence, use `debugfs -R "ea_get -V <2> security.peios.sd"`.

## Security-descriptor-aware population

`-d DIRECTORY` populates the new filesystem from an existing directory. On Peios this pass also computes a security descriptor for each node it creates.

The source node's explicit descriptor is read from its **`user.peios.sd`** extended attribute — not `security.peios.sd`, which is sealed on a live Peios filesystem and requires `CAP_SYS_ADMIN` on a Linux build host. Both names are excluded from the generic extended-attribute copy, so neither is propagated verbatim.

For each node:

| Source node | Parent in the new filesystem | Result |
|---|---|---|
| Has `user.peios.sd` | Has a descriptor | Explicit and inherited ACEs merged, written to `security.peios.sd`. |
| Has `user.peios.sd` | Has none | The explicit descriptor written verbatim. |
| Has no `user.peios.sd` | Either | Nothing written. The node inherits on first access instead. |

Directories are stamped before their contents are visited, so a child always reinherits against a parent whose descriptor is already on disk.

The merge follows the same rules as [inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) on a live system, and runs entirely in userspace — no kernel and no KACS are involved, so a populated Peios filesystem can be built on a host that is not running Peios.

## Peios defaults

`mke2fs` reads its defaults from a profile. Peios changes three of them, and the changed profile is **compiled into the binary**, so it applies whether or not a configuration file exists.

| Setting | Upstream | Peios | Reason |
|---|---|---|---|
| `inode_size` | 256 | **512** | Keeps a typical security descriptor in the inode's inline extended-attribute space, where reading it costs no extra I/O. |
| `default_mntopts` | `acl,user_xattr` | **`user_xattr`** | Peios uses security descriptors, not POSIX ACLs. |
| ext4 features | — | **+ `ea_inode`** | Lets an oversized descriptor spill into its own inode rather than failing to fit. |

`base_features`, `blocksize`, `inode_ratio` and `enable_periodic_fsck` are unchanged from upstream.

The ext4 **`quota`** feature is deliberately *not* enabled. The Peios kernel is built with `CONFIG_QUOTA` and `CONFIG_QUOTACTL` but without `CONFIG_QFMT_V2`, and ext4 refuses to mount a filesystem carrying the quota feature unless the vfsv1 on-disk format is compiled in. Enabling it in the profile therefore produced filesystems the kernel would not mount. Turning it on again is a pair of changes that have to land together — the kernel option, then the feature.

Resolution order for the profile, highest first:

1. The file named by the `MKE2FS_CONFIG` environment variable, if set.
2. `/etc/mke2fs.conf`, if present.
3. The compiled-in Peios profile.

Peios ships no `/etc/mke2fs.conf`, so the compiled-in profile is what you get unless you deliberately supply a file. Supplying one replaces the profile wholesale — the compiled-in values are a fallback, not a layer underneath — so a partial configuration file silently reverts every setting it does not mention to the upstream default.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Failure. Includes an unparseable `root_sddl`/`root_sd_file` value, an unreadable `root_sd_file`, and a failure to write the descriptor to the new filesystem. |

`mke2fs` distinguishes no further; unlike `e2fsck`, it has no bitwise-summed status. When the security-descriptor options fail they report the offending value on standard error before exiting, and no filesystem is created.

## See also

For the model behind the two options — why stamping beats mount-time synthesis, and what one inheritable ACE on the root does and does not express — read [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md).

For reading and rewriting descriptors on a mounted filesystem, read [The sd command](/peios/security-fundamentals/security-descriptors/sd-command.md).

For how the descriptor is stored, and when `ea_inode` becomes load-bearing, read [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

---

# Installing to disk

_Peios / Using Peios / Disks and filesystems_

> How a Peios system is copied from a live medium onto a disk, and how the installed system boots itself afterwards.

A live Peios runs from a read-only squashfs with a tmpfs stacked on top, so every write it accepts is discarded at reboot. Installing to disk replaces that arrangement with a writable filesystem that survives — and, less obviously, replaces a security policy chosen at *mount* time with one the filesystem carries itself.

Installation is five steps and one retirement. Nothing about it is magic, and all of it can be done by hand.

## Two ways to invoke it

```
peios-install --yes --whole-disk /dev/vda      # partition the disk, then install
peios-install --yes /dev/vda1 /dev/vda2        # install onto partitions that exist
```

The first form runs [`part`](/peios/using-peios/disks-and-filesystems/partitioning.md) before anything else — a fresh GPT, a 512 MiB ESP, and a root filling the remainder — and then proceeds exactly as the second. **The whole disk is erased.**

If the disk already carries a partition table `part` did not create, the install stops rather than overwriting it; `--force` is how you say you meant it. That refusal happens before the first `mkfs`, so nothing has changed when it does.

The second form is for any layout other than the one above: partition however you like with `part`, then name the two partitions.

## What the installer does

**1. Format the EFI system partition.** UEFI requires FAT, so this is `mkfs.vfat -F 32`. A FAT filesystem cannot hold a security descriptor and never will, so the ESP's access policy comes entirely from its mount — necessarily one of the synthesising classes.

**2. Format the root, with a descriptor.** This is the step with no equivalent on other systems:

```
mke2fs -t ext4 -E root_sddl="O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)" /dev/vdb2
```

The descriptor is written to the root directory's `security.peios.sd` at format time, so the filesystem is administrable from the instant it exists. Because both ACEs are inheritable, everything created inside it derives its own descriptor from that one. See [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md).

**3. Copy the system.** `cp -ax`, which preserves owner, DACL, SACL, timestamps, links and extended attributes, and stops at filesystem boundaries. Every one of those is required rather than best-effort, so a descriptor that cannot be carried across stops the install instead of quietly downgrading it. Mountpoints are recreated as empty directories rather than copied into: `/proc`, `/sys` and `/dev` are mount-moved into the new root by prelude at boot, and `/bin`, `/etc`, `/lib` and the rest are StrataFS views mounted over theirs. What lives *behind* those views — `/usr` and `/lcl` — is ordinary content on the root filesystem, and is copied in full.

**4. Write the kernel command line.** The installed system needs `root=`, naming the filesystem that did not exist until step 2. That cannot be package data, so it is generated: the `disk-boot` package ships a template at `/usr/share/disk-boot/cmdline` carrying everything stable, and the installer appends `root=UUID=<the new root>` and writes the result to `/lcl/etc/boot/cmdline` on the target. Package data supplies the general part; the operator tree holds the per-install part.

**5. Build the boot artifact.** `mkuki` bundles the kernel, the initramfs and that command line into a single EFI binary, written to the ESP at `EFI/BOOT/BOOTX64.EFI`.

That last path is the removable-media fallback, which UEFI firmware boots without an NVRAM entry. A Peios installation therefore needs **no bootloader and no boot manager** — there is nothing between the firmware and the kernel.

## What it refuses before it starts

Both partitions you name are about to be formatted, so the installer checks them before it does anything irreversible. It stops if either is not a block device, if you name the same partition twice, or — the one worth stating plainly — **if either is currently mounted**:

```
# peios-install --yes /dev/vda1 /dev/vda2
peios-install: /dev/vda2 is mounted; refusing to format it
```

That last check is what stands between you and naming the partition you are running from. All of it happens before the first `mkfs`, so a rejected install has changed nothing.

## The first-account service is retired, the account is not

Your accounts come across. What does not is the thing that *creates* them.

A live image ships `lpsd-first-account`: a oneshot service that runs `lps add` at boot to create the development account. It is written for exactly one situation, which is a live image — the root there is a tmpfs, so every boot starts from an empty store and the provisioner has work to do.

An installed system is the other situation. The accounts themselves live in lpsd's store at `/var/state/lpsd/principals`, which is ordinary content on the root filesystem and is copied like everything else. So the installed machine already has the account before it first boots, and re-running a provisioner against a populated store is not a harmless no-op: the script carries no idempotence guard, so `lps add` fails on the name that already exists and the service crashes on every boot. Once installation grows a "choose a password" step, it would be worse than noisy — a provisioner that reasserts the image's credential would undo the operator's choice at the next reboot.

So before it copies anything, the installer deletes the service from the registry:

```
reg del 'Machine\System\Services\lpsd-first-account'
```

Note *which* registry. It removes the key from the **live** system it is running on, not from the target — the registry is a live database served by `registryd`, and the only thing that can safely edit it is the `registryd` currently holding it open, so the removal happens at the source and the copy never carries it. Running the installer again, or running it from an already-installed system, finds nothing to remove and says so. The removal is checked afterwards and a failure stops the install, which happens before either partition is formatted and therefore costs nothing.

> [!CAUTION]
> One thing this deliberately does not solve: the account arrives with the password it had, and on an image that shipped a development account that password is in the image and therefore public. Until installation can prompt for a new one, treat a freshly installed system as carrying a known credential and change it — `lps` is the tool — before the machine is anywhere it matters.

## Use UUIDs, not device names

Step 4 records the root by UUID, and the reason is worth understanding rather than copying.

The disk that is `/dev/vdb` while you install — second device, behind the install medium — is `/dev/vda` when you boot it with the medium removed. A device name baked into the command line is a name for *where a disk was plugged in*, not for the disk, and it stops being true the moment the arrangement changes.

The initramfs resolves the UUID by probing block devices directly rather than reading `/dev/disk/by-uuid`, because no device manager runs in the initramfs and those directories do not exist there — see [Stable device names](/peios/using-peios/disks-and-filesystems/stable-device-names.md).

## How the installed system boots

The initramfs contains **two** hooks that can mount a root, and both are present in every image:

| Hook | Mounts |
|---|---|
| `mount-root.sh` | a live medium's squashfs, with a tmpfs overlay above it |
| `mount-root-disk.sh` | an installed root partition, directly |

Both run at every boot. `root=` on the command line decides which one acts; the other exits successfully having done nothing. [Boot hooks](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md) covers the mechanism.

Two differences in what the disk hook does are worth calling out, because they are the point of installing at all.

**It mounts the partition directly — no overlay.** The live path stacks a tmpfs because the layer beneath it is read-only. An installed root is writable, so an overlay would serve only to throw away everything written to it.

**It mounts `policy=deny-missing`, and runs no `seed-sd`.** A live root has no descriptors at all — the squashfs ships none — so the live system mounts it `synth-ephemeral` and has KACS invent one per inode, then seeds a single inheritable descriptor onto the tmpfs above. An installed root needs none of that: it carries a real descriptor, written at format time, so a file *without* one is a fault worth surfacing rather than a gap to paper over.

This is the substantive difference between the two, and it is easy to check on a running installed system:

```
# sd show /
  Owner:      LocalSystem (S-1-5-18)
  DACL: (2 ACEs)
    [0] allow LocalSystem (S-1-5-18)                   0x10000000  [CI,OI]
    [1] allow BUILTIN\Administrators (S-1-5-32-544)    0x10000000  [CI,OI]
```

No `ID` flag on either ACE. `ID` is `INHERITED_ACE`, so its absence means this descriptor is *explicit* — stored on the root inode by `mke2fs`, not derived from an ancestor and not synthesised at mount. The filesystem's access policy is a property of the filesystem.

## What the installer does not do

**It does not choose your partition layout.** `--whole-disk` writes one specific arrangement — a 512 MiB ESP and a root filling everything else — and that is all it will ever write. Anything else is a job for [`part`](/peios/using-peios/disks-and-filesystems/partitioning.md) followed by the two-partition form of the installer.

**It does not choose a layout.** One ESP, one root, no separate `/home`, no swap, no encryption. Each of those is a real thing to want and none of them exists yet.

**It does not express a per-directory access policy.** The whole installed tree inherits from that single descriptor on the root. That is a complete answer for a system tree where everything is administrator-owned, and it is not a way to say "readable system tree, private home directories" — one inheritable ACL cannot say two different things. Per-subtree descriptors are the work that makes that expressible.

## Where to go next

For the format-time descriptor and how a populated tree gets its own, read [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md).

For the hook mechanism the two root-mounting hooks share, read [Boot hooks](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md).

For what `deny-missing` does with a file that has no descriptor, and why the live path cannot use it, read [Policy classes](/peios/using-peios/mount-policies/policy-classes.md).

---

# StrataFS

_Peios / Using Peios / Disks and filesystems / StrataFS_

> StrataFS merges ordinary directories by precedence while keeping each stratum independently manageable. Mounting a stack and the stratafs inspection tool.

StrataFS presents several ordinary directories as one merged directory tree.
The directories remain ordinary and independently manageable at their real
paths; the StrataFS mount only supplies the merged view.

Strata are ordered from highest to lowest precedence. For each name, the first
stratum that holds it is its **provider**. Directories at the same name merge,
so their children are resolved in the same precedence order. A non-directory
provider masks every lower object at that name, including a lower directory's
whole subtree.

## A `/bin` example

Suppose `/usr/bin` contains packaged programs and `/lcl/bin` is for local
changes. Mount this stack at `/bin`:

```sh
mount -t stratafs none /bin -o 'strata=/lcl/bin+create:/usr/bin+ro'
```

`/lcl/bin` has higher precedence and receives newly-created objects. The `+ro`
on `/usr/bin` means “do not modify this stratum **through `/bin`**”. It does not
make the real `/usr/bin` mount read-only: an authorised writer can still modify
`/usr/bin/tool` directly at `/usr/bin/tool`.

Reading `/bin/tool` uses `/lcl/bin/tool` when it exists, otherwise
`/usr/bin/tool`. Writing an existing packaged tool through `/bin` copies it to
`/lcl/bin` first and changes the copy. Removing that copy through `/bin`
restores the unchanged `/usr/bin` version to view, because StrataFS does not use
whiteouts.

## The base Peios topology

The `fsbase` package installs the `mount-rootfs-stratafs-base.sh` [boot hook](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md)
in the initramfs. It runs after the deployment-specific hook has mounted the
real root and before prelude hands off to it, mounting the conventional
root-level views as one boot step:

| View | Strata, highest precedence first |
|---|---|
| `/bin` | `/lcl/bin+create`, `/usr/bin+ro+am` |
| `/sbin` | `/lcl/sbin+create`, `/usr/sbin+ro+am` |
| `/lib` | `/lcl/lib+create`, `/usr/lib+ro` |
| `/libexec` | `/lcl/libexec+create`, `/usr/libexec+ro+am` |
| `/share` | `/lcl/share+create`, `/usr/share+ro+am` |
| `/include` | `/lcl/include+create`, `/usr/include+ro+am` |
| `/etc` | `/system/retc`, `/lcl/etc+create`, `/usr/etc+ro+am` |
| `/conf` | `/lcl/conf+create`, `/usr/conf+ro+am` |

`am` permits an optional vendor directory to be absent when the system boots
and makes it participate automatically if a later package creates it. The
operator create directories are provisioned by `fsbase`; their absence is a
boot error rather than something StrataFS silently creates, because their own
security descriptors govern creation through each view.

`/lib` views `/usr/lib` rather than the architecture triplet directory beneath
it, so `/lib/modules` and `/lib/firmware` resolve. Both matter: kmod has
`/lib/modules` compiled in, and the kernel's firmware loader searches
`/lib/firmware`, and neither can be told to look elsewhere. Shared libraries are
unaffected — the loader finds them through its own absolute system search path
rather than through this view — and `/lib/x86_64-linux-peios/` still resolves,
one level down, which is the shape a foreign binary expects.

`/lib64` is not a StrataFS view. On x86-64 it remains the package-owned relative
symlink `lib64 -> usr/lib/x86_64-linux-peios`, because the psABI dynamic-loader
path must work before any hook can mount the base topology. It is a distinct
object from the `/lib` view above and is unaffected by what that view maps.

The mount option grammar is:

```text
strata=<stratum>[:<stratum>]...
<stratum> := <path>[+<flag>]...
<flag> := create | ro | am
```

Paths are absolute. `create` selects the one stratum that receives creations
and copy-up, `ro` prevents modification through the merged view, and `am`
allows the stratum directory itself to be temporarily absent. Literal `:`,
`+`, `,`, and `\` in a path are escaped with `\`.

## Inspecting a stack

The `stratafs` command is a read-only inspector. It does not mount, modify, or
clean anything, and it never gains extra authority. Every direct stratum read
runs with your normal access rights. If it cannot see all the information
needed for a complete answer—especially every participant of a protected
merged directory—it fails instead of showing a misleading partial result.

List every StrataFS mount in the current mount namespace, or one exact mount:

```sh
stratafs list
stratafs list /bin
```

Typical output is:

```text
/bin
  [0] /lcl/bin+create (present)
  [1] /usr/bin+ro (present)
```

The order is the precedence order. A stratum is `present`, `absent`, or
`not_directory`; the mount itself is also labelled when generically mounted
read-only.

## Explaining one path

Use `resolve` when you want to know why a name looks the way it does and where
a mutation would go:

```sh
stratafs resolve /bin/sh
```

The per-stratum states are:

| State | Meaning |
|---|---|
| `provider` | This object currently provides the name. |
| `participant` | This lower directory contributes to the merged directory. |
| `shadowed` | A higher object of the same type wins. |
| `masked` | A provider of another type hides this object. |
| `absent` | This stratum does not hold the path. |

The report also explains `write` and `delete`. A write can route `in_place`,
`copy_up`, or `create`; it can instead report `erofs`, a missing parent, an
unknown immutable-attribute state, or that content I/O follows a symlink.
Deletion identifies the real provider entry to remove and names a lower object
that will resurface. Removing a directory is still conditional on the complete
merged directory being empty.

These are routing answers, not authorisation promises. KACS checks the actual
operation against the caller when it is attempted.

For the kernel's answer without the explanation, print the synthetic origin
attribute:

```sh
stratafs origin /bin/sh
```

A non-directory prints its one real provider path. A merged directory prints
every participating real directory in precedence order, one escaped path per
line.

## Inspecting local state

The create stratum is deliberately easy to audit. `sweep` recursively reports
every object in it:

```sh
stratafs sweep /bin
```

Each result is classified as:

- `gap`: only the create stratum has this path;
- `override`: a lower stratum also has it; or
- `shadowed`: a higher stratum, or a higher non-directory ancestor, makes it
  unreachable.

Directories are included. For a directory, `override` describes structural
presence; corresponding directories still merge. When `sweep` says
`create stratum is empty`, the local stratum has no entries to reconcile. The
command never deletes anything—remove a reviewed entry through the merged view
or at its real create-stratum path, according to the result you intend.

To inspect the content of an override:

```sh
stratafs diff /bin/sh
```

`diff` compares the create-stratum object with the first lower default. It
supports regular files and symbolic links, reports type changes, refuses other
object types, and bounds each regular-file read to 16 MiB. Binary files are
reported only as different.

## Structured output and status

`list`, `resolve`, and `sweep` accept `--json`. JSON is the stable scripting
interface; it includes the same paths, flags, states, object types, and routing
actions. A non-UTF-8 path is represented losslessly in human output but causes
JSON mode to fail rather than substitute a lossy name.

Exit status has probe-friendly meaning:

| Status | Meaning |
|---|---|
| `0` | Success; for `sweep`, no entries; for `diff`, no difference. |
| `1` | `sweep` found entries or `diff` found a difference. |
| `2` | Usage, visibility, malformed-state, or operational error. |

## Where to go next

For the generic command that creates this and other mounts, read
[`mount`](/peios/using-peios/mount-policies/mount.md). For how access to every real object is
decided, start with [File access](/peios/security-fundamentals/file-access/overview.md).

---

# Package management

_Peios / Using Peios / Package management_

> peipkg installs, upgrades, removes, and queries software on a Peios system. This page covers packages, repositories, the security model, and the commands.

`peipkg` is the command that manages software on a running Peios system. It installs packages and the dependencies they need, upgrades them, removes them, and answers questions about what is installed and where it came from.

```
$ peipkg install nginx
$ peipkg upgrade
$ peipkg list
```

This page explains what a package is, where packages come from, and the design principle that peipkg holds no authority of its own. It then names every command and points you at the rest of the topic.

## What a package is

Peios distributes software as **`.peipkg` files**: signed, self-contained archives. Each one carries a **manifest** — a name, a version, a target architecture, the other packages it depends on, the packages it conflicts with — and a **payload**, the files that land on disk when it is installed.

A package is a **low-level primitive**. It is the unit peipkg installs and tracks. The curated, user-facing concepts that most operators think in — bundles of software, "the web-server role", applications — are built above packages and are out of peipkg's scope: peipkg installs `nginx` the package; it does not know what a "web server role" is. This topic is about the primitive.

What peipkg does coordinate at the package level is [claims](/peios/using-peios/package-management/claims.md): a claim is a single shared name that exactly one installed package may hold, so that two packages offering the same role — for example, `registryd` and `loregd` both providing the registry — do not silently collide.

Every installed package is tracked in a private database. peipkg records, for every package, which files it owns — the record that makes a clean removal, a correct upgrade, and the [`verify`](/peios/using-peios/package-management/inspecting-and-verifying.md) check possible.

## Where packages come from

Packages come from **repositories**: HTTP or HTTPS locations that serve a set of `.peipkg` files alongside signed indexes describing them. A Peios system is configured with one or more repositories, each one **anchored to a signing key the operator has chosen to trust**.

peipkg keeps a local, verified copy of each repository's metadata. `peipkg refresh` updates that copy; `peipkg install` and `peipkg upgrade` plan against it. The trust model — how a repository is anchored, how its signing keys rotate, and how unsigned repositories are handled — is covered in [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

A package can also be installed straight from a `.peipkg` file on disk, with no repository involved. That path trades the repository's trust guarantees for convenience; [Installing and removing packages](/peios/using-peios/package-management/installing-and-removing.md) covers what it keeps and what it gives up.

```mermaid
flowchart LR
    A["Repository<br/>signed packages + indexes"] -->|"refresh"| B["Cached metadata"]
    B -->|"install / upgrade"| C["peipkg<br/>resolve → verify → commit"]
    C -->|"file changes"| D["Your system"]
    C -->|"operation events"| E["Audit stream"]
```

## peipkg has no authority of its own

The key design point is that peipkg works differently from package managers on most systems.

peipkg is not a privileged daemon. It is not setuid. It has no service account, no special identity, no broker it asks to do privileged work. It is an ordinary program, and it runs as you — under your token, with your rights.

The question "may I install this?" is therefore not a question peipkg answers. It is the same question Peios asks of any attempt to write a file: it compares your token against the [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) on the directory being written. If the security descriptors on `/usr`, `/opt`, and the rest say your token may write there, the install succeeds. If they do not, it fails — at the file operation, the same way any unauthorised write fails.

"Who may install software" is therefore not a peipkg setting. It is just the access rules on the system directories — `Administrators`, by default. To grant someone install rights, you grant them write access to the directories packages land in; to scope what they may touch, you scope those security descriptors. This is the ordinary [access-decision](/peios/security-fundamentals/access-decisions/overview.md) machinery, with nothing package-specific layered on top.

One consequence follows directly: a package cannot grant its installer rights the installer did not already have. Any security descriptors a package asks to set on its own files can only be descriptors the caller already had the authority to set. There is no confused-deputy problem, because peipkg holds no authority that a package could misuse.

## Every change is a transaction

An install, an upgrade, a downgrade, a removal — each is a **transaction**, and each is atomic. There is a single instant, the **commit**, before which any failure, interruption, or power loss leaves the system exactly as it was, and after which the operation is complete. There is no partially installed intermediate state for a transaction to get stuck in.

Transactions are also reversible. peipkg keeps a history of them, retains the data needed to walk one back, and offers [`undo`](/peios/using-peios/package-management/keeping-a-system-current.md) and [`recover`](/peios/using-peios/package-management/transactions-and-recovery.md) to do it. [Transactions and recovery](/peios/using-peios/package-management/transactions-and-recovery.md) covers the model in full.

## Every operation is audited

peipkg records each operation it performs — what was installed, upgraded, or removed, and the outcome — to the Peios [audit stream](/peios/security-fundamentals/auditing/overview.md). When a plan contains an action that needs deliberate authorisation, the authorising act itself is recorded too.

These events are a semantic summary: a readable account of what peipkg set out to do. They are not the security boundary. The authoritative record is the kernel's own audit of the actual file operations — and because peipkg runs with no special authority, it cannot suppress that record.

## The command surface

Every command is invoked as `peipkg <command> [arguments]`.

| Command | Does |
|---|---|
| `install` | Install packages, with dependencies, from a repository or a local `.peipkg` file. |
| `remove` (alias `uninstall`) | Remove installed packages. |
| `upgrade` | Move installed packages to their newest available version. |
| `downgrade` | Move one package to a specific older version. |
| `undo` | Reverse the most recent transaction. |
| `claim` | Show or change which package holds a claim — a shared name that exactly one package may own. |
| `refresh` | Update the cached metadata of the configured repositories. |
| `repo` | Configure repositories — `add`, `list`, `remove`. |
| `root` | Manage named roots — `add`, `list`, `remove`, `show`. |
| `list` | List the installed packages. |
| `info` | Show one installed package's details. |
| `files` | List the files a package owns. |
| `owns` | Report which package owns a given path. |
| `search` | Search the configured repositories for a package. |
| `verify` | Check installed files against what was recorded at install. |
| `history` | Show the transaction log. |
| `recover` | Roll back a transaction left pending by an interruption. |
| `clean` | Delete cached metadata for repositories no longer configured. |

One global option sits before the command: `--root TARGET` makes peipkg operate on a Peios installation other than the running system at `/`. `TARGET` can be a literal path — a Peios installation mounted at `DIR`, the form used by image builders and offline maintenance — or the name of a [named root](/peios/using-peios/package-management/named-roots.md). Named roots are more than a convenience: they are how peipkg keeps components such as the initramfs current. See [Named roots](/peios/using-peios/package-management/named-roots.md) for the detail.

## The producer side

peipkg is the **consumer** half of the Peios packaging story — the tool that runs on a deployed system and consumes packages. It has a counterpart it never shares a process with: the **producer** tools (`peipkg-build`, `peipkg-repo`, `peipkg-manager`) that turn source into signed `.peipkg` files and assemble the repositories peipkg fetches from. Building packages and running a repository are a separate job with their own documentation, the *Peios Packages* guide. This topic is for the operator of a Peios system, not the operator of a build farm.

peipkg has one consumer-side companion of its own: **`peipkg-compose`**, a separate binary — not a `peipkg` subcommand. Where peipkg mutates a running system in place, `peipkg-compose` builds a fresh, package-owned root directory offline from a declarative manifest, the form image builders use when they assemble a system from nothing. It consumes the same packages peipkg does. See [Composing a root](/peios/using-peios/package-management/composing-a-root.md).

## Where to start

For the everyday work — putting software on a system and taking it off — read [Installing and removing packages](/peios/using-peios/package-management/installing-and-removing.md).

For keeping a system up to date, and for walking a change back, read [Keeping a system current](/peios/using-peios/package-management/keeping-a-system-current.md).

To configure where packages come from and how their authenticity is established, read [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

To understand why an interrupted install is always safe, read [Transactions and recovery](/peios/using-peios/package-management/transactions-and-recovery.md).

To understand how peipkg decides which versions of which packages a request implies — and why some actions require a second, separate authorisation — read [Dependency resolution](/peios/using-peios/package-management/dependency-resolution.md).

To understand how two packages that offer the same shared name settle on a single winner, read [Claims](/peios/using-peios/package-management/claims.md).

To inspect what is installed and check that it is intact, read [Inspecting and verifying](/peios/using-peios/package-management/inspecting-and-verifying.md).

For the more specialised work of operating on a Peios installation other than the running one — and how peipkg keeps things like the initramfs current — read [Named roots](/peios/using-peios/package-management/named-roots.md).

For the specialised work of assembling a fresh root offline from a manifest — the image-builder's path, run with the separate `peipkg-compose` binary — read [Composing a root](/peios/using-peios/package-management/composing-a-root.md).

---

# Installing and removing packages

_Peios / Using Peios / Package management_

> install puts packages on the system; remove takes them off. The plan-and-confirm flow, raw installs from a local .peipkg file, and cascade removals.

`peipkg install` puts packages on the system; `peipkg remove` takes them off. They are the two commands you reach for most, and they share one flow — peipkg works out the full set of changes, shows it to you, and waits for your approval before touching anything.

```
$ peipkg install nginx
$ peipkg remove oldtool
```

## Installing packages

```
peipkg install <package|file.peipkg>...
```

Each argument is either the **name** of a package to fetch from a configured repository, or the **path** of a local `.peipkg` file (recognised by its `.peipkg` suffix). You can mix the two in one command.

Installing a package rarely means installing just that package. peipkg works out everything the request implies — the dependencies the package needs, and the dependencies of those in turn — and presents the whole set. How that set is computed is the subject of [Dependency resolution](/peios/using-peios/package-management/dependency-resolution.md); this page is about the flow around it.

### The plan-and-confirm flow

`install`, `remove`, and the commands on [Keeping a system current](/peios/using-peios/package-management/keeping-a-system-current.md) all work the same way. peipkg first produces a **plan** — the ordered list of changes that satisfy your request — and prints it:

```
$ peipkg install nginx
the following changes will be made:
  install    pcre2 10.44
  install    zlib 1.3.1
  install    nginx 1.27.4
proceed? [y/N]
```

Nothing has been downloaded and nothing on the system has changed. peipkg waits for an answer. Anything other than `y` or `yes` — including pressing Enter, or end-of-input — is a refusal, and the command exits having done nothing.

Answer `y` and peipkg carries the plan out as a single [transaction](/peios/using-peios/package-management/transactions-and-recovery.md): it downloads and verifies every package, then commits the change atomically.

| Option | Effect |
|---|---|
| `--dry-run` | Produce and print the plan, then stop — never prompt, never change anything. |
| `--yes`, `-y` | Skip the `proceed?` prompt and apply the plan. |
| `--no-claim` | Install a provider without taking any claim it offers. |
| `--allow-stale` | Proceed although a repository's trust state exceeds its maximum trusted age. See [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md). |
| `--claim <names>` | Comma-separated claims to force-claim, overriding the current holder(s). |
| `--claim-all` | Force-claim every claim the installed packages provide, overriding incumbents. |
| `--dangerously-bypass-path-restrictions` | Permit packages that declare `special_system_package` to install outside the payload layout rules. Exempts nothing that has not declared itself special, and never reaches `/lcl/policy`. Needed only for the handful of packages whose job is to lay down the filesystem structure those rules protect. |

`--dry-run` is the safe way to see what a command would do. `--yes` is for scripts and unattended runs — but note that it skips only the routine prompt. A plan that contains an action needing deliberate authorisation will still stop and ask; `--yes` does not override that. See [Elevated authorisation](/peios/using-peios/package-management/dependency-resolution.md) for which actions those are and why.

`--claim-all` cannot be combined with `--claim` or `--no-claim`. Claims — shared names exactly one package may hold — are covered in [Claims](/peios/using-peios/package-management/claims.md).

An install can also target a root other than the current one — either explicitly with `--root`, or because a package declares its own default root. See [Named roots](/peios/using-peios/package-management/named-roots.md) for how roots are named and nested.

### Installing from a local file

When an argument is a path ending in `.peipkg`, peipkg installs that file directly:

```
$ peipkg install ./nginx-1.27.4.peipkg
```

This is a **raw install**, and it differs from a repository install in one specific way: it skips the repository **trust layer**. There is no signed index to check the file against, no signing key to verify it under, and none of the freshness or rollback protection a repository provides. You are vouching for the file yourself.

Everything else still happens. The package format is fully verified: the archive structure, the manifest, the integrity manifest, and the hash of every payload file are all checked before anything is staged. A corrupt or truncated `.peipkg` is rejected in the same way as one from a repository. The file's dependencies still resolve normally against your configured repositories — a locally-installed package can pull in repository packages to satisfy what it needs.

A package supplied as an explicit local file takes precedence over any repository's version of the same package, so `install ./foo.peipkg` installs that file even if a repository offers `foo` too. In the plan, a local-file operation is marked so the choice is visible:

```
  install    nginx 1.27.4  (local file)
```

In the future, peipkg will be able to consult system policy to decide whether raw installs are permitted at all, and that gate will be configurable. For now a raw install is always allowed; the verification above is what stands behind it.

## Removing packages

```
peipkg remove <package>...
peipkg uninstall <package>...
```

`remove` and `uninstall` are the same command. Each argument names an installed package; peipkg plans the removal — the files to take off disk — and runs the plan-and-confirm flow described above.

A removal leaves shared directories in place and removes only the files the package owns. peipkg knows which files those are from its database, so a removal is clean and complete.

### Removing something that is depended on

peipkg will not, by default, leave the system inconsistent. If you ask to remove a package that another installed package depends on, the plan is refused: peipkg tells you what still needs it, and stops.

| Option | Effect |
|---|---|
| `--cascade` | Also remove every installed package that depends on the ones named. |
| `--dry-run` | Print the plan and stop. |
| `--yes`, `-y` | Skip the `proceed?` prompt. |

`--cascade` turns that refusal into a wider plan: peipkg computes the full set of packages that would be left with a broken dependency and adds them to the removal. The plan then shows everything that will be removed. Review it before approving, because a cascade can reach further than expected.

```
$ peipkg remove --cascade libfoo
the following changes will be made:
  remove     toolA 2.1
  remove     toolB 1.0
  remove     libfoo 3.3
proceed? [y/N]
```

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded — or, for `--dry-run`, the plan was produced. A declined prompt is also `0`: nothing failed. |
| `1` | The operation failed — a package was not found, a dependency could not be satisfied, a download or verification failed, or a file operation was denied. |
| `2` | A usage error — an unknown command or a malformed option. |

---

# Keeping a system current

_Peios / Using Peios / Package management_

> refresh learns what is available; upgrade moves to it; downgrade and undo walk a bad change back. The routine update cycle and the recovery commands.

Keeping a Peios system current is two commands in sequence — `refresh` to learn what is available, then `upgrade` to move to it. The other two commands here, `downgrade` and `undo`, go the other way: they walk a change back when an upgrade turns out badly.

## Refreshing repository metadata

```
peipkg refresh [repository]...
```

peipkg plans against a local, verified copy of each repository's metadata. That copy does not update itself. `peipkg refresh` is what updates it: for every configured repository — or just the ones you name — peipkg fetches the current signed descriptor and index, verifies them, and replaces the cached copy.

```
$ peipkg refresh
refreshed "official"
refreshed "internal"
```

Refresh has two properties worth knowing:

- **Repositories are independent.** If one repository is unreachable or fails verification, peipkg reports it and carries on with the rest. One bad repository never blocks a refresh of the others.
- **A failed refresh changes nothing.** If a repository cannot be refreshed, peipkg keeps the metadata it already had. It never falls back to unverified or stale-but-unchecked content. The worst a failed refresh does is leave you on yesterday's view.

What "verified" means here — signing keys, freshness floors, the handling of an unsigned repository — is the subject of [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

Run `refresh` before an upgrade. An upgrade plans against whatever metadata is cached, so an upgrade without a recent refresh moves you to the newest version peipkg last heard about.

Skip it for long enough and peipkg stops waiting for you: an install, upgrade, or downgrade against a repository whose trust state has passed its **maximum trusted age** (30 days by default) refreshes that repository itself, and refuses to proceed against one that stays stale — unreachable, or frozen on an unchanging index — unless you pass `--allow-stale`. The bound and its configuration are covered in [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

## Upgrading

```
peipkg upgrade [package]...
```

With no arguments, `upgrade` considers every installed package and moves each one that has a newer available version forward to it. With one or more package names, it upgrades only those — and still pulls in any new dependencies they need.

```
$ peipkg refresh && peipkg upgrade
the following changes will be made:
  upgrade    zlib 1.3.1 -> 1.3.2
  upgrade    nginx 1.27.4 -> 1.27.5
proceed? [y/N]
```

`upgrade` uses the same plan-and-confirm flow as `install` — peipkg shows the full set of changes and waits for approval — and the same options:

| Option | Effect |
|---|---|
| `--dry-run` | Print the plan and stop. A good way to preview what an upgrade would move. |
| `--yes`, `-y` | Skip the `proceed?` prompt. |
| `--no-recurse` | Confine the upgrade to the current root only — disable the cascade into nested named roots. |
| `--allow-stale` | Proceed although a repository's trust state exceeds its maximum trusted age. Warned and audited. |

By default, when named roots are configured, `upgrade` **cascades**: it reconciles the current root and every named root nested under it, each as an independent continue-on-error transaction with its own summary. `--no-recurse` disables that and upgrades the current root alone. See [Named roots](/peios/using-peios/package-management/named-roots.md) for the named-roots model.

If there is nothing to do — every package already at its newest version — peipkg says so and exits.

## Downgrading

```
peipkg downgrade <package> <version>
```

`downgrade` moves one package to a specific older version. You name the package and the exact version you want:

```
$ peipkg downgrade nginx 1.27.4
```

Older versions are not in a repository's active index — that index lists current versions only. They live in its **archive index**, which peipkg fetches on demand when you ask for a version that is not current. The package must still exist in some configured repository's archive for the downgrade to be possible.

A downgrade is treated as a deliberate move. Going backward — onto a version that may have known issues a newer one fixed — is one of the actions peipkg flags for **explicit authorisation**: beyond the routine `proceed?` prompt, peipkg asks you to authorise the specific downgrade, and `--yes` does not stand in for that answer. See [Elevated authorisation](/peios/using-peios/package-management/dependency-resolution.md) for the full set of actions that work this way.

`downgrade` accepts `--dry-run`, `--yes`, `-y`, and `--allow-stale` with the same meaning as elsewhere.

## Undoing the last change

```
peipkg undo
```

`undo` reverses the most recent committed transaction. If that transaction installed a package, `undo` removes it; if it upgraded, downgraded, or removed packages, `undo` restores each one to the version it had before.

```
$ peipkg undo
undoing transaction 47 (upgrade nginx, zlib)
the following changes will be made:
  downgrade  nginx 1.27.5 -> 1.27.4
  downgrade  zlib 1.3.2 -> 1.3.1
proceed? [y/N]
```

Be precise about what `undo` is. It is not a rollback of committed state — the previous transaction happened and stays in the history. `undo` computes the inverse of that transaction and applies it as a new transaction of its own. The history grows; it does not rewind. That new transaction can itself be undone, and so on.

Because restoring an older version is a backward move, `undo` carries the same explicit-authorisation requirement as `downgrade` for any package it walks back. It accepts `--dry-run` and `--yes`, `-y`.

To undo something other than the most recent transaction, or to see the history `undo` works against, use [`peipkg history`](/peios/using-peios/package-management/transactions-and-recovery.md) — and revert a specific package directly with `downgrade`.

## The routine cycle

For day-to-day maintenance the loop is:

```
$ peipkg refresh        # learn what is available
$ peipkg upgrade --dry-run   # preview the move
$ peipkg upgrade        # apply it
```

`downgrade` and `undo` are the recovery commands — use them when an upgrade has brought in a change you want to remove. Every one of these commands is a [transaction](/peios/using-peios/package-management/transactions-and-recovery.md), so every one of them is atomic and itself reversible.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded — including a dry run, a plan with nothing to do, and a declined prompt or authorisation (nothing failed). |
| `1` | The operation failed — a repository could not be refreshed, a repository's trust state exceeded its maximum age and a forced refresh could not clear it, resolution or a download or verification failed, a root in an upgrade cascade failed, there is no committed transaction to undo, or a command's arguments were wrong (`downgrade` without a package and version, an unparsable version, a malformed option). |
| `2` | A usage error before any command ran — no command, an unknown command, or a malformed global option (including a `--root` reference that does not resolve). |

---

# Repositories and trust

_Peios / Using Peios / Package management_

> The repo commands, the trust ceremony that anchors a repository to a signing key, key rotation, signature policy, priority, and freshness protection.

A **repository** is where packages come from: an HTTP or HTTPS location that serves `.peipkg` files alongside signed indexes describing them. A Peios system has zero or more repositories configured, and the `peipkg repo` commands manage that configuration.

Beyond configuration, this page covers **trust**: establishing that the packages a repository serves are genuinely the ones its operator published, and not something a network attacker or a compromised mirror substituted.

## What a repository serves

At its base URL a repository serves three signed documents:

- A **descriptor** — the repository's identity: the set of signing keys it uses, each with a status. This is the root of trust for everything else.
- An **active index** — the catalog of current package versions: names, versions, dependencies, hashes, and download locations.
- An **archive index** — the catalog of older versions, kept so a [downgrade](/peios/using-peios/package-management/keeping-a-system-current.md) can reach them. peipkg fetches it only on demand.

The descriptor and the indexes each carry a detached signature. peipkg verifies all of them; an unverifiable document is discarded, never used.

## Adding a repository

```
peipkg repo add <name> <base-url> --anchor <fingerprint>
```

`repo add` does two things: it records the repository in your configuration, and it runs the **trust ceremony** that anchors it.

```
$ peipkg repo add official https://pkgs.peios.org \
    --anchor ef86709c4b1d8a02e5f3c719d640aa8b7c2e9105f8d3b6470a1c2e9d8b5f3a04
added repository "official"
```

The `--anchor` value is a **signing-key fingerprint**, and it is the part of the command that establishes trust. peipkg cannot tell you whether a repository is genuine; only you can, by obtaining its fingerprint through a channel you trust (the project's website over HTTPS, a colleague, a printed reference) and supplying it here. The anchor is your out-of-band statement that the key is trusted.

Given an anchor, the ceremony runs:

1. peipkg fetches the repository's descriptor and its signing keys.
2. It checks the fetched keys against the anchors you supplied — a fingerprint must match bit for bit.
3. Only if the descriptor's signature verifies against an anchored key does peipkg accept it and record the trust state.

If the fetched descriptor does not verify against any anchor, `repo add` fails and leaves nothing behind — no half-added repository, no configuration file. You can supply `--anchor` more than once to trust several keys at first contact.

| Option | Default | Effect |
|---|---|---|
| `--anchor FINGERPRINT` | — | A signing-key fingerprint to trust. Repeatable. |
| `--priority N` | `50` | Resolution priority — a lower number wins. See below. |
| `--policy required\|optional` | `required` | Whether a valid signature is required. See *Signature policy*. |
| `--min-index-version N` | `0` | A freshness floor for the index. See *Freshness*. |
| `--max-trusted-age-days N` | `30` | How stale the repository's trust state may grow before operations force a refresh. See *Freshness*. |
| `--max-index-staleness-days N` | `90` | How old the index's own `generated_at` may be before operations force a refresh. See *Freshness*. |
| `--insecure` | off | Permit a plain `http://` base URL. |

`--insecure` exists for a repository on a trusted local network with no TLS. It lowers transport confidentiality and integrity; the package signatures still protect authenticity, but prefer `https://` whenever it is available.

## Adding a repository your system already carries

An image can ship a repository's configuration — its base URL, its policy, and its trust anchors — as a `.repo` file in `/conf/peipkg/` (stored at `/lcl/conf/peipkg/`, which is what that view exposes). A Peios installation medium does exactly that for the offline repository it carries.

Configuration alone is **not** trust. peipkg does not trust a configured repository on sight: until the ceremony has run there is no recorded trust state, and every operation skips the repository with a warning rather than quietly using it. The decision to trust a key is yours, never a default.

What a baked-in `.repo` file changes is only where the anchor comes from. It arrived with the image rather than being typed at the prompt, which is still out-of-band — it did not come from the repository it authenticates. So the ceremony can run against it without you retyping a 64-character fingerprint that is already on disk:

```
$ peipkg repo add peios-medium
added repository "peios-medium"
```

Given a name and nothing else, `repo add` reads that repository's existing configuration and runs the same ceremony as the two-argument form. It is still an explicit act — nothing here happens on its own.

Two differences from the full form are worth knowing:

- It refuses a configuration with no `trust_anchors` under the `required` policy, because there would be nothing to verify the descriptor against.
- A failed ceremony leaves the `.repo` file alone. The two-argument form wrote that file and removes it again on failure; here it came from somewhere else, and deleting another party's configuration because a ceremony failed would turn a retryable problem — a medium not mounted yet, a repository not published — into lost settings.

## How trust survives key rotation

A repository operator will, over time, rotate signing keys — retiring an old one, bringing a new one into use. If trust were pinned permanently to the fingerprint you first anchored, every rotation would break every consumer.

Rotation does not break trust, because the descriptor carries the keys. Each key in the descriptor has a status:

| Status | Meaning |
|---|---|
| `active` | In current use for signing. |
| `transitioning` | Being phased in or out — honoured until its stated expiry. |
| `revoked` | Withdrawn — never honoured again. |

Each time peipkg refreshes a repository, it verifies the new descriptor against the keys in the descriptor it already trusts, and then adopts the new descriptor's key set as the current trust state. A rotation that is itself signed by a still-trusted key propagates automatically: you anchored once, and the chain carries forward from there without you re-anchoring. A `revoked` key is dropped and never accepted again.

The anchor therefore matters only at first contact. After that, trust is a chain, and each refresh extends it.

## Signature policy

`--policy` chooses how strict peipkg is about signatures for this repository.

- **`required`** (the default) — every descriptor and index must carry a valid signature from a trusted key. An unverifiable document is rejected. This is the correct setting for any repository reached over a network.
- **`optional`** — combined with no trust anchors, this puts the repository into **unsigned mode**: peipkg fetches and uses its metadata and packages without cryptographic verification.

Unsigned mode is a deliberate escape hatch for a scratch repository on a build host, or a local mirror you fully control. Every operation that touches an unsigned repository prints a warning:

```
peipkg: warning: repository "scratch" is unsigned — its metadata and
packages are not cryptographically verified
```

In unsigned mode, only the transport stands between you and a substituted package. Do not use it for anything reachable from an untrusted network.

## Priority

`--priority` is a number — lower is stronger, default `50` — that decides which repository wins when **more than one offers the same package**. If both an `official` repository at priority `10` and an `internal` one at priority `20` carry `nginx`, the resolver takes the `official` build. Priority does not restrict anything; it only breaks ties. Its full role in version selection is covered in [Dependency resolution](/peios/using-peios/package-management/dependency-resolution.md).

Priority also feeds one of the safety checks there: a package from a *lower*-priority repository that tries to displace a package installed from a *higher*-priority one is flagged for explicit authorisation, so a low-trust repository cannot quietly take over a package you got from a trusted one.

## Freshness — defeating a stale-repository attack

A signature proves a document is authentic; it does not prove the document is current. An attacker who cannot forge a signature can still serve you a genuine, correctly-signed, but old index — one from before a security fix was published — and a naive consumer would accept it.

peipkg closes that gap by tracking, per repository, the highest index version it has ever seen, and refusing any index that goes backward. Once you have seen version 5 of an index, version 4 — however well signed — is rejected. A repository can only ever move forward.

`--min-index-version N` lets you set that floor explicitly, out of band: if you know the current index is at least version `N`, anchoring that number means peipkg will reject anything older even on the very first fetch, before it has a history to compare against.

Going backward is one half of the attack; standing still is the other. A frozen repository — one that serves the same, correctly-signed index forever — never trips the rollback check, yet it can hold you on a view from before a security fix indefinitely. Against that, peipkg tracks when each repository last refreshed **with progress** and enforces a **maximum trusted age**: 30 days by default, tunable per repository with `max_trusted_age_days` in its `.repo` file or `--max-trusted-age-days` at add time.

When an install, upgrade, or downgrade finds a repository's trust state older than its maximum, peipkg refreshes that repository first. If it cannot — the repository is unreachable, or it refreshes without progressing — the operation is refused rather than planned against outdated metadata. Passing `--allow-stale` to the operation overrides the refusal; the override is warned about and recorded in the audit stream. A maximum above 180 days draws a warning on every operation, because a bound that loose effectively disables the check.

There is a third way to stand still, and it needs its own check. Maximum trusted age asks *how long since I last refreshed successfully* — and a repository can keep that answer flattering forever by bumping its index version on every publication while stamping the index with an ancient `generated_at`. Every refresh looks like progress; the metadata never actually moves. So peipkg separately enforces a **maximum index staleness** measured from the index's own `generated_at`: **90 days** by default, tunable per repository with `max_index_staleness_days` in its `.repo` file or `--max-index-staleness-days` at add time.

The two are deliberately independent, and raising one does not widen the other — a `max_trusted_age_days` of 180 still leaves the 90-day staleness window in place. An index past that window triggers a refresh before any install proceeds, and the same `--allow-stale` override applies, with the same warning and audit record. A staleness bound above 365 days draws a warning on every operation.

## Listing and removing repositories

```
peipkg repo list
peipkg repo remove <name>
```

`repo list` prints the configured repositories — name, base URL, priority, signature policy. It accepts `--json`.

```
$ peipkg repo list
official  https://pkgs.peios.org  priority=10  required
internal  https://pkg.corp.example  priority=20  required
```

`repo remove` deletes a repository's configuration and the trust state peipkg recorded for it. Packages already installed from that repository stay installed — removing a repository controls where future packages come from; it does not remove past ones. Those packages simply no longer have a source for upgrades until you add a repository that carries them again.

## Where the configuration lives

Each repository is one file: `/lcl/conf/peipkg/<name>.repo`, in flat TOML.

```toml
base_url         = "https://pkgs.peios.org"
priority         = 10
signature_policy = "required"
trust_anchors    = ["ef86709c4b1d8a02e5f3c719d640aa8b7c2e9105f8d3b6470a1c2e9d8b5f3a04"]
```

The files are hand-editable, and editing one is a legitimate way to configure a repository — a trust anchor written into the file is you supplying that anchor out of band. `peipkg repo add` is the convenient front end: it runs the ceremony and writes the file for you. Who may edit these files is, like everything else on Peios, the [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) on `/lcl/conf/peipkg/`.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded — the repository was added, listed, or removed. |
| `1` | The operation failed — the trust ceremony failed (`repo add` backs out and leaves nothing behind), or the subcommand was missing, unknown, or given the wrong arguments (a missing `repo add <name> <base-url>`, a malformed option). Removing a name that is not configured is not a failure. |
| `2` | A usage error before any command ran — no command, an unknown command, or a malformed global option. |

---

# Transactions and recovery

_Peios / Using Peios / Package management_

> Every peipkg change is an atomic, reversible transaction. This page covers the three phases, the commit instant, interrupted runs, and recover and history.

Every change peipkg makes — an install, an upgrade, a downgrade, a removal — is a **transaction**. A transaction is atomic: it either happens completely or not at all. There is no state in which a package is half-installed, and no failure, signal, or sudden power loss that can leave one there.

This page explains how that guarantee is built, what an interrupted run leaves on disk, and the two commands — `recover` and `history` — that exist because of it.

## The three phases

Every operation moves through the same three phases.

```mermaid
flowchart LR
    P["Plan<br/>read-only, no lock"] -->|"you approve"| S["Stage<br/>lock held; fetch + verify"]
    S --> C["Commit<br/>the atomic flip"]
    C --> D["Done"]
    P -.->|"abort"| X["Nothing changed"]
    S -.->|"abort / crash"| X
```

**Plan.** peipkg reads your request, reads the installed set, reads the cached repository metadata, and computes the ordered list of changes — or rejects the request. This phase is entirely read-only and takes no lock. It is why `--dry-run` and the query commands never block, even while another transaction is running, and why a plan can always be abandoned for free.

**Stage.** Once you approve the plan, peipkg takes a single-writer lock — only one transaction touches the system at a time — and prepares everything. It downloads every `.peipkg` in the plan and **verifies all of them before staging any one of them**, so no package's contents can influence another's verification. Verified payloads are written into a staging area, each file hash-checked as it lands. Nothing the system uses has changed yet.

**Commit.** peipkg moves the staged files into place and records the new package state. This is the phase that changes the system.

## The one instant that matters

Within the commit there is a **single instant** — the moment peipkg records the new state in its database — that divides the entire operation in two:

- **Before it:** any failure, any signal, any power loss rolls everything back. The staged files are discarded, any displaced files are put back, and the system is exactly as it was.
- **After it:** the operation is complete and durable.

There is no third outcome. A transaction is never "partly applied". The rest of this page describes the machinery that delivers this guarantee.

## Backups make rollback free

When peipkg replaces or removes a file, it does not overwrite or delete it. It **renames the old file aside** — to a sibling name in the same directory — and puts the new file in place. The old contents are untouched, just under a different name.

Rolling back is then simply renaming everything back. No data is copied, no contents are reconstructed; the rollback is the same cheap rename operation in reverse. This is why a failed transaction recovers to a byte-exact prior state.

Those set-aside files also outlive a successful commit for a while — retained so that an [`undo`](/peios/using-peios/package-management/keeping-a-system-current.md) of a recent transaction is fast and needs no network. They are cleaned up automatically as they age out.

## What an interruption leaves behind

If a transaction is interrupted mid-stage or mid-commit, you may find files with these names near where a package was being installed:

| Name | Is |
|---|---|
| `<name>.peipkg-staged-<id>` | An incoming file that had been staged but not yet moved into place. |
| `<name>.peipkg-backup-<id>` | A file that had been renamed aside to make room — the backup. |

These names are deliberate. They have no leading dot, so they are visible rather than hidden — you are meant to find them and understand them. The `<id>` is the transaction number; look it up with `peipkg history` to see which operation left it.

You do not clean these up by hand. peipkg knows about them and resolves them itself — see `recover`, next.

## Recovering an interrupted transaction

```
peipkg recover
```

When a transaction is interrupted before it commits, peipkg records it as **pending**. The next time peipkg starts any transaction it checks for a pending one first and rolls it back automatically before doing anything else — so in normal use recovery just happens, and you never see it.

`peipkg recover` runs that step on demand. Use it when a transaction was interrupted and you want the system put right immediately, without waiting for the next install or upgrade.

```
$ peipkg recover
recovered: the interrupted transaction was rolled back
```

Recovery only ever **rolls back**. A transaction that was interrupted after its commit instant is already complete — there is nothing pending and nothing to recover. Recovery deals exclusively with the "before the instant" case, and it always resolves it the same way: back to the prior state.

If there is no pending transaction, `recover` says so and exits cleanly.

### Across more than one root

When an operation spans more than one named root, those roots commit as a unit under a single **cross-root transaction**, and `undo` reverses all participating roots together. `recover` reconciles pending cross-root transactions across every reachable root: a torn commit is rolled back, or — if it had already passed the commit instant — rolled forward to completion. See [Named roots](/peios/using-peios/package-management/named-roots.md) for how multiple roots come about.

## The transaction log

```
peipkg history
```

`history` prints the transactions peipkg has carried out, most recent first — each with an id, a timestamp, its state (`committed`, `rolled-back`, or `pending`), and a short summary.

```
$ peipkg history
49  2026-05-19T14:02:10Z  committed    upgrade nginx, zlib
48  2026-05-19T09:31:55Z  committed    install nginx
47  2026-05-18T22:14:03Z  rolled-back  install brokenpkg
```

| Option | Effect |
|---|---|
| `-n N` | Show at most `N` transactions. `-n 0` shows all of them. The default is `20`. |
| `--json` | Emit JSON. |

The history is what [`undo`](/peios/using-peios/package-management/keeping-a-system-current.md) reads to find the most recent transaction, and what ties a stray `*.peipkg-backup-<id>` file back to the operation that created it.

## Configuration files on upgrade

Upgrading a package raises a question for any configuration file under `/etc/` that the package owns: the new version ships a new default, but you may have edited the old one. peipkg decides per file, by comparing the file on disk against the hash it recorded at install:

- **Unchanged since install** — peipkg replaces it with the new default. You wanted the package's settings, and you get the current ones.
- **Edited since install** — peipkg keeps your file untouched and writes the new default beside it as `<name>.peipkg-new`. The upgrade report notes it:

  ```
  peipkg: warning: /etc/nginx/nginx.conf has been modified since install —
    keeping it; the new default was written to /etc/nginx/nginx.conf.peipkg-new
  ```

Your edits are never silently discarded, and the new defaults are never silently lost — you are simply told the two diverged and left to merge them when you choose.

## Side effects run after commit

A few packages need a system-wide step after their files are in place — refreshing the shared-library cache, the kernel-module map, or the manual-page index. peipkg runs those steps after the commit instant, once per transaction.

Because they run after the operation is already complete and durable, a side-effect step that fails is reported as a warning, not a failure. The transaction stands; the step is one that corrects itself the next time it runs. An install is never rolled back over a stale cache.

## Why this shape

The pay-off of the three-phase model is concrete:

- An install interrupted by a crash or a power cut never leaves a broken package — it leaves either the old state or the new one.
- The plan you approve under `--dry-run` is the plan that executes — resolution is read-only and deterministic.
- Queries and dry-runs never wait on an in-flight transaction, because only staging and commit take the lock.
- Every transaction is reversible, by `undo` for a recent one or `downgrade` for a specific package.

The model rests on one principle: all the fallible work — downloading, verifying, staging — happens before the commit instant, so the commit itself is the only point at which the system changes state.

## Where to go next

For the commands that reverse a committed transaction, read [Keeping a system current](/peios/using-peios/package-management/keeping-a-system-current.md).

For how a transaction that spans several named roots commits and recovers as a unit, read [Named roots](/peios/using-peios/package-management/named-roots.md).

For the events every transaction emits, read [Auditing](/peios/security-fundamentals/auditing/overview.md).

---

# Dependency resolution

_Peios / Using Peios / Package management_

> How a request becomes a concrete plan — dependencies, conflicts, provides and replaces, version choice, and the elevated-authorisation prompt.

When you ask peipkg to install `nginx`, the request does not yet describe the full work. `nginx` needs other packages; those need others; some version of each has to be chosen; everything has to be installed in an order where a package's dependencies come before it. Turning the short request into that concrete, ordered plan is **resolution**, and it is the first thing every change command does.

## Resolution is a pure calculation

peipkg resolves from **metadata alone** — the package descriptions in the cached repository indexes and in installed packages' manifests. It does not download any `.peipkg` files to work out a plan. Downloading happens later, only once a plan is approved.

That makes resolution a pure calculation over three inputs — your request, the set of installed packages, and the set of available packages — and gives it a property worth relying on: it is **deterministic**. The same inputs always produce the same plan. The plan you inspect with `--dry-run` is the plan that will execute; nothing is re-decided between previewing and applying.

If a request cannot be satisfied — a dependency that no repository can provide, two requirements that contradict — resolution rejects it, with an explanation, before anything is fetched or changed.

## The relationships between packages

A package's manifest declares how it relates to others. Four relationships drive resolution.

**Dependencies.** A package can require other packages, each by a version range. peipkg pulls every dependency into the plan, and their dependencies in turn, until the request is closed.

**Conflicts.** A package can declare that it cannot coexist with another. If a plan would put two conflicting packages on the system at once, resolution rejects it.

**Provides.** Several packages can advertise the same capability — a **virtual** name that is not itself a package. A dependency written against that name is satisfied by any package that provides it. This is how "needs a mail transport agent" can be met by whichever one you actually install.

You can ask for a virtual name directly, too: `peipkg install coreutils` works even when nothing is named `coreutils`, and installs whichever package provides it. Because you asked for one name and got a package with another, peipkg says which one it chose. Upgrades and removals are the exception — those name a package you already have, so they match by name only and never substitute one package for another.

**Replaces.** A package can declare that it supersedes another — the usual case being a rename, or a merge of two packages into one. Installing a package that `replaces` another causes the replaced package to be removed as part of the same plan.

`provides` has a stronger cousin: a **claim**, a single shared name that exactly one package may hold at a time. Where any number of packages can advertise the same `provides` name at once, a claim has one holder — see [Claims](/peios/using-peios/package-management/claims.md).

A dependency can also be routed into a different root, written `Depends: foo IN <root>`, so that a whole root can be composed through the dependency graph — see [Named roots](/peios/using-peios/package-management/named-roots.md).

## Choosing a version

When more than one version of a package could satisfy the plan, peipkg chooses by a fixed rule:

1. Prefer the highest version that satisfies every constraint on that package.
2. When two repositories offer the same version, prefer the one with the stronger (lower-numbered) **priority** — see [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

When the candidates are competing to fill a *virtual* name rather than being versions of one package, "highest version" means the version each one advertises **for that name**, not its own. Two packages providing `coreutils` are unrelated software numbered on unrelated scales, so peipkg compares what each offers the role — otherwise the role would go to whichever project numbers its releases higher. If everything above still ties, package name order settles it, so the answer never depends on the order packages happened to be read in.

For an upgrade, the installed version anchors the search: peipkg looks for something newer and stays put if there is nothing newer to move to.

**Optional dependencies** are never chosen automatically. A package can suggest companions that are useful but not required; peipkg surfaces those as suggestions and leaves the decision to you. A plan only ever contains what is genuinely needed plus what you asked for.

## The plan

The output of resolution is the ordered list of operations you see at the `proceed?` prompt — installs, upgrades, downgrades, and removals, sequenced so every package's dependencies are in place before it.

A removal gets one extra step. peipkg first computes the **reverse-dependency closure** — everything that would be left needing a package you are removing — and either refuses the plan or, with `--cascade`, widens it to include them. That logic is covered in [Installing and removing packages](/peios/using-peios/package-management/installing-and-removing.md).

Resolution is also **bounded**. A dependency graph cannot be crafted to make peipkg search forever; the resolver works under a hard cap and gives up cleanly rather than hanging if a graph is pathological.

## Elevated authorisation

Most of a plan is routine, and the single `proceed?` prompt covers it. A plan can also contain an action that a routine yes should not cover — one you must review and approve individually, on its own.

peipkg detects three such actions and, for each one in a plan, asks a separate question:

```
this operation requires elevated authorisation:
  nginx would move backward from 1.27.5 to 1.27.4
authorise this specific action? [y/N]
```

The three actions that trigger it:

| Action | Why it is elevated |
|---|---|
| **A downgrade** | Moving a package backward can reintroduce a problem a newer version fixed. It should be a conscious choice, never a side effect of some larger plan. |
| **A foreign `replaces`** | A package from a *lower*-priority repository using `replaces` to displace a package you installed from a *higher*-priority one. Left unguarded, a low-trust repository could quietly take over a package you trusted a better source for. |
| **A low-trust `provides`** | A package from a low-trust repository advertising a virtual capability in a way that shadows a package from a more-trusted source — satisfying a dependency with its own build instead of the one you would expect. |

Two things make this prompt different from the routine one:

- **`--yes` does not satisfy it.** `--yes` skips the routine `proceed?` prompt; it has no effect on an elevated-authorisation question.
- **It fails closed.** With no terminal to answer on — an unattended script, a pipeline — there is no answer, so the action is not authorised and the operation is cancelled. An elevated action is never authorised by default.

Each elevated action is presented and authorised on its own; approving one does not approve the next. And the authorising act is itself written to the [audit stream](/peios/security-fundamentals/auditing/overview.md) — the record shows not just what was done, but that it was specifically authorised and what was authorised.

If an elevated action is not one you want, the plan as a whole is the thing to reconsider: where the package is coming from, whether the repository priorities are right, whether the downgrade is really what you meant.

## Where to go next

For the flow that presents and applies the plans resolution produces, read [Installing and removing packages](/peios/using-peios/package-management/installing-and-removing.md).

For the single-holder extension of provides, read [Claims](/peios/using-peios/package-management/claims.md).

For the repository priorities and trust states resolution consults, read [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

---

# Claims

_Peios / Using Peios / Package management_

> A claim is a shared filesystem name that only one installed package may hold. How claims materialise as symlinks, and the claim command for reassigning them.

Some filesystem names can be provided by more than one package. Two registry daemons — `loregd` and an alternative — both install a working binary, but only one of them can own `/usr/bin/registryd`. peipkg calls that shared name a **claim**, and it owns the machinery that decides which package holds it.

Three words are used precisely on this page:

- A **claim** is a single shared filesystem name that many installed packages may be able to answer, but exactly one may hold at a time.
- An **eligible provider** is an installed package able to answer a given claim.
- The **holder** is the one package that currently answers it.

(This is unrelated to [token claims](/peios/security-fundamentals/identity/claims.md) — the security-token attribute concept in the identity docs. The same word names a different subsystem.)

## What a claim is

A claim materialises as a **symlink** on disk. The link lives at the **claim path** — the shared name, such as `/usr/bin/registryd` — and points at a file inside the holder's payload, the **target**, such as `/usr/sbin/loregd`. Ask the system for the shared name and you reach whichever provider currently holds it.

```
/usr/bin/registryd -> /usr/sbin/loregd
```

The claim symlink is **owned and managed by peipkg**. It is not shipped inside any package's payload; no provider installs it, and removing a provider does not remove it out from under peipkg. peipkg creates, repoints, and tears down the link as part of the transactions that install, remove, grant, and revoke.

The two sides of a claim are declared in package manifests:

- A **provider** package declares the **target** file it offers for a claim — the real binary the shared name would resolve to.
- A **consumer** package references the **claim path** where it expects the shared name to appear.

peipkg joins the two. A provider can offer a target for a claim, a consumer can depend on the name being present, and peipkg mediates between them, keeping exactly one provider wired to the shared path at any moment.

## Claims and provides/replaces

A claim is the "exactly one owner of a shared name" extension of the `provides` and `replaces` relationships covered in [Dependency resolution](/peios/using-peios/package-management/dependency-resolution.md). There, several packages can advertise the same *virtual* name and any one of them satisfies a dependency written against it — that mechanism answers "is something here that provides this?". A claim goes one step further: it answers "which single provider owns this concrete filesystem name right now?", and enforces that the answer is never more than one. Provides establishes that a name can be answered; a claim decides which package answers it on disk.

## Auto-claim on install

Installing an eligible provider auto-claims every claim it provides that is currently unheld. If no package yet holds `registryd`, installing `loregd` makes `loregd` the holder and materialises the link as part of the same transaction — you get a working shared name without a second step.

The rule is strictly unheld-only. Auto-claim never overrides a claim that is already held by another package. Install a second registry daemon while `loregd` holds `registryd` and the newcomer is installed as an eligible provider but takes nothing; `loregd` stays the holder. Reassigning a held claim is always a deliberate act — see the `claim` command below, or the install flags that force it.

### Install flags

`peipkg install` accepts flags that override the default auto-claim behaviour for the packages in that install:

| Option | Effect |
|---|---|
| `--no-claim` | Claim nothing. Install the provider(s) without taking any claim, even ones that are currently unheld. |
| `--claim <names>` | Force-claim the named claims (comma-separated), overriding the current holder of each. |
| `--claim-all` | Force-claim every claim the installed packages provide, overriding incumbents. |

Two combinations are hard errors:

- `--claim-all` together with `--claim`.
- `--claim-all` together with `--no-claim`.

`--claim` and `--claim-all` are how you take a claim that is already held during an install; without them, an install only ever fills claims that are empty.

## The claim command

`peipkg claim` inspects a claim and reassigns its holder. It has three forms.

```
peipkg claim <claim>
peipkg claim <claim> grant <package>
peipkg claim <claim> revoke
```

**Report status.** With just a claim name, peipkg prints the current state of the claim: the current holder, the materialised links shown as `path -> target`, and the installed eligible providers.

```
$ peipkg claim registryd
holder: loregd
links:
  /usr/bin/registryd -> /usr/sbin/loregd
eligible providers:
  loregd
  altregd
```

**Grant.** `grant <package>` makes an installed eligible provider the holder. peipkg atomically repoints all of the claim's links to that package's targets — every path the claim covers moves together, or none does. The named package must be an installed eligible provider for the claim.

```
$ peipkg claim registryd grant altregd
```

**Revoke.** `revoke` removes the grant. The claim becomes **unheld** and its links are torn down. peipkg does not automatically promote another provider — a revoked claim has no holder until you grant one.

| Option | Effect |
|---|---|
| `--yes`, `-y` | Skip the confirmation prompt. Applies to `grant` and `revoke`. |

`grant` and `revoke` each run as a standalone transaction. Like every other peipkg change they appear in [`history`](/peios/using-peios/package-management/transactions-and-recovery.md), can be reversed with [`undo`](/peios/using-peios/package-management/keeping-a-system-current.md), and are fully auditable — each emits a `claim` event to the [audit stream](/peios/security-fundamentals/auditing/overview.md). A reassignment is never a silent, unrecorded edit to a symlink; it is a first-class, reversible operation.

## What happens on uninstall

Uninstalling the current holder **auto-withdraws** the claim. The holder is going away, so peipkg tears down its links and the claim becomes **unheld** as part of the removal.

peipkg does not auto-promote another provider in its place — an automatic promotion would be the kind of silent reassignment claims exist to prevent. Instead it surfaces the remaining eligible providers and hands you a ready-to-run command to reassign the claim yourself:

```
$ peipkg remove loregd
...
claim 'registryd' is now unheld. eligible providers: altregd
to reassign it, run:
  peipkg claim registryd grant altregd
```

If the holder was the only eligible provider, the claim is left unheld with nothing to promote, and any consumer relying on the shared name will find it absent until a new provider is installed.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded — the status was reported, or the grant or revoke was applied (a declined prompt is also `0`: nothing failed). |
| `1` | The operation failed — the claim has no eligible provider, the named package is not an eligible provider, or a named package or claim is not installed. |
| `2` | A usage error — an unknown subcommand (the only subcommands accepted after the claim name are `grant` and `revoke`) or a malformed option. |

---

# Inspecting and verifying

_Peios / Using Peios / Package management_

> The read-only commands — list, info, files, owns, and search to see what is installed and available, verify to check files are intact, clean to tidy the cache.

The commands on this page change nothing. They report what is installed, look up a package in the repositories, check that installed files are still intact, and tidy the metadata cache. Use them to see what is on a system and to check that it is still as it should be.

## Listing what is installed

```
peipkg list
```

`list` prints every installed package — name, version, and architecture.

```
$ peipkg list
nginx  1.27.5  x86_64
pcre2  10.44   x86_64
zlib   1.3.2   x86_64
```

With `--json` it emits the same set as JSON, with each package's origin repository included.

## Showing one package's details

```
peipkg info <package>
```

`info` prints the full record of one installed package: its version and architecture, the repository it came from (or `(local file)` if it was installed from a `.peipkg` directly), when it was installed, and — from its manifest — its description, license, and homepage.

```
$ peipkg info nginx
name:         nginx
version:      1.27.5
architecture: x86_64
origin:       official
installed:    2026-05-19T14:02:10Z
description:  HTTP and reverse proxy server
license:      BSD-2-Clause
homepage:     https://nginx.org
```

## Listing the files a package owns

```
peipkg files <package>
```

`files` prints every filesystem object the package owns — the files, directories, and symlinks that were placed by its install and are tracked against it. This is the record peipkg uses to remove the package cleanly and to verify it.

```
$ peipkg files zlib
/usr/lib/libz.so.1
/usr/lib/libz.so.1.3.2
/usr/include/zlib.h
```

## Finding which package owns a path

```
peipkg owns <path>
```

`owns` is the reverse lookup: given a path, it reports which installed package placed it.

```
$ peipkg owns /usr/lib/libz.so.1
zlib
```

If no installed package owns the path, `owns` says so and exits non-zero. A path that nothing owns is either not part of any package or was created outside peipkg.

## Searching the repositories

```
peipkg search <term>
```

`search` looks through the configured repositories' active indexes for packages whose name or description contains the term, case-insensitively. It searches what is available, not what is installed — it is how you find a package before installing it.

```
$ peipkg search proxy
nginx     1.27.5  [official]  HTTP and reverse proxy server
haproxy   2.9.7   [official]  Reliable, high-performance TCP/HTTP load balancer
```

`search` reads the cached repository metadata, so run [`peipkg refresh`](/peios/using-peios/package-management/keeping-a-system-current.md) first if you want it to reflect the latest catalog. A repository with no usable cached metadata is skipped with a warning rather than failing the search. `--json` emits the matches as JSON.

## Verifying installed files

```
peipkg verify [package]...
```

`verify` checks that what is on disk still matches what was recorded when each package was installed. For every file a package owns it checks:

- a **regular file** — that it is present and its content still hashes to the recorded value;
- a **symlink** — that it is present and still points where it was recorded to point;
- a **directory** — that it is still present and still a directory.

With no arguments, `verify` checks every installed package. With package names, it checks only those.

```
$ peipkg verify nginx
nginx: /etc/nginx/nginx.conf has been modified since install
verify: 1 problem(s) found
```

A reported file is not necessarily a fault. A configuration file you edited on purpose will show up — `verify` is telling you the file diverged from the package's version. `verify` reports; it does not judge intent and it does not change anything.

If every checked file is intact, `verify` says so and exits `0`. If anything diverged, it lists each problem and exits non-zero — which makes it usable as a check in a monitoring script.

## Cleaning the metadata cache

```
peipkg clean
```

peipkg keeps a verified copy of each repository's metadata in a local cache. When you remove a repository, its cached metadata is no longer needed. `clean` deletes only those orphaned cache files — the ones belonging to repositories that are no longer configured.

```
$ peipkg clean
removed 2 orphaned cache file(s)
```

`clean` never touches the metadata of a repository that is still configured, so it cannot leave you needing a refresh. It is pure housekeeping, and it is optional; run it whenever you like.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The command succeeded. For `verify`, every checked file was intact. |
| `1` | The command failed — a named package is not installed, a path is owned by nothing, or, for `verify`, at least one file had diverged. |
| `2` | A usage error — an unknown command or a malformed option. |

---

# Named roots

_Peios / Using Peios / Package management_

> peipkg can operate on several roots at once — named references, dotted composition, default_root, the cascading upgrade, and cross-root dependencies.

Every peipkg command runs against a **root** — a subtree it treats as a complete Peios installation. By default that root is `/`, the running system. The [`--root DIR`](/peios/using-peios/package-management/overview.md) global option points peipkg at a different one: an image mounted at `DIR`, an offline system under maintenance, a tree being built.

A single bare path is enough when there is exactly one alternate tree and you always spell it out in full. It stops being enough when a system is several cooperating trees that are built and kept current together. Peios's **dynamic initramfs** is the case that motivates this page: a real system root at `/` and an initramfs image at `boot/initramfs`, both assembled from ordinary packages, both upgraded by the same package manager. Passing `--root boot/initramfs` on every command that touches the image — and remembering that path everywhere — is the friction named roots remove.

So `--root` generalises. A root can **register** other roots under it by name, reference them by that name instead of a path, compose those names by dotting, install a dependency into a *different* root than its depender, and upgrade every nested root in one command. The mechanism is deliberately general — the initramfs is one arrangement it expresses, not a special case wired into the tool.

## What a named root is

A **named root** is a registered name for a subtree, recorded in the registry of the root it lives under. The name `initramfs` bound to the path `boot/initramfs` is a named root of `/`: it says "within this root, the name `initramfs` means the subtree at `boot/initramfs`, and that subtree is itself a root."

Two ideas follow from that.

**Every root has its own registry.** The bindings a root knows about are its own. `/` may register `initramfs`; what `initramfs` registers is recorded inside `initramfs`, not in `/`. Paths in a registry are stored **relative to the root that owns them**, so a registry travels with its tree — an image built in one place and mounted in another resolves the same way.

**References are resolved from the current root.** The current root is whatever `--root` points at (or `/` when it is omitted). A reference is resolved by starting there and walking its registry.

### Referencing by name

Once `initramfs` is registered under `/`, these two commands target the same tree:

```
$ peipkg --root boot/initramfs install live-boot
$ peipkg --root initramfs install live-boot
```

The second names the root instead of spelling out its path. The name is looked up in the current root's registry and resolved to the bound path.

### Dotted composition

Names compose by dotting. `initramfs.subroot` resolves left to right: from the current root, walk into `initramfs`'s registry, then from there into `subroot`. Each dot is one more step outward through one more registry.

```
$ peipkg --root initramfs.subroot install ...
```

Every dotted segment must match the grammar `[a-z0-9][a-z0-9_-]*` — a lowercase-alphanumeric lead followed by lowercase alphanumerics, hyphens, and underscores. A segment that names no registered root is a **hard error**, not a silent miss. A registry arrangement that loops back on itself is detected and reported as a **resolution cycle** rather than followed forever.

### Path or name: how `--root` decides

`--root` accepts either form and tells them apart by a single rule:

- **Any value containing `/` is a filesystem path** and is used exactly as before — `--root boot/initramfs`, `--root /mnt/image`, `--root ./tree`. This is unchanged behaviour.
- **A bare dotted identifier is a named reference** — `--root initramfs`, `--root initramfs.subroot` — resolved through the registries as above.

The `/` is the discriminator. If you mean a literal directory, include a slash (even `./name`); if you mean a registered name, use the bare identifier.

## The `root` command

`root` manages named roots. **Every subcommand operates on the current root's registry** — the registry of whatever `--root` points at. To manage the initramfs's own named roots, aim `--root` at the initramfs first.

```
peipkg root add <name> <path>
peipkg root remove <name> [--purge]
peipkg root list [--json] [--tree]
peipkg root show <reference> [--json]
```

`root` with no subcommand is a usage error — *a subcommand is required (add, remove, list, show)*. An unknown subcommand is a usage error too.

### `root add`

```
peipkg root add <name> <path>
```

Register a named root in the current root's registry. `<name>` is a single root segment — it must match `[a-z0-9][a-z0-9_-]*` and must not contain a dot; you register one name at a time, not a dotted chain. `<path>` is stored relative to the current root. There are no flags.

```
$ peipkg root add initramfs boot/initramfs
```

### `root remove`

```
peipkg root remove <name> [--purge]
```

Unregister a named root. By default this removes only the **registry entry** — the files on disk are left in place, so the subtree survives and can be re-registered. `--purge` additionally deletes the named root's filesystem tree.

| Option | Effect |
|---|---|
| `--purge` | Also delete the named root's filesystem tree, not just its registry entry. |

### `root list`

```
peipkg root list [--json] [--tree]
```

Print the current root's registry. Plain output lists the names registered directly under the current root.

| Option | Effect |
|---|---|
| `--json` | Emit JSON — for each entry its `name`, its stored `path`, its `resolved_path`, its `status`, and its `children`. |
| `--tree` | Recurse into each child's registry and print the whole nested arrangement. The recursion is cycle-guarded, so a registry that loops is reported rather than followed endlessly. |

### `root show`

```
peipkg root show <reference> [--json]
```

Resolve one reference and report on it. `<reference>` is either a dotted named-root reference or a path — the same path-or-name rule as `--root`. `show` reports the reference's resolved **path**, its **status**, and the number of packages installed in it.

The status is one of two words:

- **`present`** — the resolved path exists on disk and is a real root.
- **`dangling`** — the reference resolves through the registries, but the path it names is not there. The binding exists; the tree it points at does not.

| Option | Effect |
|---|---|
| `--json` | Emit the same report as JSON. |

`present` and `dangling` are the vocabulary the rest of this page uses for "the binding points at something real" versus "the binding is valid but its target is missing".

## Automatic target selection with `default_root`

A package's manifest can declare the root a top-level install of it should land in — its **`default_root`**. When you install such a package without saying where, peipkg reads that field and re-roots the install onto it for you.

```
$ peipkg install live-boot
```

If `live-boot`'s manifest sets `default_root: initramfs`, this installs into the `initramfs` root even though no `--root` was given. The package's manifest records where it belongs, so you do not have to specify it.

Two rules keep this predictable:

- **It applies only when no explicit `--root` was given.** Passing `--root` with any value, including a named one, states the target outright and suppresses `default_root` re-rooting entirely. An explicit root always wins.
- **The packages in one command must agree.** If the packages named in a single install declare two or more distinct default roots, peipkg cannot pick one and does not guess — the command is rejected as an error. Split it into one command per target.

`default_root` only chooses a target; it does not create or resolve anything the registry could not already reach. The chosen root is resolved through the registries like any other named reference.

## Cascading upgrade

By default `peipkg upgrade` reconciles the current root and every named root nested under it, recursively — the whole tree of roots, in one command. Run it against `/` on a dynamic-initramfs system and both `/` and `boot/initramfs` are brought current together.

Each root is upgraded as an independent transaction that continues on error: peipkg walks the tree, upgrades each root on its own, and prints a per-root summary. One root's failure does not abort the others — a broken upgrade in `initramfs` leaves `/`'s upgrade to complete and report normally, and vice versa. You get one summary per root and a clear picture of which succeeded.

Each of those per-root upgrades is an ordinary transaction with the full atomic guarantee of [Transactions and recovery](/peios/using-peios/package-management/transactions-and-recovery.md); the cascade runs several of them, it does not weaken any one.

| Option | Effect |
|---|---|
| `--no-recurse` | Confine the upgrade to the current root only. The cascade into nested roots is disabled. |

Use `--no-recurse` when you deliberately want to move just one root — for example to upgrade the initramfs on its own without touching `/`, by pointing `--root` at it and disabling the cascade.

## Cross-root dependencies

A dependency can be routed into a different root than the package that declares it. A manifest writes this with `IN`:

```
Depends: foo IN initramfs
```

This says "I depend on `foo`, and `foo` belongs in the `initramfs` root" — the root name is resolved through the registries from the depending package's root. It is what lets a whole root be composed out of ordinary packages through the dependency graph: a single package installed into `/` can pull the pieces of the initramfs into `initramfs` as its dependencies, so the image is built by the same resolver and the same packages as everything else. An image builder starting from nothing can assemble an entire multi-root arrangement like this offline, from a declarative manifest, with the separate `peipkg-compose` binary — see [Composing a root](/peios/using-peios/package-management/composing-a-root.md).

`install` is the only verb that crosses roots. When resolution produces a plan whose changes land in more than one root, that plan is committed as a **two-phase commit** across the participating roots, under a single generated **cross-root transaction id** that ties the per-root pieces together. Either the change lands in every participating root or in none — the multi-root plan is atomic as a whole, not merely per root.

A plan that reaches beyond the current root announces it before you approve:

- a `note: this also changes other roots: ...` line naming the other roots the plan touches, and
- a per-line `-> <root>` tag on each plan entry that lands outside the current root, so you can see at a glance which change goes where.

You are never taken across a root boundary silently; a cross-root plan always shows its routing.

## Cross-root undo and recovery

Because a cross-root install commits as a unit, it reverses as a unit.

**`undo`** on a cross-root transaction reverses all of its participating roots together. There is no way to walk back only the `/` half of a change that also touched `initramfs` — the cross-root transaction id binds the pieces, and `undo` reverses the whole. See [Keeping a system current](/peios/using-peios/package-management/keeping-a-system-current.md) for `undo` in general.

**`recover`** understands cross-root transactions too. It reconciles them across every reachable root, walking the registry outward from the current root to find each participant. A cross-root commit that was torn by an interruption is resolved the same way a single-root one is — with one addition that follows from the two-phase commit: a torn commit is rolled back if it was interrupted before its point of no return, or rolled forward to completion if it had already passed that point. Either way every participating root ends in the same, consistent state.

This is the single-root recovery model of [Transactions and recovery](/peios/using-peios/package-management/transactions-and-recovery.md) extended across roots: the same commit-instant guarantee, now applied to a commit that spans several trees at once. Read that page for the underlying transaction model this builds on.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Failure — an unregistered name, a dangling or otherwise invalid reference, a resolution cycle, a per-root transaction that failed, or a cross-root commit failure. |
| `2` | Usage error — a missing or unknown `root` subcommand, a malformed option, or the wrong number of positional arguments. |

---

# Composing a root

_Peios / Using Peios / Package management_

> peipkg-compose builds a fresh, package-owned root from a declarative manifest — offline and deterministically. The lock and build verbs and the lock file.

`peipkg` mutates a running system in place: every change is an atomic, reversible [transaction](/peios/using-peios/package-management/transactions-and-recovery.md) against a live root. **`peipkg-compose`** is its counterpart on the assembly side. Instead of changing a system that already exists, it builds a fresh, fully package-owned root directory from nothing — offline, deterministically, and verifiably, with no live system involved anywhere in the process.

`peipkg-compose` is a separate binary, not a `peipkg` subcommand. You run `peipkg-compose`, and its two verbs (`lock` and `build`) are the whole of its command surface.

Given a declarative TOML **manifest** that names a package set and the repositories to draw it from, compose produces a populated root directory: package payloads laid out at their installed paths, a seeded peipkg state database at `var/lib/peipkg/db.sqlite`, and each repository written out as `lcl/conf/peipkg/<name>.repo`. The result is a legitimate peipkg-managed system — once booted it can manage itself, and repository trust bootstraps on its first `refresh`.

It is meant for **image builders** — an outer image-assembly tool, or a person standing one up by hand. Because it only ever writes inside the output directory and never touches the host `/`, it is safe to run on a non-Peios host, and even on a host of a different architecture from the image being built.

## The mental model

Compose runs in two stages, and the two verbs map onto them.

**Stage 1 — resolve.** Read the manifest, resolve the requested package set against the declared repositories into a complete transitive closure, verify each repository's trust chain, and pin the result. This stage needs repository metadata; its output is the **lock file**.

**Stage 2 — assemble.** Fetch the pinned `.peipkg` payloads, verify them against the lock's hashes, and lay out the root directory. This stage needs only package bytes, not metadata, and can run fully air-gapped once a lock exists.

`lock` does Stage 1 alone. `build` does Stage 2, running Stage 1 first when it needs to. Everything build-stamped in the output is derived from the manifest's `source_date`, so given the same manifest, lock, and packages, the output is **reproducible**.

## `peipkg-compose lock`

```
peipkg-compose lock <manifest> [-o <lock>]
```

Resolve the manifest against its repositories, verify the trust chain, and write the lock file. `lock` builds nothing — it produces only the lock.

The `<manifest>` positional is required. Flag order is not significant: the manifest may appear before or after `-o`.

| Option | Effect |
|---|---|
| `-o <lock>` | Output lock path. Defaults to the manifest path with a trailing `.toml` replaced by `.lock.toml` — so `image.toml` locks to `image.lock.toml`. Only the single-dash `-o` form exists here; there is no `--out` for `lock`. |

## `peipkg-compose build`

```
peipkg-compose build <manifest> --out <dir> [--locked | --update]
                     [--dangerously-bypass-path-restrictions]
```

Assemble the root directory. The `<manifest>` positional is required; flag order is not significant.

`--out <dir>` is required — omitting it is a usage error. The directory named by `--out` must not already exist: compose creates it, and refuses to build into or over an existing tree.

| Option | Effect |
|---|---|
| `--out <dir>` | The output root directory. Required. Must not already exist. |
| `--locked` | Require an existing lock and do **not** resolve. Fails if no lock is present. Fetches only package bytes, taking integrity from the lock's hashes — the air-gap-friendly path. |
| `--update` | Re-resolve from scratch, overwrite the lock, then build. |
| `--dangerously-bypass-path-restrictions` | Permit packages that declare `special_system_package` to compose payloads outside the payload layout rules. An image built from a package set including the base filesystem needs this; `peiso` passes it through from `bypass_path_restrictions` in the image's build spec, so the grant stays a visible decision of the image rather than something the composer assumes. |

`--locked` and `--update` are **mutually exclusive**.

With neither flag, `build` follows Cargo's `build` / `Cargo.lock` behaviour:

- if the sibling lock is **missing**, it resolves, writes the lock, and builds;
- if the sibling lock is **present**, it verifies the lock still matches the manifest and then builds.

Use `--locked` for reproducible or air-gapped rebuilds where no metadata resolution should happen at all, and `--update` when you want to pick up newly-available versions and refresh the lock in the same run.

## The manifest

The manifest is a TOML file describing the package set to assemble and the repositories to draw it from. Any filename works — it is passed positionally — and its sibling lock is `<stem>.lock.toml`. The manifest and the lock are **tool-local build artifacts**; they are not an on-wire Peios format.

The parser is strict: an unknown key anywhere in the manifest is a hard error, not a warning. A mistyped field name fails the build rather than being silently ignored.

```toml
schema = 1
arch = "x86_64"
source_date = "2026-07-01T00:00:00Z"

local_packages = ["./bootstrap/*.peipkg"]

[[repository]]
name = "core"
base_url = "https://repo.example.org/core"
priority = 10
signature_policy = "required"
trust_anchors = ["a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"]

[[root]]
name = "initramfs"
path = "boot/initramfs"

[[package]]
name = "base-system"

[[package]]
name = "nginx"
version = ">=1.27, <1.28"
repository = "core"

[[package]]
name = "live-boot"
root = "initramfs"
```

### Top-level fields

| Field | Type | Required | Meaning |
|---|---|---|---|
| `schema` | int | yes | Manifest schema version. Must be `1`. |
| `arch` | string | yes | Target architecture; becomes the image's primary architecture. |
| `source_date` | string | yes | An RFC 3339 timestamp. Fixes every build-stamped time in the output, for reproducibility — the manifest's `SOURCE_DATE_EPOCH`. |
| `local_packages` | array of strings | no | Paths or globs of local `.peipkg` files. A bootstrap path for packages that live in no repository. |
| `[[repository]]` | array of tables | no | Package sources. Also written into the image as `.repo` files. |
| `[[root]]` | array of tables | no | Named roots, for a multi-root image. |
| `[[package]]` | array of tables | yes | The packages to install. Must be present and non-empty — an empty package set is an error. |

### `[[repository]]`

A repository table has the same shape as a [`.repo` file](/peios/using-peios/package-management/repositories-and-trust.md). These entries drive metadata fetch and trust verification during the build, **and** are written verbatim to `lcl/conf/peipkg/<name>.repo`, so the booted system inherits the same repositories and the same trust anchors.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `name` | string | yes | Repository name; also the `.repo` filename stem. |
| `base_url` | string | yes | Where the repository's index and packages are served from. |
| `priority` | int | no | Selection priority. Default `50`. |
| `signature_policy` | string | no | `required` (default) or `optional`. |
| `trust_anchors` | array of strings | no | Hex key fingerprints trusted to sign this repository's index. |
| `allow_insecure_transport` | bool | no | Permit non-TLS transport. Default `false`. |
| `min_index_version` | int | no | Reject an index older than this version. |

### `[[package]]`

Each package table names one package to install. Package **identity is `(root, name)`** — the same package may be requested in more than one root, but a duplicate `(root, name)` pair is an error.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `name` | string | yes | The package to install. |
| `version` | string | no | A version **constraint** — a range like `">=1.27, <1.28"` or an exact pin like `"9.9.1"`. Omitted, or `"*"`, means any version and the resolver picks the newest. |
| `repository` | string | no | Pin the source repository. Must name a declared `[[repository]]`. |
| `root` | string | no | Target a declared `[[root]]` — like [`peipkg install --root`](/peios/using-peios/package-management/named-roots.md). Empty means the package's own `default_root` if it has one, otherwise the anchor root. |

There is no `default_root` key in the manifest. `default_root` is a property each package carries from its repository index, and compose honours it automatically whenever a package's `root` is unset — just as the live consumer does.

### `[[root]]`

Each root table registers a [named root](/peios/using-peios/package-management/named-roots.md) in the built image.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `name` | string | yes | A single named-root segment matching `[a-z0-9][a-z0-9_-]*` — no dots. |
| `path` | string | yes | Location relative to the output root. May not be absolute, may not escape with `..`, and may not be `.`. |

## The lock file

The lock is generated by `lock`, or written implicitly by a default or `--update` build. Its header reads *generated by peipkg-compose — do not hand-edit*; it is never edited by hand.

For the full transitive package closure, the lock pins each package's exact **version**, **architecture**, **source repository** (or `local` for a local `.peipkg`), the **fetch URL**, and the content **SHA-256 hash** of the `.peipkg`. It records each package's target root when that is not the anchor, along with the manifest's `arch` and `source_date` and a **digest of the manifest**.

A build refuses a lock that no longer matches its manifest. A mismatched `arch`, a mismatched `source_date`, or a changed manifest digest all reject the lock. On the default path the error tells you to re-run with `--update` to refresh it.

The two explicit flags pin down the two ends of this behaviour:

- **`build --locked`** requires the lock to already exist and does no resolution at all — the reproducible, air-gapped rebuild.
- **`build --update`** re-resolves from scratch and rewrites the lock.

An unattended compose run cannot authorise the elevated actions that the live consumer would stop and prompt for. Rather than fail the build, compose surfaces those as stderr warnings. See [Elevated authorisation](/peios/using-peios/package-management/dependency-resolution.md) for what those actions are.

## Named roots and claims in a composed image

A composed image can be multi-root, and both mechanisms settle more simply here than on a live system — the detail lives on their own pages; this is only how compose relates to them.

**Named roots.** Each `[[root]]` becomes a [named-root](/peios/using-peios/package-management/named-roots.md) registry entry in the built image's database, so the booted system resolves `--root <name>` and cascades upgrades into those roots. Every root is a subdirectory nested under the single `--out` tree, and the whole multi-root image is assembled as **one tree** — compose needs none of the consumer's cross-root transaction machinery. The repositories and the named-root registry live at the anchor root.

**Claims.** A fresh build is the simplest case of [claim](/peios/using-peios/package-management/claims.md) reconciliation: there are no incumbents and exactly one known, closed package set, so every provided claim is auto-claimed. When two packages provide the same claim, the holder is chosen deterministically — the lexicographically smallest package name, matching `peipkg install`. Claim symlinks are written with targets relative to the link's own directory, so the root stays self-contained and relocatable.

## What ends up in the root

A successful build leaves a directory that is a valid peipkg-managed system:

- every package's payload laid out at its **installed paths**;
- a seeded peipkg state database at `var/lib/peipkg/db.sqlite`, recording the installed set, the named-root registry, and the settled claims;
- each declared repository written to `lcl/conf/peipkg/<name>.repo`, so the booted system inherits its repositories and their trust anchors.

The build is atomic as a whole tree. Because `--out` must not already exist, compose assembles into a sibling staging directory on the same filesystem and, on success, renames it into place atomically. A failed or interrupted run leaves the staging directory behind for inspection rather than a half-populated `--out` — there is no partial output to clean up or mistake for a finished one.

## What compose does not do

Compose's contract stops at producing a valid peipkg root. It is deliberately narrow.

- **Not a full image builder.** It produces only a directory tree — no bootloader, kernel, initramfs contents, registry seed, or peinit wiring. Those belong to whatever assembles the image around it.
- **Not a live-system tool.** It never touches the host `/`. There is no three-phase transaction, no commit boundary, no rollback journal, and no crash-recovery — the output is disposable, and its only atomicity is the single whole-tree rename above.
- **Not the producer side.** It does not build or sign `.peipkg` files and it does not serve repositories — those are the separate `peipkg-build`, `peipkg-repo`, and `peipkg-manager` tools. Compose consumes ordinary peipkg packages and repositories unchanged, exactly as [PSPU §5](/peios/advanced-peios/pspu/package-format-and-repository-protocol/scope-and-roles.md) defines them.
- **No side effects.** `ldconfig`, `depmod`, and `man-db` are not run; no KACS security descriptors are applied; no audit events are emitted. A booted system runs those itself.

No environment variables are read, and there is no `--version` flag.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success — or the help output (`-h`, `--help`, `help`). |
| `1` | A compose operation failed — resolution, trust verification, a fetch, a hash mismatch, a stale lock a `--locked` build could not use, or a file operation. |
| `2` | A usage error — no arguments, an unknown verb, a missing manifest, a missing `--out`, extra arguments, `--locked` with `--update`, or a bad flag. |

---

# The registry

_Peios / Using Peios / Concepts_

> The registry is the system's typed, access-controlled, watchable configuration store — a tree of keys and values reached only through kernel system calls.

The **registry** is the structured configuration store for a Peios system. It is a hierarchy of named **keys**, each holding **values** — typed pieces of data. Subsystems read their operational settings from it, services keep their definitions in it, and policy is delivered through it. Where a [file](/peios/security-fundamentals/file-access/overview.md) holds an opaque stream of bytes, a registry value holds a small, named, typed datum that some part of the system knows how to act on.

It is also kernel-mediated. There is no file under `/etc` you edit and no daemon socket you talk to directly. Every registry operation — opening a key, reading a value, writing configuration, watching for change — goes through a dedicated set of system calls. Userspace never touches the store underneath; the kernel is always in the path, which is what lets the registry carry the same access control, the same identity model, and the same auditing as every other protected object in Peios.

This page sketches the shape, names the handful of ideas that make the registry its own thing, and points at the pages that cover each one.

## The registry in one sentence

**The registry is a tree of keys and typed values, reached only through kernel system calls, where every key is an access-controlled object — and every value you read is the effective result of resolving a stack of layered writes.**

Ignore that last clause for now. For almost everything in this section you can picture the registry as a plain hierarchical store: one key per path, one value per name, the value you wrote is the value you read. That simpler picture is correct as far as it goes, and it is the right way to learn the model. The layering underneath — what "effective" really means — is the deepest idea here, and it gets its own page once the rest is solid.

## The shape

Three entities make up the namespace.

| Entity | What it is |
|---|---|
| **Hive** | A top-level namespace — the first component of every path. `Machine\` holds system-wide configuration; `Users\<SID>\` holds per-user configuration. In practice a running system exposes just a few. |
| **Key** | A node in the tree. Keys are containers: they hold child keys (forming the hierarchy) and values (holding data). Every key is a secured object with its own [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md). Keys are roughly the registry's directories. |
| **Value** | A named, typed datum living inside a key — a `REG_DWORD` number, a `REG_SZ` string, and so on. A key can hold many values, each with a distinct name. Values are roughly the registry's files. |

```mermaid
flowchart TD
    M["Machine\ (hive)"] --> S["System"]
    S --> K["KMES (key)"]
    K --> V1["BufferCapacity = 4194304  (REG_QWORD)"]
    K --> V2["MaxEventSize = 65536  (REG_DWORD)"]
    M --> R["Registry"]
    U["Users\&lt;SID&gt;\ (hive)"] --> US["per-user keys..."]
```

Paths are written with backslashes — `Machine\System\KMES` — and compared case-insensitively while preserving the case you wrote. A handful of well-known roots anchor everything: `Machine\` for the machine, `Users\<SID>\` per principal, and `CurrentUser\` as a convenience alias the kernel rewrites to the caller's own `Users\<SID>\`.

We will return to one subtree throughout this section as a running example: the KMES event subsystem reads its tuning parameters from values under `Machine\System\KMES`. It is small, real, and exercises every idea — types, meaning, security, and change notification.

## What the registry is not

The registry borrows a familiar shape, and the familiarity is a trap. Three wrong mental models attach themselves immediately; clearing them is most of understanding what the registry actually is.

**It is not a filesystem.** It looks like one — hierarchical paths, separators, containers and leaves — but it behaves differently in ways that matter. You reach it through dedicated registry system calls, not the file API. Values are typed data, not byte streams. Access is checked once, against the single key you open, with **no traversal check** on the keys above it — a process can read `Machine\System\Services\Jellyfin` without any access to `Machine\System\Services`. And security is attached per key; there is no such thing as a per-value permission.

**It is not a dumping ground of opaque settings.** Every key is a first-class secured object and every value is typed, access-controlled, and watchable. Nothing in the registry is unmanaged or hidden — there is no "miscellaneous junk" tier. It is a deliberate, governed surface, not a place things accumulate.

**It is not self-describing.** The registry stores a value's *type tag* but never interprets its bytes. It does not know that `BufferCapacity` must be a power of two, what its default is, or which subsystem reads it. That knowledge — the *meaning* of a value — lives entirely outside the registry: in the subsystem that owns the value, and, for a human looking it up, in [`regman`](/peios/using-peios/registry-administration/regman.md), the registry's manual. `regman` is the `man` of the registry — give it a path and it tells you a value's type, default, valid range, when a change takes effect, and what the setting is for. The registry cannot describe itself, so its manual is shipped beside it. This separation of storage from meaning is the first idea worth slowing down for, and it has consequences — a write the registry accepts can still be refused by the subsystem that reads it — so it gets its own page.

**It is not flat.** Underneath the single value you read, the registry keeps a *stack* of writes, each tagged with a **layer**, and resolves the winner on every read. That is what makes configuration revert cleanly, role install and uninstall work, and domain Group Policy apply and lift without residue. It is powerful and it is the one idea worth deferring — the [Layers](/peios/using-peios/registry-layers/layers.md) page pulls the curtain back once the rest of the model is clear.

## One registry, built from two parts

When precision matters, "the registry" is really two cooperating components: a kernel subsystem that owns the data model, path resolution, access control, change notification, and layer resolution; and a userspace store that persists the data on disk. They talk over a private protocol, and the kernel is the only authority — the store never sees who is asking and never makes a security decision.

For the conceptual model you do not need that split, and these pages mostly treat the registry as one thing. The division becomes useful only when you care about how the registry is backed, swapped, or recovered — so it waits for [LCS and sources](/peios/using-peios/registry-administration/lcs-and-sources.md), under Administration.

## Where to start

If you want the data model — hives, keys, the value types, case rules, and why a value is "typed but opaque" — read [Keys, values, and types](/peios/using-peios/registry-concepts/keys-values-and-types.md).

If you want the idea that the registry stores values it does not understand — what reject-or-keep means, and why the value the registry shows and the value a subsystem is actually using can legitimately differ — read [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md).

If you are ready for the layered truth under the effective view — precedence, the base layer, how deleting a layer automatically reverts its changes, and how roles and Group Policy ride on it — read [Layers](/peios/using-peios/registry-layers/layers.md).

If you want the security model — why every key carries a security descriptor, the registry-specific access rights, and the rule that security changes are *not* undone by layer removal — read [Access control on keys](/peios/using-peios/registry-security/access-control.md).

If you want to know how a service reacts to a configuration change instead of polling for it, read [Watching for changes](/peios/using-peios/registry-concepts/watches.md).

For the two command-line tools, read [`regman`](/peios/using-peios/registry-administration/regman.md) — the registry's manual, which tells you what a key *means* — and [`reg`](/peios/using-peios/registry-administration/reg.md) — the scripting interface that reads and writes what a key *is*. You consult the first to decide what to set, and use the second to set it.

---

# Keys, values, and types

_Peios / Using Peios / Concepts_

> The registry's data model — a tree of keys holding named typed values, the value types, the naming rules, and why a value is typed but never interpreted.

The registry's data model has exactly two kinds of thing in it: **keys** and **values**. A key is a node in the tree — a container that holds child keys and values. A value is a leaf — a named, typed piece of data living inside a key. There is no third kind of thing, and the nesting only ever happens through keys. That smallness is deliberate, and it is worth getting precise about before anything else, because every later idea is built on this shape.

## Two levels, and only two

Keys nest; values do not. A key can contain other keys (its subkeys) and it can contain values, but a value cannot contain anything — it is always a leaf. You cannot put a value "inside" another value, and there is no record type between a key and a value.

| | Holds | Held by | Role |
|---|---|---|---|
| **Key** | Subkeys and values | Its parent key | The container — the registry's directories |
| **Value** | Nothing (a leaf) | Exactly one key | The data — the registry's files |

This two-level rule is a hard constraint, not a convention. When you want structure, you make subkeys; when you want data, you make values. There is no other axis.

## Keys

A **key** is identified by its path — `Machine\System\KMES` names a key three levels down from the `Machine\` hive root. Each key holds its children and its values, and each key is a first-class secured object with its own [security descriptor](/peios/using-peios/registry-security/access-control.md); that is what makes "who can read or change this configuration" a per-key decision.

A few naming rules apply to each component of a path (each segment between separators):

- Any UTF-8 is allowed in a key name **except** backslash, forward slash, and the null byte. Backslash is the separator; forward slash is accepted on input and normalised to a backslash; null is never allowed.
- Components cannot be empty. `Machine\\System` (a doubled separator) and a trailing separator are both invalid.

Every key also has one **default value** — the single value whose name is the empty string. It is the key's "main" value, the one you get when you read the key without naming a value. Most keys also carry additional named values alongside it.

## Values

A **value** is a `(name, type, data)` triple stored in a key. A key can hold many values, each with a distinct name, plus the one unnamed default value.

Value names follow almost the same rules as key names, with one difference: **backslash and forward slash are allowed** in a value name. Value names are not paths — they are not hierarchical — so a separator inside one has no special meaning. Only the null byte is forbidden, and the empty string is reserved for the default value.

## The value types

A value's **type** is a small tag stored alongside its data. The full set:

| Type | Holds |
|---|---|
| `REG_DWORD` / `REG_QWORD` | A 32-bit / 64-bit integer. |
| `REG_DWORD_BIG_ENDIAN` | A 32-bit integer in big-endian byte order. |
| `REG_SZ` | A string. |
| `REG_EXPAND_SZ` | A string containing references to be expanded (e.g. environment variables) by whatever reads it. |
| `REG_MULTI_SZ` | An array of strings. |
| `REG_BINARY` | Raw bytes with no further structure. |
| `REG_LINK` | A symbolic-link target. The one type the registry acts on itself — see [Registry links](/peios/using-peios/registry-advanced/registry-links.md). |
| `REG_NONE` | No type / no meaningful data. |

There are also three hardware-resource types carried over for format fidelity. Peios assigns them no meaning — they behave exactly like `REG_BINARY` and the registry never produces them itself. You will essentially never author one.

## Typed, but opaque

Here is the property the rest of the topic leans on. The registry stores a value's type tag and its raw bytes, and that is **all** it does with them. It does not check that the bytes match the type. It does not parse a `REG_DWORD` into a number. It does not know that `Machine\System\KMES\BufferCapacity` is supposed to be a power of two, what its default is, or which subsystem reads it. The single exception is `REG_LINK` on a link key, which the kernel follows during path resolution.

So "typed" here means **tagged**, not **validated**. The type travels with the value so that a reader knows how to interpret the bytes — but the interpreting, the validating, and the deciding-what-to-do are all somebody else's job.

Two consequences follow immediately, and both get their own treatment:

- The registry can hold a value for a subsystem that is not even running, or a setting nobody has read yet. Storage does not require a reader.
- Whether a value is *valid* is never the registry's verdict. That belongs to whatever reads it — which is the whole subject of [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md).

## Names, case, and paths

Paths use the backslash as their canonical separator. A forward slash is accepted on input and normalised to a backslash, so `Machine/System/KMES` and `Machine\System\KMES` name the same key; the stored, canonical form always uses backslashes.

Comparison is **case-insensitive but case-preserving**. `KMES` and `kmes` resolve to the same key, but the registry keeps whatever case you wrote for display. The matching uses a fixed Unicode case-folding rule, so it does not depend on locale.

One sharp edge: there is **no Unicode normalisation**. Two different byte sequences that render as the same character (a precomposed `é` versus `e` plus a combining accent) are two *different* keys. Case is folded; representation is not.

## Volatile keys

A key can be created **volatile**, meaning it is stored only in memory and disappears on reboot or when its store unloads. It is the registry's home for runtime-only state that should never survive a restart. Volatility is fixed at creation, and there is one structural rule: the children of a volatile key must themselves be volatile (you cannot place a persistent key under a non-persistent one).

Watch the word, because it collides with a different idea. "Volatile" describes **storage persistence** — does this key survive a reboot. It says nothing about **how quickly a configuration change takes effect** — whether editing a setting applies live, on service restart, or only on reboot. That second property belongs to each individual setting and is recorded in `regman` as its `applies` field. A value can live in a perfectly persistent (non-volatile) key and still only take effect on reboot. Keep the two apart.

## Where to go next

If you want the idea that the registry stores values it does not understand — what `regman` documents, what "reject-or-keep" means, and why the value the registry shows can differ from the value a subsystem is using — read [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md).

If you want the layered truth beneath the single value you read — precedence, the base layer, and automatic revert — read [Layers](/peios/using-peios/registry-layers/layers.md).

If you want the security model — why every key carries a security descriptor and what the registry-specific access rights are — read [Access control on keys](/peios/using-peios/registry-security/access-control.md).

---

# Configuration, not storage

_Peios / Using Peios / Concepts_

> The registry stores values it does not understand — meaning lives in the owning subsystem and in regman, and a stored value can differ from the one in use.

A registry value is a type tag and a pile of bytes. The registry stores it, returns it on request, and protects it with a [security descriptor](/peios/using-peios/registry-security/access-control.md) — but it never asks what the value *means*. As [Keys, values, and types](/peios/using-peios/registry-concepts/keys-values-and-types.md) put it, the registry is typed but opaque: it knows the tag, not the meaning. This page is about what follows from that, because it is the single most counterintuitive thing about the registry and the thing that most often trips people up.

## Configuration and meaning, in one sentence

**The registry holds configuration; it does not understand it. Meaning lives in the subsystem that reads the value and in `regman`, the manual that documents it — never in the registry itself.**

Every config system you have used before bundles storage and meaning together: the file holds the setting *and* the program that parses the file knows what the setting means, in one place. The registry splits them on purpose. Storage is the registry's job. Meaning lives in two other places.

## Where meaning lives

**In the subsystem that owns the value.** The code that reads `Machine\System\KMES\BufferCapacity` is the thing that knows it must be a power of two, knows the compiled-in default, and knows what to do when it changes. That knowledge is in KMES, not in the store. Ask the registry "is this a sensible buffer capacity?" and it has no answer — it was never told what the value is for.

**In [`regman`](/peios/using-peios/registry-administration/regman.md), for a human.** `regman` is the registry's manual — the `man` of the registry. Give it a path and it tells you what a key or value actually does:

```
$ regman Machine\System\KMES BufferCapacity

  Type     REG_QWORD
  Default  4194304  (4 MB)
  Valid    65536–268435456 bytes (64 KB–256 MB), power of two
  Applies  live — ring-buffer swap

Per-CPU ring buffer capacity, in bytes. Must be a power of two; values
that are not are treated as invalid and ignored.
```

`regman` is shipped documentation that lives *beside* the registry, not inside it — because the registry cannot describe itself, the manual has to sit next to it. This is the backbone of configuring a Peios system: before you touch a knob, `regman` is how you find out what it is, what values are legal, and what changing it will cost you (the `Applies` line — live, on restart, or on reboot). `regman` reads its own shipped docs; it does not read the live registry, so it tells you what a setting *is*, not what it is currently set to.

## Reject-or-keep

Because the store never validates, validation happens where the value is *read*. This produces the rule that surprises people:

**A write the registry accepts is just stored bytes. The subsystem that owns the value validates it on read — and a value it judges invalid is ignored, not applied.** The subsystem keeps its last known-good value. It never clamps the bad value to the nearest legal one, and never silently corrects it.

```mermaid
flowchart LR
    A["Admin writes a value"] --> B["Registry stores the bytes (always succeeds)"]
    B --> C["Owning subsystem reads it"]
    C --> D{"Valid?"}
    D -->|yes| E["Apply the new value"]
    D -->|no| F["Keep last known-good value + log the rejection"]
```

KMES is the worked example. Write a `BufferCapacity` that is not a power of two and the write *succeeds* — the registry has no opinion about powers of two. When KMES reads it, it rejects the value, keeps the capacity it was already using, and emits an event naming the key, the value it rejected, and the value it is still running on.

## Written versus used

That leaves two sources of truth that can legitimately disagree:

- **The registry shows what was *written*.** Read the key back and you see the value the admin set — including a rejected one, sitting there looking authoritative.
- **The log shows what is actually *in use*.** When a subsystem rejects a value and keeps its previous one, it says so in the event log. That record, not the registry read, is the truth about what the system is running on.

So "what is this subsystem actually configured to right now?" is not always answered by reading the registry. If a change does not seem to have taken effect, the registry will happily show you the value you wrote; the [audit and event log](/peios/security-fundamentals/auditing/overview.md) is where you find out it was refused and why. This is why observability matters here: the registry is the *intent*, the log is the *reality*, and they are allowed to differ.

## The registry does this to itself

The cleanest demonstration is the registry subsystem configuring itself. It reads its own tuning parameters from `Machine\System\Registry\` and applies exactly this discipline to them: a valid value is hot-swapped into effect; an invalid one (out of range, wrong type) is ignored, the previous known-good value is retained, and an audit event is emitted naming the key, the rejected value, and the value still in force. Values are never clamped. The subsystem that owns the entire registry treats its *own* configuration as "stored bytes I must validate before I trust" — see [How the registry boots and configures itself](/peios/using-peios/registry-administration/bootstrap-and-self-configuration.md).

## There is no invalid registry

Put plainly: a value is never "invalid" at the registry level, because the store has no standard to judge it against. **Validity is a verdict, and the reader makes it.** The same bytes could be valid to one subsystem and meaningless to another; the registry holds them either way.

So "is this configuration valid?" is not a question you ask the registry. You ask the subsystem that owns the value — or you read the log to see what verdict it already reached.

## Why it is built this way

Briefly, because it is worth knowing the trade rather than dwelling on it: keeping the store meaning-free keeps it small and uniform, lets it hold a value for a subsystem that is not running yet or a key nobody has read, and lets configuration be delivered from outside — a domain Group Policy, say — without the kernel needing a built-in schema for every subsystem on the machine. The price is that the store cannot tell you whether a value is sensible. That price is paid by `regman` (which documents what *should* be there) and the logs (which record what the system actually did).

## Where to go next

If you want to see how a subsystem *notices* a configuration change so it can re-validate and re-apply, read [Watching for changes](/peios/using-peios/registry-concepts/watches.md).

If you want the registry's own bootstrap and self-configuration story — the purest example of reject-or-keep — read [How the registry boots and configures itself](/peios/using-peios/registry-administration/bootstrap-and-self-configuration.md).

If you are ready for the layered model beneath the single value you read, read [Layers](/peios/using-peios/registry-layers/layers.md).

---

# Watching for changes

_Peios / Using Peios / Concepts_

> A persistent watch on a key or subtree reports changes to effective state — best-effort delivery, so a watcher must be ready to re-read.

Configuration changes while the system is running — an administrator edits a value, a role is installed, a policy is applied. A subsystem could poll the registry to notice, but Peios does not make it: the registry can **tell** a process when something it cares about changes. This is what lets services react to configuration instead of repeatedly re-reading it, and it is how the registry behaves as a live configuration backbone rather than a passive store.

## Watching, in one sentence

**A process arms a persistent watch on a key — optionally covering its whole subtree — and is notified whenever the effective state there changes, so it can react to configuration instead of polling for it.**

## Persistent, not single-shot

A watch is armed on an open key handle. Once armed, it stays armed until the handle is closed: events keep flowing, with no need to re-arm after each one. This matters because re-arming would open a race — a change slipping through in the gap between one notification and the next registration. There is no such gap here. The handle is also pollable: a process waits on it the same way it waits on any other file descriptor, and reads structured change records off it when they arrive.

A watch can cover just the one key, or the key **and its descendants** (a subtree watch), so a service can watch an entire configuration area with a single registration.

## What a watch reports

Events describe what changed, by kind:

| Event | Meaning |
|---|---|
| Value set | The effective value at a name changed or appeared. |
| Value deleted | The effective value at a name went away. |
| Subkey created / deleted | A child key became visible / became invisible. |
| SD changed | The watched key's security descriptor was modified. |
| Key deleted | The watched key itself is no longer reachable by path. |
| Overflow | Events were dropped — re-read to recover (below). |

## Watchers see effective state, not layers

Here is the important tie-back to [Layers](/peios/using-peios/registry-layers/layers.md). A watcher is told about changes to the **effective** value — the winner of the contest — and nothing about the machinery underneath. Delete a layer and cause a value to revert, and the watcher sees a plain "value set" event for its new effective value. Apply a higher-precedence policy that overrides a local setting, and the watcher sees the value change. It never sees "a layer was added" or "a write lost a contest" — only that the answer it would get from a read is now different.

This is exactly right: the layering is an implementation of *how* the effective value is chosen, and a watcher only cares *that* it changed. The curtain stays down; the watcher watches the front of it.

## Committed state only

Watch events fire when a change **commits**, never mid-flight. A [transaction](/peios/using-peios/registry-administration/lcs-and-sources.md) that writes many values produces its events as one batch at commit time; a watcher never observes a half-applied transaction, and an aborted one produces no events at all. What a watcher sees is always a state the system actually reached.

## Best-effort: be ready to re-read

A watch is a **change notification, not a guaranteed-complete journal.** Events are queued for the watcher, and the queue is bounded. If a watcher falls behind — events arriving faster than it reads them — the queue overflows: the oldest events are dropped and a single **overflow** marker is delivered in their place. The contract on overflow is simple and firm: *re-read the watched key (and subtree) to recover the current state*, then carry on with subsequent events.

So a correct watcher is written to do two things — apply individual events when it is keeping up, and fall back to a full re-read when it is told it fell behind. It never assumes the event stream is a complete edit history; it treats it as "something changed, here is a hint, and if the hint says you missed some, go look." The same overflow-and-re-read recovery covers the bigger disruptions too — a large layer operation, or the backing store restarting — where computing an exact per-change list is not worthwhile.

## The reaction loop

Put watches together with [reject-or-keep](/peios/using-peios/registry-concepts/configuration-and-meaning.md) and you get the pattern that runs throughout Peios. A subsystem watches its own configuration subtree; when a value changes, the watch fires; the subsystem re-reads, re-validates, and either applies the new value or keeps its last known-good one and logs the rejection. KMES does this for its tuning; peinit does it for service definitions; the registry itself does it for its own parameters (see [How the registry boots and configures itself](/peios/using-peios/registry-administration/bootstrap-and-self-configuration.md)). Configuration is something you *react to*, and watches are the mechanism.

## One subtlety: a watch follows the object

A watch is bound to the specific key object you opened, not to the path string. If layers later cause a *different* key to appear at the same path, your watch stays with the original object — it does not jump to the newcomer. If the original key is removed, you get a "key deleted" event; to watch whatever now lives at that path, you reopen the path and arm a fresh watch. This follows from keys having an identity of their own, distinct from the name that currently points at them.

## Where to go next

If you want the registry's own use of watches — how it picks up changes to its own configuration without restarting — read [How the registry boots and configures itself](/peios/using-peios/registry-administration/bootstrap-and-self-configuration.md).

If you want the validation half of the reaction loop — why a changed value might be read and then refused — read [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md).

If you want what it takes to be allowed to watch a key, read [Access control on keys](/peios/using-peios/registry-security/access-control.md).

---

# Layers

_Peios / Using Peios / Layers_

> A layer is a label on a write, not a position in a stack — most layers share one precedence, and the winner is chosen per value by recency.

So far the registry has been presented as if each value had a single stored entry: you write it, you read it back. That was the *effective view* — true as far as it goes, and the right way to learn everything up to here. This page replaces it with what is actually underneath, because the registry is a **layered** store, and the layering is both the reason the subsystem exists and the part most likely to be misunderstood.

The misunderstanding is worth naming up front. It is natural to picture layers as a clean stack — sheets of glass, one above another, the top sheet's value showing through. That picture is wrong in the case that matters most, and unlearning it is most of this page.

## Layers, in one sentence

**A layer is a label stamped on a write and a handle for removing a group of writes together — not a position in a stack. The registry orders individual writes, not layers, and it chooses a winner separately for every single value.**

Hold onto both halves. First: the thing that has a position is the *write*, not the layer. Second: resolution is *per value* — the contest is run again, from scratch, for every value name.

## Every write is kept

When you write a value, the registry does not overwrite anything in place. It records your write, tagged with the layer you wrote it in. If a different layer writes the same value, that write is stored *alongside* yours, with its own tag. So a single value name can hold several stored writes at once — one for each layer that has ever written it. (Within one layer there is only ever one: writing a value again in the same layer replaces that layer's entry. One layer, one opinion per value.)

A read, though, returns exactly one value — the **effective value**. Several stored writes, one answer: there is a contest, and the rest of this page is its rules.

## How the winner is chosen

Each stored write carries two ordering keys:

- **precedence** — a number it inherits from its layer. Higher wins.
- **recency** — its place in a single, global, monotonic write-order counter. Later wins.

For one value, the registry gathers every active write to it and picks the winner by those keys **in order**: highest precedence first; among writes that tie on precedence, the most recent. That is the whole rule.

```mermaid
flowchart TD
    W1["write · layer base · seq 300"] --> R{"tie on precedence — most recent wins"}
    W2["write · layer role-jellyfin · seq 50"] --> R
    R --> E["effective value = base (seq 300)"]
```

The subtlety is entirely in how those two keys play out in practice — because in practice, one of them almost never varies.

## Precedence is the exception; recency is the rule

Here is the fact that breaks the stack picture: **almost every layer has the same precedence.** The base layer is precedence 0. Role layers are precedence 0. Only a few things — chiefly domain policy — ever sit higher. So for the overwhelming majority of contests, precedence is a tie, and the winner is decided purely by **recency, value by value.**

That has a consequence the stack picture cannot express. Two layers at the same precedence have *no fixed pecking order between them*. For one value, layer A might win because it wrote that value most recently; for the value right beside it, layer B wins because it wrote *that* one last. Same two layers, same precedence, different winners — at the same instant.

A worked example. An administrator has made some manual edits (which land in the base layer) and has installed a role (`role-jellyfin`), both at precedence 0:

| Value | base wrote (seq) | role-jellyfin wrote (seq) | Effective value | Why |
|---|---|---|---|---|
| `MaxEventSize` | 300 | 50 | **base** | tie on precedence → most recent write wins |
| `BufferCapacity` | — | 250 | **role-jellyfin** | base never wrote it |
| `MaxNestingDepth` | 100 | 280 | **role-jellyfin** | role wrote it more recently |

There is no "top layer" here to point at. `base` owns one value, `role-jellyfin` owns two, and which is which is decided one value at a time by who wrote last. This is what "interwoven, not stacked" means, and it is the normal case, not an edge case.

So the stack-of-glass-sheets picture is only ever right when precedences genuinely differ — which is the minority. Drop it as your default image.

## A picture that fits

If you want one image to keep, use a **shared document where, for each cell, the last edit wins** — many editors, no locking, the most recent edit showing. That is the same-precedence case exactly: per-cell (per-value) last-write-wins, with no editor inherently above another. Precedence, when it appears, is an **administrator who can lock a cell**: once locked, that cell holds the locked value no matter who edits afterwards. Every metaphor leaks, but this one leaks in the right places — it foregrounds that resolution is per-cell and recency-driven, and casts precedence as the rare override it actually is.

## When precedence really does differ

When two writes have *different* precedence, recency stops mattering between them: **higher precedence always wins, even over a more recent lower-precedence write.** A value set by a precedence-1 layer long ago still beats one written to a precedence-0 layer a moment ago.

This is precisely what makes precedence worth having. Recency is fine for cooperating local configuration, but it cannot express "this setting must win even if someone writes it again later" — and that is exactly what policy needs. A domain **Group Policy** is delivered as a higher-precedence layer for this reason: a local administrator cannot defeat it by re-writing the value, because their write lands at precedence 0 and loses to the higher tier regardless of how recent it is. Precedence is the mechanism behind "you cannot override this locally". (Creating a layer that outranks others is itself a privileged action, so the tiering cannot be forged from below — more on that in [Access control](/peios/using-peios/registry-security/access-control.md).)

## "Most recent" is write order, not the clock

One precision worth stating, because it is what makes the model trustworthy: recency is a **monotonic write-order counter**, not a wall-clock timestamp. Every write is handed the next number from a single counter that only ever increases. The registry does record a wall-clock "last write time" on each key, but only as human-facing metadata — it is never used to resolve a contest. So "most recent" means "later in the actual order of writes", which cannot be moved backwards or spoofed by a clock change.

## It is not only values

Everything above is framed around values, but the **same contest decides whether a key exists at a path.** Each layer can make its own claim about a name — "a key lives here" — and the winner is chosen by the same precedence-then-recency rule. A layer can even claim that *nothing* lives there, masking a key another layer provides. That is how a layer adds, replaces, or hides a key, and it resolves exactly as a value does. The markers that express absence — tombstones for values, hidden entries for keys — are the subject of the next page.

## Where to go next

If you want the payoff — why all of this exists — read [What layers are for](/peios/using-peios/registry-layers/what-layers-are-for.md): the base layer, tombstones, the automatic-revert property, and how roles and Group Policy are built on it.

If you want the one thing layers do *not* revert — security — read [Access control on keys](/peios/using-peios/registry-security/access-control.md). A security change made while a layer existed is *not* undone when the layer is removed, and the reason is worth understanding.

If you want to know how a watcher sees a layer change — it observes effective-state changes, with the layering made invisible — read [Watching for changes](/peios/using-peios/registry-concepts/watches.md).

---

# What layers are for

_Peios / Using Peios / Layers_

> Layering makes configuration removable — delete a layer and its writes vanish, the next-best write resurfaces, and nothing is left behind.

[Layers](/peios/using-peios/registry-layers/layers.md) explained how the registry resolves competing writes. This page is the reason it bothers. The layered model buys one thing above all: **configuration you can add and remove as a unit, with revert that is automatic and leaves nothing behind.**

## The payoff, in one sentence

**Because every write is tagged with a layer and nothing is overwritten in place, deleting a layer makes its writes disappear and the next-best write for each value resurface on its own — so installing a bundle of configuration and removing it again are just creating and deleting a layer.**

Everything below is a consequence of that.

## The base layer

Most of the time you are not thinking about layers at all, and the **base layer** is why. It is the default layer — precedence 0 — and it is where writes go when you do not ask for anything else. Manual administrative edits land here; so do the system's own defaults. It always exists, and it cannot be deleted or disabled; it is the floor the rest of the model stands on.

When earlier pages showed you "write a value, read it back", that was the base layer doing its job. You can work with the registry for a long time and only ever touch base.

## Saying "no value", not just "another value"

A plain write competes to *be* the value. But configuration sometimes needs to say something a plain write cannot: *this value must not be set at all.* Overriding `MaxEventSize` with a different number is easy; asserting that `MaxEventSize` should be absent is a different statement.

A layer makes it with a **tombstone** — a write whose meaning is "no value here". It enters the same per-value contest as any other write ([Layers](/peios/using-peios/registry-layers/layers.md)); if it wins, a read of that value returns "not found" rather than falling through to some other layer's write. Like any write, it belongs to a layer — so when that layer is removed, the tombstone goes with it and whatever it was suppressing comes back.

A **blanket tombstone** is the same idea applied to a whole key at once: a marker that enters the contest as a "no value" candidate for *every* value name on the key. A layer can set a blanket tombstone and then write the specific values it does want — the effect is "clear everything here, then set these". It is how a layer declares the complete contents of a key instead of merging into whatever was already there.

The key-level equivalent is **hiding**: a layer can claim that no key exists at a path, masking one that an older or lower-precedence write provides. Remove the layer and the key reappears.

In every case the marker is just another write — owned by a layer, removed with the layer.

## Automatic revert

This is the property the whole design exists to deliver. Because layers are removable and nothing is ever destroyed in place, **deleting a layer cleanly undoes everything it did** — with no undo script, no bookkeeping, no leftover state:

- Its writes vanish from every contest they were in.
- Its tombstones and hidden-key markers lift.
- For each affected value or key, the registry simply re-runs the contest without the deleted layer's writes, and the next-best survivor becomes effective.

There is no separate "revert" operation. Revert is what *naturally happens* when a layer's writes stop existing, because the effective value was always just the winner of a live contest.

```mermaid
flowchart LR
    A["role-jellyfin present: MaxNestingDepth = role's value"] -->|delete the layer| B["role's write gone: MaxNestingDepth reverts to the base value underneath"]
```

There is, in particular, no **tattooing** — the failure mode where removing configuration leaves its changes burned in. In a system that overwrites in place, uninstalling something means trusting that its installer recorded the previous values and restores them correctly. Here there is nothing to restore: the previous value never left.

## Roles

A **role** is a bundle of configuration deployed as a layer. Installing a role creates a layer and writes the role's keys and values into it (atomically, as one transaction, so the role never appears half-installed). Uninstalling a role deletes the layer — and by automatic revert, every value it set and every key it added simply falls away, and whatever the system looked like before resurfaces on its own. Clean uninstall is not something a role's authors have to implement carefully; it is a property of the layer.

## Group Policy

Domain **Group Policy** is configuration delivered from outside the machine, and it rides on layers too — but at a *higher precedence* than local layers. That choice is deliberate, and [Layers](/peios/using-peios/registry-layers/layers.md) explained why: a higher-precedence write wins regardless of recency, so a domain setting beats local configuration even if a local administrator writes the value again afterwards. Applying a policy adds the layer; lifting it deletes the layer, and the local configuration it had been overriding resurfaces automatically — the same revert, one tier up. (High-precedence layers are privileged to create, so policy cannot be forged by an unprivileged process; see [Access control](/peios/using-peios/registry-security/access-control.md).)

## What layers are not

- **Not a transaction.** Layers decide *which* write wins and let you remove a group of writes together; atomicity — making several writes commit all-or-nothing — is a separate mechanism. A role install uses both: a transaction to apply the writes atomically, a layer to make them removable.
- **Not access control.** A layer does not decide who may read or change a value; the [security descriptor](/peios/using-peios/registry-security/access-control.md) on each key does. And the two do not mix: a security change made while a layer existed is **not** reverted when the layer is deleted. Security is operational state, not configuration overlay — [Access control on keys](/peios/using-peios/registry-security/access-control.md) is where that distinction lives.
- **Not a browsable history.** Layers are not a version-control timeline you can scroll through. You see the effective view — the current winners — not a log of every write that ever competed.

## Where to go next

If you want what "deleting a key" really does once names are layered — and why there is no recursive delete — read [Deleting keys and values](/peios/using-peios/registry-layers/deleting-keys-and-values.md).

If you want the security model and its sharp interaction with layers — why deleting a layer reverts its values but *not* a security change made under it — read [Access control on keys](/peios/using-peios/registry-security/access-control.md).

If you want to see how a watcher experiences a layer being added or removed — it sees the effective values change, with the layer machinery invisible — read [Watching for changes](/peios/using-peios/registry-concepts/watches.md).

For the sandboxing case — per-thread *private* layers that only one caller sees — read [Private hives and layers](/peios/using-peios/registry-advanced/private-hives-and-layers.md).

---

# Deleting keys and values

_Peios / Using Peios / Layers_

> Deleting a key withdraws one layer's claim to a name — the key stays if another layer still claims it, and there is no recursive delete.

Deletion is one more thing the [layered model](/peios/using-peios/registry-layers/layers.md) quietly reshapes. "Delete this key" sounds absolute, but in a store where a name is the winner of a per-layer contest, removing a key really means withdrawing *one layer's claim* to that name. Several behaviours follow that are worth knowing before you delete anything.

## Deletion, in one sentence

**Deleting a key withdraws one layer's claim to a name; the key disappears only if no layer still claims it — and even then, anything holding it open keeps working until it lets go.**

## Deleting withdraws a claim

When you delete a key, you remove its name in a particular layer — the [base layer](/peios/using-peios/registry-layers/what-layers-are-for.md) unless you say otherwise. Nothing about the key is special-cased; its claim in that layer is simply withdrawn, and it drops out of the [contest](/peios/using-peios/registry-layers/layers.md) for that name.

If another layer still names the key, it stays visible through that layer. So deleting a key that a role also provides removes only *your* claim — the role's key remains until the role does. To remove a key everywhere, every layer's claim has to go. For anything delivered by a role or a policy, that means removing the *layer* (which [reverts cleanly](/peios/using-peios/registry-layers/what-layers-are-for.md)) rather than deleting the key — deleting it in the base layer would not touch the role's claim anyway.

(The hive roots themselves — `Machine\`, `Users\<SID>\` — cannot be deleted or hidden. They are the anchors the namespace hangs from.)

## There is no recursive delete

You cannot delete a key that still has visible child keys; the deletion is refused. There is no "delete this whole subtree" primitive. Removing a populated subtree is a deliberate walk from the leaves upward, performed by whatever tool you are using — not a single sweep in the kernel.

This is a safety property, not a limitation to work around. A mistaken delete cannot take a populated subtree down with it; you have to mean it, key by key.

## Deleting out from under an open handle

A key can be deleted while a process still has it open, and the registry handles that the same way Linux handles deleting an open file (the *unlink* model). The open handle keeps working — reads, writes, and watches all continue against the now-unnamed key — and the key is only truly discarded once the last handle closes. Meanwhile, new attempts to open it *by path* fail at once: the name is gone, even though the object lingers for whoever still holds it.

So "deleted" means "no longer reachable by name", not "destroyed this instant". A service that had the key open does not break mid-operation; it holds the last reference until it closes, and only then does the key go away.

## Deleting values

A value works the same way one level down. Deleting a value withdraws a layer's write for that value name. If a lower-precedence or older layer also wrote that value, its write resurfaces as the new effective value — the ordinary [revert](/peios/using-peios/registry-layers/what-layers-are-for.md). Remove a value's only write and the value simply becomes absent.

## Permission

Deleting (or hiding) a key requires delete permission on it — see [Access control on keys](/peios/using-peios/registry-security/access-control.md). As everywhere in the registry, the check is against the key you are deleting, decided by its security descriptor, with no check on the keys above it.

## Where to go next

For the contest that "withdrawing a claim" feeds back into, read [Layers](/peios/using-peios/registry-layers/layers.md).

For removing configuration in bulk — deleting a layer, which is what you do instead of deleting a role's or policy's keys — read [What layers are for](/peios/using-peios/registry-layers/what-layers-are-for.md).

---

# Access control on keys

_Peios / Using Peios / Security_

> Every key is a secured object under the same AccessCheck as everything else — checked once at open, per key not per value, with no traversal check.

Every registry key is a secured object. It carries a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) — owner, DACL, and optionally a SACL — exactly like a file, a process, or a token. And the registry does not invent its own access logic: opening a key runs the same [AccessCheck](/peios/security-fundamentals/access-decisions/overview.md) pipeline that governs every other protected object in Peios, against the same tokens and the same SD format. If you understand access control for files, you already understand most of it for the registry.

## Access control on keys, in one sentence

**Every key carries a security descriptor, and access to it is decided by the same AccessCheck that governs everything else — evaluated once when you open the key, then cached on the handle for the life of that handle.**

## Security is per key, not per value

A security descriptor lives on the **key**. Values do not have their own — they inherit their key's access control. "Who can read this value, who can change it" is answered by the SD on the key that contains it, and there is no finer-grained permission than that. If two values need different access rules, they belong in different keys.

## The handle model

Access is checked at **open**, not on every operation. When you open a key for some set of rights, AccessCheck runs once; if it grants them, you get a key handle (a file descriptor) with a **granted access mask** baked in. Every later operation through that handle is a cheap bitmask check against the cached mask — *is this operation's required right in what I was granted?* — not a fresh AccessCheck.

The consequence is the same **check-at-open** rule that [files](/peios/security-fundamentals/file-access/the-handle-model.md) and [central access policies](/peios/security-fundamentals/central-access-policies/overview.md) follow: changing a key's SD affects *future* opens, not handles that are already open. An administrator who tightens a key's DACL does not retract access from a service that already has the key open — the recourse is to make the service reopen (typically by restarting it). The handle is a snapshot of the decision made at open time.

## The registry-specific rights

A key's DACL is written in terms of rights specific to keys:

| Right | Gates |
|---|---|
| `KEY_QUERY_VALUE` | Reading values. |
| `KEY_SET_VALUE` | Writing and deleting values. |
| `KEY_CREATE_SUB_KEY` | Creating child keys. |
| `KEY_ENUMERATE_SUB_KEYS` | Listing child keys. |
| `KEY_NOTIFY` | Arming a [watch](/peios/using-peios/registry-concepts/watches.md). |
| `KEY_CREATE_LINK` | Creating a [link key](/peios/using-peios/registry-advanced/registry-links.md) (privileged). |
| `DELETE` | Deleting the key, or hiding it in a layer. |
| `READ_CONTROL` | Reading the SD and key metadata. |
| `WRITE_DAC` / `WRITE_OWNER` | Changing the DACL / the owner. |
| `ACCESS_SYSTEM_SECURITY` | Reading or changing the SACL (audit policy). |

The usual convenience bundles apply — `KEY_READ` (query, enumerate, notify, read the SD), `KEY_WRITE` (set values, create subkeys), and `KEY_ALL_ACCESS`.

Ordinary access — reading a value, writing one, creating or listing subkeys — is decided entirely by the key's SD through AccessCheck. **No privilege grants ordinary registry access.** A few privileges appear only where a write means more than storage: establishing a layer that *outranks* others (policy — see [Layers](/peios/using-peios/registry-layers/layers.md)), creating a link, or bulk [backup and restore](/peios/using-peios/registry-administration/lcs-and-sources.md). Those are special operations, covered where they arise; everyday access is the SD's job alone.

## No traversal check

One surprise sets the registry apart from a filesystem: **only the SD on the key you open is checked.** The keys above it on the path are not. A process can open `Machine\System\Services\Jellyfin` with no access whatsoever to `Machine\System\Services` or `Machine\System`. There is no "execute/traverse" right that you must hold on every ancestor the way a filesystem demands. Access is decided at the destination, full stop.

This matters when you reason about exposure: locking down a parent key does **not** lock down what is beneath it. If a subtree must be protected, the protection has to be on the keys that hold the data, not merely on a key somewhere above them.

## Where a key's SD comes from

A key's SD is computed **once, at creation**, by inheriting from its parent — the same eager, static [inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) files use. It is then a complete value stored on the key; the parent is not consulted again at access time. Changing a parent's SD does **not** ripple down to children that already exist — re-applying inheritance to an existing subtree is a deliberate administrative walk, not something that happens on its own.

The chain has to start somewhere, and it starts at the hive roots, whose SDs are the seeds for everything below:

| Hive root | Default access |
|---|---|
| `Machine\` | SYSTEM and Administrators: full control. Authenticated Users: read. (All inheritable.) |
| `Users\<SID>\` | That user, SYSTEM, and Administrators: full control. |

Subsystems that need something tighter than "Authenticated Users can read" set an explicit SD on their own subtree root at creation, overriding the inherited default for everything beneath it.

## Watching requires permission to read

Arming a [watch](/peios/using-peios/registry-concepts/watches.md) needs `KEY_NOTIFY`, so a process cannot monitor a key it could not otherwise observe. One deliberate asymmetry: a *subtree* watcher learns that a descendant key was created or deleted — structural facts — without holding any access to that descendant. It learns that something appeared; it must still open it (and pass AccessCheck) to read what is inside. Structure visibility is intentionally weaker than content visibility.

## The sharp edge: security is not layered

This is where the registry's two big ideas meet, and the result surprises people. [Layers](/peios/using-peios/registry-layers/what-layers-are-for.md) revert cleanly — delete a layer and its values fall away. **A security change does not.**

A key's SD is a direct property of the key object. It is *not* tagged with a layer and it is *not* a write in the per-value contest. So when you change a key's DACL — tightening access, say — you are mutating the key itself, permanently. If a role layer existed at the time and is later uninstalled, the values it set revert, but the security change you made stays exactly where it is.

The reasoning is deliberate: security is **operational state, not configuration overlay.** Imagine the alternative — an administrator locks down a sensitive key while some unrelated role happens to be installed, and then removing that role silently reopens the key. Configuration is the kind of thing you want to revert in bundles; an access decision is not. So the registry keeps them on different tracks: values and key existence are layered and revertible; ownership and permissions are mutations on the object that outlive any layer.

The short version: **layers revert what the system is configured to do; they never revert who is allowed to do it.**

## Where to go next

If you want to see how a process *reacts* to a change instead of polling for it — and how watch events report effective-state changes with the layering invisible — read [Watching for changes](/peios/using-peios/registry-concepts/watches.md).

If you want the kernel/store split underneath all of this — who actually holds the SDs, and the trust boundary that follows — read [LCS and sources](/peios/using-peios/registry-administration/lcs-and-sources.md).

For the broader access model these rights plug into — the AccessCheck pipeline itself — read [Access decisions](/peios/security-fundamentals/access-decisions/overview.md).

---

# Default security descriptors

_Peios / Using Peios / Security_

> Where a component keeps the security descriptors it stamps at runtime — the SdDefaults convention — and the reject-or-keep rule that guards it.

Some software stamps [security descriptors](/peios/security-fundamentals/security-descriptors/overview.md) onto objects it creates at runtime — a freshly mounted filesystem root, a state file, a spool directory. Each of those descriptors is a policy decision: who can enumerate this directory, who can read this file, what everything created beneath this root will inherit. Decisions like that are configuration, and in Peios configuration has one home. The **SdDefaults** convention is the standard place a component keeps these descriptors, so that an operator can inspect them, change them, and trust that every component publishes them the same way.

## Default security descriptors, in one sentence

**A component keeps each security descriptor it stamps at runtime as a named SDDL value under `Machine\Software\<Software>\SdDefaults\`, with a compiled-in copy as the fallback — a value that is present and valid overrides the compiled default; a value that is invalid is ignored, loudly.**

## The convention

```
Machine\Software\<Software>\SdDefaults\<SD Name>
```

- `Machine\Software\<Software>` is the component's own configuration key — the same key the rest of its settings live under.
- `SdDefaults` is the literal subkey name.
- `<SD Name>` names the object the descriptor protects: `SpoolDirectory`, `StateFile`, `Run`. One value per descriptor, stored as a string containing SDDL.

A fictional spooler that creates its spool directory at startup would publish:

```
Machine\Software\ExampleSpooler\SdDefaults\SpoolDirectory
    REG_SZ  "O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)"
```

The defaults are **per component, deliberately**. A single shared tree of default descriptors would need every SD name to be unique across every application ever written — a namespacing promise nobody can keep. Scoping the names under the component that reads them dissolves the problem, and it keeps discovery unsurprising: a component's descriptors live where the rest of its configuration lives.

## Compiled default, registry override

The registry value is an override, never the only copy. Every component that follows the convention carries a compiled-in default for each named descriptor, and resolves the one to use at the point where it stamps it:

| State of `SdDefaults\<SD Name>` | Descriptor in force |
|---|---|
| Absent | The compiled-in default. This is the normal state — the key need not exist at all. |
| Present, valid SDDL | The registry value. |
| Present, invalid | The compiled-in default. The stored value is ignored, and an event records the key, the rejected value, and the descriptor actually in force. |

This is the registry's general [reject-or-keep](/peios/using-peios/registry-concepts/configuration-and-meaning.md) rule applied to descriptors, and here it is doing its most important work. A security descriptor that fails to parse is never repaired, never approximated, and never replaced with something broader: the descriptor in force is always one that was compiled in and reviewed. A typo in an SdDefaults value costs you your customisation, not your system.

## When a change takes effect

A default descriptor is read when the component stamps it — typically when the object is created. Two consequences follow:

- Changing a value affects objects stamped **after** the change. It does not rewrite objects that already exist.
- Where the stamped object is a directory whose descriptor carries inheritable ACEs, everything created beneath it derives its descriptor at creation time ([inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) is computed once, when the child is born). Changing the default later does not re-derive existing children.

So the honest answer to "when does my change apply?" varies by descriptor — next boot, next mount, next time the object is recreated — and the component's [`regman`](/peios/using-peios/registry-administration/regman.md) documentation is where that answer lives. Every `<SD Name>` a component publishes has a regman entry, and its `Applies` line states exactly this.

## The values are access policy — protect them

Whoever can write a component's SdDefaults values decides what access the component will grant on the objects it creates. These values are not settings *about* security; they **are** the security. A component's `SdDefaults` key therefore carries a tight security descriptor of its own — writable by Administrators and SYSTEM, no wider — which the registry enforces like [any other key](/peios/using-peios/registry-security/access-control.md). The arrangement is self-hosting: the store that holds the descriptors is protected by the same mechanism the descriptors configure.

## What the convention does not cover

**Bootstrap seeding.** The very first descriptors of a boot — the seed stamped onto a fresh root before any registry source is attached — are compiled into the tools that write them, and are not customisable through the registry. There is no registry to read at that point in boot; that is not a gap in the convention but the reason it has a floor.

**Kernel fallbacks.** The descriptors the kernel synthesises when a mount policy has no template, and the default DACL applied when a created object has no parent to inherit from, are fixed. They are the floor under a missing policy, not configuration.

**Files installed by packages.** A package payload entry gets its descriptor by inheritance from its destination directory, or from a declaration in the package manifest — see [PSPU §5.20](/peios/advanced-peios/pspu/package-format-and-repository-protocol/security-descriptor-overrides.md). SdDefaults governs what software stamps at runtime, not what the package manager installs.

## For component authors

If your component stamps descriptors at runtime, follow the convention:

1. Ship a compiled default for every named descriptor. The registry value is an override; your component must work, with reviewed policy, on a system where the `SdDefaults` key has never been created.
2. Name each value for the object it protects, not for its content — `SpoolDirectory`, not `SystemOnlyOici`.
3. Resolve with reject-or-keep. An unparseable value is ignored in favour of the compiled default, and the rejection is recorded in an event naming the key, the rejected value, and the descriptor in force. Never substitute anything broader than the compiled default.
4. Document every name in `regman`, including an `Applies` line that says when a change is picked up.

## Where to go next

For the doctrine behind reject-or-keep — why the registry stores values it does not validate, and why readers keep their last known-good — read [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md).

For what an SDDL string actually says — owner, DACL, ACEs, and the inheritance flags that make one descriptor govern a whole tree — start at [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md) and [Inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md).

For protecting the `SdDefaults` key itself, read [Access control on keys](/peios/using-peios/registry-security/access-control.md).

---

# How the registry boots and configures itself

_Peios / Using Peios / Administration_

> The registry runs on compiled-in defaults from the instant the kernel loads and hot-swaps to registry-backed configuration once its store is up.

The registry holds the configuration for everything on the system. Which raises an awkward question: what configures the *registry*? The honest answer is that it mostly configures itself — and doing that means breaking a circular dependency, because the registry's own settings live in the registry, and the store those settings live in is itself a service that has to start up. This page is how that knot is untied, and it doubles as the purest example of the [reject-or-keep](/peios/using-peios/registry-concepts/configuration-and-meaning.md) discipline.

## Bootstrap, in one sentence

**The registry subsystem is fully operational on compiled-in defaults from the instant the kernel loads, and hot-swaps to registry-backed configuration once its store comes up — it never enters a "waiting for configuration" state.**

## The circular dependencies

Three loops have to be broken at boot:

- The registry reads its own operational parameters (timeouts, size limits, and so on) from a key under the `Machine\` hive — but that hive is held by a store that has not started yet.
- Other early services want to read *their* configuration from the registry before that same store is up.
- On a brand-new install there is no data at all: the store's database is empty.

A configuration store that refused to function until it was configured would deadlock on the first of these. The registry is designed so that never happens.

## Compiled-in defaults

Every operational parameter the registry uses has a **compiled-in default** that produces correct behaviour. From the moment the kernel module loads, the registry runs on those defaults — it is immediately operational and never blocks waiting for configuration. The [base layer](/peios/using-peios/registry-layers/what-layers-are-for.md) likewise exists unconditionally, hardcoded, needing no persisted state. So before any store has registered, the machinery is already alive; it simply has no hives to serve yet.

## Hot-swap, not restart

When the store registers and the configuration keys become readable, the registry **reads, validates, and swaps its parameters in place** — no restart, no re-initialisation. Operations already in flight finish on the values they started with; new operations pick up the new values. The transition from "compiled-in defaults" to "registry-backed configuration" is seamless and is the *only* configuration transition there is.

```mermaid
flowchart LR
    A["Kernel loads — registry live on compiled-in defaults"] --> B["Store registers its hives"]
    B --> C["Read Machine\System\Registry\* parameters"]
    C --> D["Validate and hot-swap into effect"]
```

## It applies reject-or-keep to itself

This is the cleanest demonstration of the idea from [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md): the registry treats its *own* configuration as untrusted bytes it must validate. A parameter value that is valid is hot-swapped into effect. One that is invalid — out of range, wrong type — is **ignored**: the registry keeps the value it was already using and emits an audit event naming the key, the rejected value, and the value still in force. It never clamps the bad value to something legal. The subsystem that owns the entire registry does not trust even its own settings until it has checked them — and when stored and in-force disagree, the audit log is the truth.

## First boot, with no data

A fresh install has an empty store, and the sequence is built to cope:

1. The store — `registryd`, the base registry source [peinit](/peios/using-peios/services-and-jobs/overview.md) starts at early boot (see [LCS and sources](/peios/using-peios/registry-administration/lcs-and-sources.md)) — starts, finds its database empty, and creates the hive root keys with their default [security descriptors](/peios/using-peios/registry-security/access-control.md) — but no configuration beneath them.
2. The registry looks for its parameter keys, finds nothing, and keeps its compiled-in defaults. It is fully operational regardless.
3. The init system (not the registry's concern) restores a **seed** — a [backup](/peios/using-peios/registry-administration/backup-and-restore.md) of the system's initial configuration — that populates `Machine\` with the system's real configuration.
4. That write trips the registry's watch on its own configuration area; it re-reads, validates, and hot-swaps to the seeded values. Normal operation continues.

At no point is there a stall. Empty store, missing keys, seed arriving later — each is handled by "use the defaults until something better shows up", driven by the same [watch](/peios/using-peios/registry-concepts/watches.md) mechanism every other reactive consumer uses.

## The self-watch

The registry notices changes to its own configuration the same way any service notices changes — by watching the subtree — except the watcher is internal to the kernel rather than a userspace handle. It is the [reaction loop](/peios/using-peios/registry-concepts/watches.md) turned on the registry itself: a change to a parameter key fires the watch, the registry re-reads and re-validates, and applies or rejects. There is no polling and no special-case configuration path; self-configuration is just the registry being one more consumer of the registry.

## Where to go next

If you want the store itself — what it is, how the kernel and the userspace store divide the work, and the trust boundary between them — read [LCS and sources](/peios/using-peios/registry-administration/lcs-and-sources.md).

If you want the validation discipline this page leans on, read [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md).

If you want the change-notification mechanism behind the self-watch, read [Watching for changes](/peios/using-peios/registry-concepts/watches.md).

---

# Backup and restore

_Peios / Using Peios / Administration_

> Export a subtree to a stream and restore one back wholesale — how the system is seeded, recovered, and migrated. Restore is a replace, not a merge.

Beyond reading and writing individual values, the registry can move whole subtrees at once: **export** a key and everything beneath it to a stream, and **restore** such a stream back into the tree. This is the bulk-data path, and it is the mechanism behind things you have already met — [first-boot seeding](/peios/using-peios/registry-administration/bootstrap-and-self-configuration.md) — as well as the disaster-recovery and migration any real deployment needs.

## Backup and restore, in one sentence

**Backup streams a point-in-time copy of a subtree out; restore replaces a subtree wholesale from such a stream — both are privileged, whole-subtree operations, not value-by-value edits.**

## Backup is a point-in-time snapshot

A backup captures a key and its entire subtree as it stands at one instant — a consistent snapshot, unaffected by writes happening concurrently. The result is a self-contained stream you can send to a file, a pipe, or across a network.

It is **full-fidelity with respect to [layers](/peios/using-peios/registry-layers/layers.md)**: a backup records every layer's writes, not merely the effective values. Restore it and the layered structure comes back intact — the base values, the role layers, the policy overlays, all of them, resolving the way they did before. A backup is not a flattened picture of "what the values currently are"; it is the whole stack.

The stream is also self-verifying: it carries an integrity check, so a truncated or corrupted backup is caught when you try to restore it, rather than restored as garbage.

## Restore is replace, not merge

This is the rule to internalise. Restoring into a key **replaces** that key's contents and entire subtree: the existing descendants are removed and the backup's contents take their place. It is not a merge and it does not add to what is there — whatever was under the target key before is gone, supplanted by the stream.

The target key itself survives as the anchor — it keeps its identity and its place in the tree — and everything beneath it is rebuilt from the backup. The whole operation is atomic: it either completes and swaps the new subtree in, or fails and leaves the original untouched. There is no half-restored state to clean up.

## Both bypass per-key permissions — by design

Backup and restore do not consult the security descriptor on each key the way an ordinary open does. They are gated by **privilege** instead — a backup privilege to read the whole subtree, a restore privilege to write it. This is the same model as file backup: a backup operator reads files they were never granted access to, because reading-for-backup is the privilege, not per-file permission.

> [!WARNING]
> **The privilege to restore is, in effect, the privilege to rewrite security on everything in the target subtree.** A backup carries each key's security descriptor, and restore writes those descriptors back — so restoring lets the caller replace owners and permissions throughout the subtree, which is close to taking ownership of all of it. Granting restore privilege is therefore a serious act, not a routine one: read it as "may rewrite this subtree, security and all," because that is what it grants.

(Both operations are audited every time they run, regardless of a key's own audit settings — see the [auditing topic](/peios/security-fundamentals/auditing/overview.md).)

## What it is for

- **First-boot seeding.** A fresh machine's registry is empty; its initial configuration is delivered by *restoring* a seed. Same mechanism, described in [bootstrap](/peios/using-peios/registry-administration/bootstrap-and-self-configuration.md).
- **Disaster recovery.** Snapshot a subtree — or a whole hive — and restore it after a failure or a bad change.
- **Migration.** Move configuration between machines by backing up on one and restoring on another; the stream is portable.

## Where to go next

For where restore comes from at first boot, read [How the registry boots and configures itself](/peios/using-peios/registry-administration/bootstrap-and-self-configuration.md).

For who actually performs these operations — the kernel coordinating the store — read [LCS and sources](/peios/using-peios/registry-administration/lcs-and-sources.md).

For the privilege model they lean on, read [Access control on keys](/peios/using-peios/registry-security/access-control.md).

---

# LCS and sources

_Peios / Using Peios / Administration_

> Underneath, the registry is two components — LCS, the kernel authority, and userspace sources that only persist data, with loregd as the default.

Every page until now has treated the registry as a single thing, and that was the right altitude to learn the model. This page pulls it apart, because at the implementation level "the registry" is two cooperating components — and the division between them is clean, deliberate, and worth knowing once the concepts are solid.

## The split, in one sentence

**The registry is a kernel subsystem (LCS) that is the sole authority for the data model, access control, layer resolution, and change notification — plus one or more userspace *sources* that do nothing but persist and return data; the base source, `registryd`, is `loregd` by default and backs the `Machine` and `Users` hives.**

## Who does what

The dividing line is sharp, and it is the principle the whole design rests on: **the kernel decides meaning; sources only store.**

| The kernel subsystem (LCS) owns | A source owns |
|---|---|
| The data model — keys, values, types | Persisting entries to disk and returning them |
| Path resolution and routing | Nothing about *who* is asking |
| [Access control](/peios/using-peios/registry-security/access-control.md) — running AccessCheck | No security decisions at all |
| [Layer resolution](/peios/using-peios/registry-layers/layers.md) — choosing the effective value | No idea which write is effective |
| [Watch](/peios/using-peios/registry-concepts/watches.md) dispatch and transaction coordination | Executing storage operations it is told to |

A source never sees a caller's identity, never evaluates a security descriptor, and never resolves a contest between layers. It stores writes tagged with layer names and hands *all* of them back when asked; the kernel does the deciding. That is why earlier pages could say "the registry checks", "the registry resolves" — it is always the kernel doing it, never the store.

```mermaid
flowchart LR
    P["Any process"] -->|registry system calls| L["LCS (kernel): access control, layer resolution, watches"]
    L -->|private protocol| S["Source — e.g. loregd (userspace): storage only"]
    S --> DB["On-disk database"]
```

Userspace never talks to a source directly. Every registry operation goes through the kernel; the source talks only to the kernel, over a private protocol on a dedicated device. This is what lets the registry carry the same identity model, access control, and auditing as everything else — the kernel is unavoidably in the path.

## Why split it this way

Storage is a separable concern. Putting it in userspace means the backing store can be replaced, or different stores can back different hives, without changing the kernel — and the store can be developed, tested, and hardened as an ordinary (if privileged) service. The kernel keeps the parts that *must* be trusted and uniform — the data model and the security model — and delegates the part that is "just a database".

Hives are how the work is parcelled out: each hive is backed by exactly one source, and a source may back several hives. In practice the base source registers `Machine` and `Users`; other sources could register additional hives.

## The base source: registryd

The standard hives have to exist before anything can read configuration, so one source is always started at boot. **`registryd` is that base source — the registry store [peinit](/peios/using-peios/services-and-jobs/overview.md) starts in early boot**, and the first thing to register hives with LCS so the rest of startup has configuration to read. Its interface is deliberately minimal: one `HiveName=path` argument per hive, naming the hive and the on-disk database that backs it.

```
registryd Machine=/var/state/registry/machine.regdb Users=/var/state/registry/users.regdb
```

That is the whole of its configuration — `registryd` *is* the configuration store, so it takes what it needs from its arguments rather than from a config file of its own.

`registryd` is a **role**, not a fixed program. `loregd` — the Local Registry Daemon, which backs each hive with a SQLite database — is the **default** implementation, and the one running on a stock system. But the name is a swappable slot: another source can ship a `registryd` claim, and installing it puts that source in loregd's place with no change to anything that depends on the registry. Whatever holds the role starts the same way and answers to the same name. (Writing an alternative source is a developer task — the [Peios SDK](/peios/developing-for-peios/registry-sources/overview.md) covers the protocol one speaks to the kernel.)

## The trust boundary

It is worth being blunt about the security boundary, the same way the rest of these docs are. **A source is part of the trusted computing base.** The kernel runs AccessCheck, but it runs it against the security descriptor the *source* hands back — so a compromised source can return a permissive SD for a key and cause access that should have been denied, or fabricate the precedence of a layer and tilt every contest in the system. The kernel validates that a source's responses are *structurally* well-formed and rejects malformed data, but it cannot detect a well-formed lie.

This is not a gap so much as a fact about where the boundary is: trusting the store to tell the truth about what it stores is inherent in having a userspace store at all. The mitigations are operational — a source runs with tightly scoped privileges, is protected by the security descriptors on its own service definition, and is managed as a critical service (with process-integrity protection where available). The honest summary is: the store is small, privileged, and trusted, and protecting it is part of protecting the system.

## Two operations the kernel coordinates

Two registry capabilities are the kernel *orchestrating a source* rather than features of the data model: atomic [transactions](/peios/using-peios/registry-advanced/transactions.md), and [backup and restore](/peios/using-peios/registry-administration/backup-and-restore.md). In both, the source does the storage work — committing a batch atomically, or reading out and replacing a subtree — while the kernel coordinates it and enforces the rules around it. Each has its own page; the point here is only that the same division of labour holds: the kernel decides, the source stores.

## When a source goes away

Because the store is a separate process, it can crash or restart. When a source goes down its hives become unavailable: open key handles stay valid (they hold identity and a granted mask, neither of which needs the store), but operations that need to reach the store return an error until it is back. [Watches](/peios/using-peios/registry-concepts/watches.md) stay armed across the outage, and when the source re-registers, watchers receive an overflow so they re-read and resynchronise. The registry treats a store restart as a disruption to recover from, not a reason to lose state.

## Where to go next

You have now seen the whole model — data, meaning, layers, security, change notification, and the parts underneath.

For the tool you will reach for most when configuring a system — looking up what any key or value means — read [The registry manual (regman)](/peios/using-peios/registry-administration/regman.md).

Three advanced topics remain:

For grouping several writes into one all-or-nothing change — how a role installs without ever being half-applied — read [Transactions](/peios/using-peios/registry-advanced/transactions.md).

For complete per-caller isolation — hives and layers visible only to one sandboxed process — read [Private hives and layers](/peios/using-peios/registry-advanced/private-hives-and-layers.md).

For keys that point at other keys — the one place the registry follows a value — read [Registry links](/peios/using-peios/registry-advanced/registry-links.md).

---

# The registry manual (regman)

_Peios / Using Peios / Administration_

> regman documents what a registry key or value means — type, default, valid range, and when a change applies — from shipped docs, never the live registry.

[Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md) established that the registry holds values but not their meaning: it keeps a type tag and some bytes, and never knows that `BufferCapacity` must be a power of two or what it is for. That knowledge has to live *somewhere* a person can look it up. It lives in **`regman`**, the registry's manual.

`regman` is the `man` of the registry. Give it a path and it tells you what a key or value actually is. It is the everyday tool for configuring a Peios system — before you touch a knob, `regman` is how you find out what it does and what changing it will cost.

## regman, in one sentence

**`regman <path> [value]` documents what a registry key or value *means* — its type, default, valid values, when a change takes effect, and a prose description — drawn from shipped documentation, never from the live registry.**

That last clause is the one to hold onto; there is a section on it below.

## Looking up a value

Give `regman` a key path and a value name and it prints a **knob-card** — an identity line, an aligned block of facts, and a description:

```
$ regman Machine\System\KMES BufferCapacity

Machine\System\KMES BufferCapacity                    documented by kmes

  Type     REG_QWORD
  Default  4194304  (4 MB)
  Valid    65536–268435456 bytes (64 KB–256 MB), power of two
  Applies  live — ring-buffer swap

Per-CPU ring buffer capacity, in bytes. Must be a power of two; values
that are not are treated as invalid and ignored.
```

Every registry value is a *knob*, and every knob has the same few facts worth knowing, so the card is uniform rather than free-form:

| Field | Tells you |
|---|---|
| **Type** | The value's registry type (`REG_QWORD`, `REG_SZ`, …). |
| **Default** | The value used when nothing is set. |
| **Valid** | The range, set, or constraint a sensible value must satisfy. |
| **Applies** | When a change takes effect — `live`, on `restart`, or on `reboot`. |

The **Applies** field is the one operators reach for most: it is the difference between "edit it and you're done" and "edit it and schedule a reboot". Only the fields that make sense for a given item are shown, and a knob that is being retired carries a prominent deprecation notice at the top of its card.

## Looking up a key

Give `regman` just a key — no value — and it does double duty as an *index*: the key's own description, then a list of every value documented under it, one summary line each.

```
$ regman Machine\System\KMES

Machine\System\KMES                                   documented by kmes

The KMES event subsystem reads its tuning parameters from the values
under this key...

Values
  BufferCapacity         Per-CPU ring buffer capacity in bytes.
  MaxEventSize           Max total size of a userspace-emitted event.
  MaxNestingDepth        Max nesting depth for userspace event payloads.
  MaxEmitRatePerProcess  Max events per second a single process may emit.
```

So you can start at a subtree, see what is configurable there, and drill into any single value.

## Searching, when you do not know the path

If you don't know where a setting lives, `regman -k` searches names and summaries — the registry's `apropos`:

```
$ regman -k buffer
Machine\System\KMES BufferCapacity   Per-CPU ring buffer capacity in bytes.
```

## regman documents intent, not current state

This is the boundary that matters most. **`regman` tells you what a setting *should be* — what it means and which values are legal — not what it is currently set to on this machine.** It reads shipped documentation, never the live registry; it does not talk to [LCS](/peios/using-peios/registry-administration/lcs-and-sources.md) at all. Ask `regman` about `BufferCapacity` and you learn it must be a power of two and defaults to 4 MB; you do *not* learn that this particular box has it set to 8 MB right now. Reading the current value is a separate, live query against the registry — that is [`reg`](/peios/using-peios/registry-administration/reg.md)'s job. `regman` tells you a knob *should* be a power of two; `reg get` tells you what it is set to, and `reg set` changes it. Reach for `regman` to decide what to write, and `reg` to write it.

Put `regman` next to the other two things from [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md) and a clean division of labour appears:

| To learn… | Look at… |
|---|---|
| What a setting *means*, and what is valid | `regman` — the manual |
| What the value is *set to* | [`reg`](/peios/using-peios/registry-administration/reg.md) — a live query against the store |
| What the subsystem is *actually running on* | the [event log](/peios/security-fundamentals/auditing/overview.md) |

`regman` is the one you consult first, and the only one that works before you have changed anything — because documentation ships with the software, not with the machine's state. It is the natural complement to [reject-or-keep](/peios/using-peios/registry-concepts/configuration-and-meaning.md): `regman` tells you the valid range up front, so you set a good value the first time instead of writing one the owning subsystem will quietly refuse.

## Where the documentation comes from

`regman` has no built-in database of every setting. Each package **ships its own documentation** as files dropped into a well-known directory (`/usr/share/regman/`), one fragment per package documenting that package's whole configuration surface. Install a package and its registry documentation appears; remove the package and it goes away. The package that *owns* a set of keys is the package that documents them — the only arrangement that stays correct as the system changes.

A consequence worth knowing: `regman` can only describe settings that some installed package has documented. An undocumented key is invisible to it — `regman` is exactly as complete as the packages on the machine make it. If two packages happen to document the same item, `regman` shows both and flags the overlap rather than silently picking a winner.

(For the people who *write* that documentation, `regman fmt` and `regman lint` prepare and check fragments, and `regman index` keeps lookups fast on large corpora — the lookup is always correct without an index, which is purely an accelerator. These are packaging concerns rather than everyday operator ones.)

## Where to go next

For the idea `regman` exists to serve — why the registry holds values without holding their meaning, and what reject-or-keep means — read [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md).

For the other half of the picture — reading what a value is actually *set to*, which is a live query against the store rather than a documentation lookup — read [LCS and sources](/peios/using-peios/registry-administration/lcs-and-sources.md).

---

# reg

_Peios / Using Peios / Administration_

> The reg command reads and writes the live registry — values, keys, layers, links, security descriptors, watches, and atomic batches.

`reg` is the command-line interface to the **live** registry — the running [LCS](/peios/using-peios/registry-administration/lcs-and-sources.md) store, reached through the registry system calls. Where [`regman`](/peios/using-peios/registry-administration/regman.md) reads shipped documentation and tells you what a key *means*, `reg` talks to the kernel and reads or writes what a key *is*. It is the everyday tool for scripting configuration: querying effective values, writing to layers, masking and hiding, managing security descriptors, watching for change, and taking backups.

```
reg <command> [options] <key> [value] [data]
```

`reg` is the read/write half of the registry toolset and [`regman`](/peios/using-peios/registry-administration/regman.md) is the lookup half. The division is exact:

| To… | Use… |
|---|---|
| Read or change what a value is *set to*, on this machine, right now | `reg` |
| Look up what a value *means* — its type, default, valid range, when it applies | [`regman`](/peios/using-peios/registry-administration/regman.md) |

Consult `regman` first to learn the legal range for a knob, then use `reg set` to write a value inside it. `reg` never checks a value's meaning and `regman` never touches the live store; they are complementary.

## How reg treats access and privilege

Every `reg` operation goes through the kernel's access check against the single key it opens — the same [access-control](/peios/using-peios/registry-security/access-control.md) path as any other protected object, with **no traversal check** on the parent keys. `reg` performs no identity or membership checks of its own: it does not refuse a privileged operation up front. It attempts the operation and reports faithfully whatever the kernel returns. An operation that needs a privilege you lack (creating a link, a positive-precedence layer, a backup or restore, reading a SACL) simply fails with *access denied* (exit `3`) rather than being blocked by the tool. A single `reg` command may legitimately span privilege tiers.

## Addressing model

Almost every subcommand shares one addressing scheme: a **key path** positional, and an optional **value name** positional. Understanding the split is most of understanding `reg`.

### Key paths

The key is a positional path argument. Both `/` and `\` are accepted as separators — neither character is legal inside an LCS key component, so there is no ambiguity, and you may use whichever your shell quotes most comfortably:

```
reg get Machine/System/KMES
reg get 'Machine\System\KMES'      # identical
```

A leading separator is optional and ignored (`/Machine/X` is the same as `Machine/X`). Paths are compared case-insensitively. The first component is the **hive**:

| Path | Meaning |
|---|---|
| `Machine\…` | The machine hive — system-wide configuration. |
| `Users\<SID>\…` | A specific principal's hive. |
| `CurrentUser\…` | A kernel alias, rewritten to the caller's own `Users\<SID>\` at the syscall boundary. |

On **output**, `reg` displays paths with a backslash by default. `--sep=/` (or `REG_SEP=/`) switches display to forward slash for that invocation; this affects display only, never how a path is parsed.

### The value-name positional

The value name is a **separate** positional argument — it is never folded into the key path. This is deliberate: an LCS value name may itself contain `/` or `\` (which a key component may not), so keeping them apart removes all ambiguity.

```
reg get Machine/System/KMES BufferCapacity
#       └──── key path ─────┘ └── value ──┘
```

The presence or absence of the value argument selects what the command targets:

- **No value argument ⇒ the command targets the key itself** — its metadata, its values as a set, its security descriptor, its children.
- **A value argument ⇒ the command targets that one named value.**
- The **default value** (the empty-name value) is addressed by the literal token `@` in the value-name position, mirroring `.reg` convention: `reg get Machine\App @`.

This split applies uniformly to `get`, `set`, `del`, `mask`, and `unmask`.

### Value literals and types

On `set` (and in a batch), the value's data is a single token whose registry type is either **forced** by a `type:` prefix or **inferred** from its shape. The registry value types and their literal syntax:

| Prefix | Registry type | Data syntax |
|---|---|---|
| `sz:` | `REG_SZ` | UTF-8 string (rest of token, verbatim). |
| `expand:` | `REG_EXPAND_SZ` | UTF-8 string, `%VAR%` left unexpanded on disk. |
| `dword:` | `REG_DWORD` | `42` or `0x2A`; must fit `u32`. |
| `dword-be:` | `REG_DWORD_BIG_ENDIAN` | `42` or `0x2A`; must fit `u32`. |
| `qword:` | `REG_QWORD` | `42` or `0x2A`; must fit `u64`. |
| `multi:` | `REG_MULTI_SZ` | Comma-separated; `\,` escapes a literal comma. |
| `hex:` / `bin:` | `REG_BINARY` | Hex bytes; `:`, `-`, and space separators are ignored. |
| `link:` | `REG_LINK` | An absolute key path (a symlink target). |
| `none:` | `REG_NONE` | No data (token must be empty after the prefix). |

When the substring before the first `:` is **not** a recognised keyword (for example `http://host`), the token is not treated as typed and inference applies:

| Token shape | Inferred type |
|---|---|
| all decimal digits, fits `u32` | `REG_DWORD` |
| all decimal digits, fits `u64` but not `u32` | `REG_QWORD` |
| `0x…` hex, ≤ 8 significant hex digits | `REG_DWORD` |
| `0x…` hex, ≤ 16 significant hex digits | `REG_QWORD` |
| anything else (including empty) | `REG_SZ` |

Inference is intentionally broad, so any all-digit token becomes a number — a PIN, a zip code, or `007` becomes a `REG_DWORD` and loses its textual form. **To force a string, prefix it with `sz:`** (`sz:007` stores the string `007`; `sz:dword:42` stores the literal string `dword:42`). Because the coercion is a known trap, `reg set` **always echoes the resolved type on success**, so a surprising conversion is never silent even under `--quiet`.

## Global options

Every subcommand accepts the following. Each behavioural toggle also has an environment-variable equivalent, so it can be set per-invocation (the flag) or once per shell or script (the variable). The flag wins over the variable, which wins over the built-in default.

| Option | Env var | Effect |
|---|---|---|
| `--json` | `REG_JSON=1` | Emit structured JSON instead of human text. `watch` emits JSON-lines. |
| `-v`, `--verbose` | `REG_VERBOSE=1` | More detailed output. |
| `-q`, `--quiet` | — | Suppress non-essential output. (A surprising type coercion is still reported.) |
| `--sep=CHAR` | `REG_SEP` | Path *display* separator: `\` (default) or `/`. |
| `--help` | — | Print help for the command and exit `0`. |
| `--version` | — | Print the version and exit `0`. |

Mutating subcommands additionally accept:

| Option | Env var | Effect |
|---|---|---|
| `--layer NAME` | `REG_LAYER` | Target layer for the write (default: the base layer). |
| `-y`, `--yes` | `REG_ASSUME_YES=1` | Skip the confirmation prompt on a destructive command. |

Destructive commands (`del -r`, `restore`, `layer del`) prompt for confirmation when standard input is a terminal, and auto-proceed when it is not — so interactive use is guarded while scripts are never blocked. Set `-y`/`--yes` or `REG_ASSUME_YES=1` to skip the prompt explicitly.

---

## Reading

### `reg get <key> [value]`

Read the effective (resolved) value. With a `value`, prints that value's data; with no value, lists the key's effective values (the default `@` value first, then named values sorted case-insensitively) — a shorthand for `ls --values-only`.

| Option | Effect |
|---|---|
| `-L`, `--layers` | Annotate the value with the layer it resolved from and its sequence number. |
| `--raw` | Write the value's raw bytes to standard output verbatim — for piping `REG_BINARY` data. |
| `--no-follow` | Operate on a symlink key itself rather than following it to its target. |

The single-value default form prints data bare (no quotes; one element per line for `REG_MULTI_SZ`) so it pipes cleanly. `-L` shows the winning layer's provenance:

```
$ reg get Machine\System\KMES BufferCapacity
4194304

$ reg get Machine\App Theme -L
Theme = REG_SZ "dark"   (layer: policy, seq 7)
```

Only the *winning* value is shown; the full shadowed stack beneath it is not yet exposed by any client ABI (the `-L` flag will render the whole stack unchanged once that primitive lands). See [Layers](/peios/using-peios/registry-layers/layers.md) for what "effective" resolves against.

### `reg ls <key>`

One-level listing of a key's immediate subkeys and values.

| Option | Effect |
|---|---|
| `-l`, `--long` | Long form: for subkeys, child and value counts; for values, type and byte size. |
| `--keys-only` | List subkeys only. |
| `--values-only` | List values only. |

```
$ reg ls Machine\System\KMES -l
BufferCapacity = REG_QWORD 4194304   (8 bytes)
MaxEventSize   = REG_DWORD 65536   (4 bytes)
```

### `reg tree <key>`

Recursively list the subkey tree rooted at `key`.

| Option | Effect |
|---|---|
| `--depth N` | Limit recursion to `N` levels. |
| `--values` | Include each node's values, not just its keys. |

```
$ reg tree Machine\System --depth 2
```

### `reg info <key>`

Print a key's metadata: leaf name, subkey and value counts, last-write time, hive generation, security-descriptor size, and the volatile and symlink flags.

| Option | Effect |
|---|---|
| `--no-follow` | Inspect a symlink key itself rather than its target. |

```
$ reg info Machine\System\KMES
path             Machine\System\KMES
name             KMES
subkeys          0
values           4
last_write_ns    1717243800000000000
hive_generation  42
sd_size          164
volatile         false
symlink          false
```

---

## Writing

### `reg set <key> <value> <data>`

Create or update a value. The `value` name (or `@`) and the `data` token are both required. Writes are tagged with `--layer` (default: base). See [value literals](#value-literals-and-types) for the `data` syntax.

| Option | Effect |
|---|---|
| `--layer NAME` | Write into layer `NAME` instead of the base layer. |
| `-p`, `--parents` | Create any missing ancestor keys. |
| `--expected-seq N` | Compare-and-swap guard: apply only if the value's current sequence is `N`; a mismatch exits `6` without writing. |

```
$ reg set Machine\App Build 4096
set Machine\App Build = REG_DWORD 4096   (layer: base)

$ reg set Machine\App Servers multi:alpha,beta --layer policy
```

`--expected-seq` supports lock-free read-modify-write: read the value with `-L` to learn its sequence, then write back with `--expected-seq` set to that number; if another writer changed it in between, your write fails cleanly instead of clobbering theirs.

### `reg new <key>`

Create a key with no values. Reports whether the key was created or already existed.

| Option | Effect |
|---|---|
| `--layer NAME` | Create the key in layer `NAME`. |
| `-p`, `--parents` | Create any missing ancestor keys. |
| `--volatile` | Create a volatile (RAM-only) key that does not survive a reboot. |

```
$ reg new Machine\App\Cache --volatile
created Machine\App\Cache   (layer: base)
```

### `reg del <key> [value]`

Delete a value, or a key. (Alias: `delete`.)

With a `value`, deletes **this layer's** entry for that value only — lower-layer entries resurface, because a per-layer delete is not a tombstone (use `mask` for that). With no value, deletes the key, which must be empty unless `-r` is given.

| Option | Effect |
|---|---|
| `--layer NAME` | Delete from layer `NAME`. |
| `-r`, `--recursive` | Delete the key and all its descendants (walks and removes children). |
| `-y`, `--yes` | Skip the confirmation prompt (recursive deletes prompt on a terminal). |

```
$ reg del Machine\App Legacy
deleted value Legacy from Machine\App

$ reg del Machine\App\Temp -r
Recursively delete key Machine\App\Temp and all its contents? [y/N] y
deleted Machine\App\Temp (3 keys)
```

See [Deleting keys and values](/peios/using-peios/registry-layers/deleting-keys-and-values.md) for how a per-layer delete differs from a tombstone.

---

## Masking and hiding

These commands expose LCS's tombstone and hide primitives, which *mask* lower-precedence state rather than removing a layer's own entry — the distinction that makes [layers](/peios/using-peios/registry-layers/layers.md) revertible. The layer is chosen with `--layer` (default: base).

### `reg mask <key> [value]` / `reg unmask <key> [value]`

`mask` sets a **tombstone**: a per-value marker in the target layer that hides that value from all layers below it. `unmask` clears it.

| Option | Effect |
|---|---|
| `--layer NAME` | The layer that carries the tombstone. |
| `--all` | Blanket tombstone: mask *all* lower-layer values on the key (no `value` argument). |

A `value` is required unless `--all` is given.

```
$ reg mask Machine\App ApiKey --layer policy
set tombstone on Machine\App ApiKey (layer: policy)

$ reg mask Machine\App --all --layer policy
set blanket tombstone on Machine\App (layer: policy)
```

### `reg hide <key>` / `reg unhide <key>`

`hide` installs a HIDDEN path entry in the target layer, masking the key's existence in that layer and below. `unhide` removes that layer's path entry, letting the key reappear.

| Option | Effect |
|---|---|
| `--layer NAME` | The layer that hides (or unhides) the key. |

```
$ reg hide Machine\App\Debug --layer policy
hid Machine\App\Debug (layer: policy)
```

---

## Layers

### `reg layer ls`

List layers with name, precedence, enabled state, and (informational) owner SID, ordered by descending precedence.

| Option | Effect |
|---|---|
| `-l`, `--long` | Long form. |

```
$ reg layer ls
policy                   prec=100    enabled   owner=S-1-5-32-544
base                     prec=0      enabled
```

### `reg layer new <name>`

Create a layer by writing its metadata key under `Machine\System\Registry\Layers\`.

| Option | Effect |
|---|---|
| `--precedence N` | Precedence (higher wins). A precedence above `0` requires `SeTcbPrivilege`; the tool attempts it and reports any denial. |
| `--owner SID` | Record an informational owner SID. |
| `--disabled` | Create the layer disabled. |

```
$ reg layer new policy --precedence 100 --owner S-1-5-32-544
created layer policy (precedence 100, enabled)
```

### `reg layer set <name>`

Modify an existing layer's metadata.

| Option | Effect |
|---|---|
| `--precedence N` | Change the precedence. |
| `--enable` / `--disable` | Enable or disable the layer. |
| `--owner SID` | Change the informational owner SID. |

### `reg layer del <name>`

Delete a layer — removes its metadata key; LCS tears down the layer's entries. Prompts for confirmation on a terminal.

```
$ reg layer del policy
Delete layer policy and all its entries? [y/N] y
deleted layer policy
```

Managing per-process **private** layer views is out of scope for `reg`; see [private hives and layers](/peios/using-peios/registry-advanced/private-hives-and-layers.md).

---

## Security descriptors

### `reg sd <key>`

Show or set a key's [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) as SDDL, using the same codec as the [`sd`](/peios/security-fundamentals/security-descriptors/overview.md) tool, so its output is consistent. With no scope flags, the default is owner + group + DACL (a SACL requires the privileged `ACCESS_SYSTEM_SECURITY` right).

| Option | Effect |
|---|---|
| `--set SDDL` | Apply the given SDDL. Without it, `sd` prints the current descriptor. |
| `--owner` | Scope to the owner. |
| `--group` | Scope to the group. |
| `--dacl` | Scope to the DACL. |
| `--sacl` | Scope to the SACL (needs `ACCESS_SYSTEM_SECURITY`). |

```
$ reg sd Machine\App
O:BAG:BAD:(A;;KA;;;BA)(A;;KR;;;WD)

$ reg sd Machine\App --set 'O:BAG:BAD:(A;;KA;;;BA)' --owner --dacl
set security descriptor on Machine\App
```

A security-descriptor change takes effect on future opens only (existing handles keep the access mask they were granted). **Security changes are not layered and are not undone by layer removal** — they mutate the key object directly. See [Access control on keys](/peios/using-peios/registry-security/access-control.md).

---

## Symlinks

### `reg link <key> <target>`

Create a symlink key pointing at the absolute `target` key path. This creates a key with the immutable symlink flag and a default `REG_LINK` value holding the target. Requires `KEY_CREATE_LINK` and `SeTcbPrivilege`/Administrator; the tool attempts it and reports any denial.

| Option | Effect |
|---|---|
| `--layer NAME` | Create the link key in layer `NAME`. |

```
$ reg link Machine\App\CurrentConfig Machine\App\Config\v3
linked Machine\App\CurrentConfig -> Machine\App\Config\v3 (layer: base)
```

`get` and `info` follow symlinks to their target by default; pass `--no-follow` to inspect the link key itself. See [Advanced: registry links](/peios/using-peios/registry-advanced/registry-links.md).

---

## Watching

### `reg watch <key>`

Arm a change watch and stream one event per change until interrupted (or until `--count` events). Each event faithfully means "something under the filter changed — re-read"; `reg` does not decode the change records' contents. With `--json`, emits one JSON object per line.

| Option | Effect |
|---|---|
| `--subtree` | Watch descendant keys too, not just this key. |
| `--filter LIST` | Comma-separated subset of `value`, `subkey`, `sd` (default: all). |
| `--count N` | Exit after `N` events. |

```
$ reg watch Machine\System\KMES --subtree --filter value
changed: Machine\System\KMES
changed: Machine\System\KMES
```

See [Watching for changes](/peios/using-peios/registry-concepts/watches.md) for why a service watches instead of polling.

---

## Batch, export, backup, and restore

Two paths move a subtree around: `export`/`apply` are the *portable, reviewable* path (a diffable text or JSON document); `backup`/`restore` are the *exact, privileged* path (an opaque kernel snapshot).

### `reg apply <file>`

Apply a batch of operations to one hive in a single [transaction](/peios/using-peios/registry-advanced/transactions.md) — all-or-nothing. All operations must target one hive (a cross-hive batch fails with exit `4`). `-` reads standard input.

| Option | Effect |
|---|---|
| `--dir DIR` | Apply every batch file in `DIR` (sorted), each as its own transaction. A missing or empty directory applies nothing and succeeds. |
| `--once-delete` | Delete each batch file after it applies successfully (a drain). |
| `-y`, `--yes` | Skip confirmation. |

The batch format is JSON (canonical and exact) or a line-oriented text format; `apply` auto-detects (a leading `{` or `[` means JSON). **Text-format input is not yet implemented** — supply JSON, or generate one with `reg export --json`. Text *export* works for review.

```
$ reg export --json Machine\App - | reg apply -
applied 4 keys
```

### `reg export <key> [file]`

Dump a subtree to the batch format. Omit `file` (or pass `-`) to write to standard output. Human-readable text by default; `--json` emits the canonical exact form.

| Option | Effect |
|---|---|
| `--layer NAME` | Export one layer's view (default: the effective state). |

```
$ reg export Machine\App app-config.reg
```

### `reg backup <key> <file>`

Write an opaque, exact, binary kernel snapshot of a key and its subtree to `file`. Requires `SeBackupPrivilege`.

```
$ reg backup Machine\System\KMES kmes.snap
backed up Machine\System\KMES -> kmes.snap
```

### `reg restore <key> <file>`

Replace a key and its entire subtree from a snapshot, in one transaction. Requires `SeRestorePrivilege`. Prompts for confirmation on a terminal.

```
$ reg restore Machine\System\KMES kmes.snap
Replace key Machine\System\KMES and its entire subtree from kmes.snap? [y/N] y
restored Machine\System\KMES from kmes.snap
```

See [Backup and restore](/peios/using-peios/registry-administration/backup-and-restore.md) for when to reach for each path.

---

## Output formats

Default output is terse, human-readable, and grep-friendly. `--json` switches every command to structured output — a single self-contained JSON document, except `watch`, which emits JSON-lines (one object per event). In any value listing, the default (`@`) value is emitted first, then named values sorted case-insensitively.

Type formatting on read:

| Registry type | Human form | JSON form |
|---|---|---|
| `REG_SZ` / `REG_EXPAND_SZ` | the string | `{"type":"sz","data":"…"}` |
| `REG_DWORD` / `REG_QWORD` | decimal | `{"type":"dword","data":42}` |
| `REG_MULTI_SZ` | one element per line | `{"type":"multi","data":["a","b"]}` |
| `REG_BINARY` | hex | `{"type":"binary","data":"deadbeef"}` |
| `REG_LINK` | the target path | `{"type":"link","data":"…"}` |

## Exit status

| Code | Meaning | Typical cause |
|---|---|---|
| `0` | Success. | — |
| `1` | Usage error. | Bad arguments; a required option missing. |
| `2` | Key or value not found. | `ENOENT`. |
| `3` | Access denied. | `EACCES`, `EPERM` — the kernel refused the operation. |
| `4` | Invalid specification. | Bad path, type, or literal; a cross-hive batch; a name too long (`EINVAL`, `EXDEV`, `ENAMETOOLONG`). |
| `5` | Syscall or source failure. | `EIO`, `ETIMEDOUT`, `ENOSPC`, `ENOMEM`. |
| `6` | Compare-and-swap conflict. | `EAGAIN` — an `--expected-seq` guard did not match; the value changed under you. |

A denial (`3`) reports the operation, the target, and, where relevant, the access that was required — in the style of the `sd` and `token` tools.

## See also

- [`regman`](/peios/using-peios/registry-administration/regman.md) — the registry *manual*: what a key or value *means*, as opposed to what it is set to. Consult it before you `reg set`.
- [Keys, values, and types](/peios/using-peios/registry-concepts/keys-values-and-types.md) — the data model `reg` addresses.
- [Layers](/peios/using-peios/registry-layers/layers.md) — what "effective" resolves against, and what `--layer`, `mask`, and `hide` operate on.
- [Access control on keys](/peios/using-peios/registry-security/access-control.md) — the check every `reg` operation goes through.

---

# Private hives and layers

_Peios / Using Peios / Advanced_

> Private hives and layers give one caller its own view of the registry — the building block for sandboxing, with authorisation deferred to KACS.

Everything in the core topic describes one shared registry that every process sees the same way (subject to access control). **Private hives** and **private layers** relax that: they let a particular caller see a registry that differs from everyone else's — the building block for sandboxing and container-style isolation.

This is an advanced feature, and a partly forward-looking one. The registry defines how a private hive or layer participates in [resolution](/peios/using-peios/registry-layers/layers.md); but *who* is allowed to see one is decided by a thread's credentials, and that credential model is part of KACS and not yet fully specified. Treat this page as the shape of the feature, not a how-to.

## Private hives

A **private hive** is a hive that is visible only to threads whose credentials carry a matching scope — an opaque identity that marks "you are allowed to see this hive". To everyone else it does not exist.

The powerful case is **shadowing**. A private hive can take the same name as a global one — `Machine\`, say — and for a thread that carries the scope, the private hive is what `Machine\` resolves to, not the global one. A sandboxed process can therefore be given an entirely separate `Machine\` while every other process continues to see the real one, with no change to the paths anyone uses. That is complete registry isolation without a parallel namespace: same paths, different contents, decided by who is asking.

## Private layers

A **private layer** is a [layer](/peios/using-peios/registry-layers/layers.md) that is globally disabled — invisible in normal resolution — but attached to a specific thread's credentials. For that thread, and only that thread, the layer is treated as active and competes in the per-value contest at its precedence exactly like any other layer. Everyone else resolves as if it were not there.

This is the lighter-weight tool, for when you want a different *value* here and there rather than a whole separate hive:

- **Per-session overrides** — run one application with experimental settings without disturbing other sessions.
- **Testing** — inject configuration for one process without writing it into the shared registry.
- **Sandboxing** — give a confined process a slightly different view without standing up a private hive.

Private layers are **per thread, not per process**: because the attachment rides on a thread's credentials, different threads in the same process can carry different private layers (for instance, a service thread acting on behalf of a particular client). The number a thread can carry is bounded.

## Authorisation lives in KACS

The registry defines only the *resolution* behaviour — private hives are checked ahead of global ones, and a thread's private layers fold into its contests. The decision about whether a thread may join a scope or attach a layer belongs to KACS's credential model. One constraint is already clear and worth stating: attaching a layer that sits *above* others in precedence must be privileged, or an unprivileged process could attach an existing high-precedence policy layer to itself and gain a window onto configuration it should not see or influence. The isolation is only as strong as the rule that decides who may carry a scope or a private layer.

## Where to go next

For the resolution model these build on — precedence, recency, and the per-value contest — read [Layers](/peios/using-peios/registry-layers/layers.md).

For where private hives are routed and served, read [LCS and sources](/peios/using-peios/registry-administration/lcs-and-sources.md).

---

# Transactions

_Peios / Using Peios / Advanced_

> A transaction groups several registry writes into one all-or-nothing commit — how a role installs without ever being half-applied.

Most registry writes stand alone. But sometimes a set of changes only makes sense *together* — a role's keys and values are one coherent configuration, and a half-written version would be worse than none at all. A **transaction** makes several writes atomic: all of them commit, or none do. This is a developer-facing feature — you reach for it when *writing* to the registry, not when operating a running system — which is why it sits among the advanced topics.

## Transactions, in one sentence

**A transaction groups many registry writes into one all-or-nothing commit, so other readers only ever see the complete change, never a partial one.**

## All or nothing

Inside a transaction you perform a series of writes and then commit. At commit, they all take effect together. If you abort instead — or the process dies, or the transaction sits open too long and times out — none of them take effect. There is no partial application.

External readers see only committed state. Until you commit, your in-progress writes are invisible to everyone else; the instant you commit, the whole set appears at once. A service reading configuration never catches a transaction halfway. Within your own transaction you *do* see your own pending writes, so you can write a value and read it back before committing.

## The canonical use: installing a role

This is why [role installation](/peios/using-peios/registry-layers/what-layers-are-for.md) is clean. A role's entire configuration — its keys, its values, its layer — is written inside one transaction and committed as a unit. The role is never observed half-installed: either the whole role is there or none of it is. (Removing a role is the *other* mechanism — deleting its layer — covered with [layers](/peios/using-peios/registry-layers/what-layers-are-for.md).)

Any consistent multi-key update wants the same treatment: change several related settings as one atomic step, rather than letting readers see an inconsistent in-between.

## Limits worth knowing

- **One store at a time.** A transaction is scoped to a single hive's store; it cannot span hives backed by different [sources](/peios/using-peios/registry-administration/lcs-and-sources.md). Atomicity across separate stores is not offered.
- **No nesting.** Transactions are flat — there are no sub-transactions or savepoints.
- **Bounded lifetime.** An open transaction holds a write position, so it cannot be left open indefinitely; if it is not committed in time it is aborted automatically. This stops a stalled or abandoned writer from blocking everyone else's writes to that store.
- **Abandonment is safe.** Closing the handle without committing aborts cleanly, and a process dying does the same — there are no orphaned transactions left holding things up.

## Transactions and layers

A transaction provides *atomicity*; it is not a [layer](/peios/using-peios/registry-layers/layers.md) and it does not change which write wins. The two compose: a role install uses a transaction to apply its writes atomically *and* a layer to make them removable later. Keep them distinct — atomicity is "all together", layering is "which one wins, and how to revert".

Transactions are not conflict-detected at the value level. If two committed transactions wrote the same value, both succeed, and the usual rule settles it: the more recent write wins, exactly as in [layer resolution](/peios/using-peios/registry-layers/layers.md). A transaction guarantees its *own* writes land together; it does not lock anyone else out of the values it touched. (For the case where you must not clobber a concurrent change, a conditional write lets a single write proceed only if the value has not changed since you read it.)

## Where to go next

For how a role uses a transaction and a layer together, read [What layers are for](/peios/using-peios/registry-layers/what-layers-are-for.md).

For the recency rule that settles competing writes, read [Layers](/peios/using-peios/registry-layers/layers.md).

---

# Registry links

_Peios / Using Peios / Advanced_

> A key can be a symbolic link to another key — the one place the registry interprets a value — with layered targets and privileged creation.

A registry key can be a **symbolic link** to another key. Open a link key by its path and the registry follows it through to the target, handing you a handle on the target key. Links let one part of the namespace point at another, the way a filesystem symlink does.

Links are an advanced feature with a small, sharp set of rules, and they are the **one exception** to a property the rest of the topic relies on.

## A link is a flag plus a target value

Two pieces make a link, and both are needed:

- The key is **marked** as a link when it is created. This is a fixed property of the key.
- The key's **default value**, of type `REG_LINK`, holds the **target path** — an absolute registry path the link points at.

When path resolution reaches a link key, the registry reads that target and continues resolving from there; the handle you get back refers to the target, not the link.

## The one place the registry reads a value

[Keys, values, and types](/peios/using-peios/registry-concepts/keys-values-and-types.md) made a point of it: the registry stores a value's type and bytes but never interprets them. `REG_LINK` is the single exception. It is the one type the registry acts on itself — following it during path resolution — rather than handing it back untouched. Everything else remains opaque; links are the lone case where the store cares what a value *says*.

## Managing the link itself

Normally you want to follow a link. To operate on the link key *itself* — to change where it points or delete it — you open it with a flag that says "open the link, do not follow it". Without that flag every open lands on the target, which would make the link impossible to manage.

## Creating a link is privileged

Because a link silently redirects whoever opens it, creating one is restricted. It requires the `KEY_CREATE_LINK` right on the parent **and** a privileged caller (the system-trust privilege, or Administrator membership). A link is a small piece of trusted plumbing, not something an ordinary process gets to introduce into a path other callers will traverse.

There is one safety rule worth knowing: a link target is followed **literally**, and the `CurrentUser\` convenience alias is *not* expanded inside it. This stops a link from redirecting a privileged service that resolves `CurrentUser\` into the service's *own* user hive — a classic confused-deputy trap. Link targets route by their literal hive name. Resolution is also bounded by a hop limit, so a cycle of links fails rather than looping forever.

## Layers can redirect a link

The target is an ordinary [layered](/peios/using-peios/registry-layers/layers.md) value — the link key's default value — so it plays by the same rules as any other value. A higher-precedence or more-recent layer can write a *different* `REG_LINK` target and redirect the link; remove that layer and the original target resurfaces, by the usual automatic revert. And if a layer writes a default value that is *not* a `REG_LINK` onto a key that is still flagged as a link, resolution through it fails until the offending layer is removed or overridden. The link's *identity* is fixed at creation; its *target* is just configuration, and configuration is layered.

## Where to go next

For the opaque-value rule this is the exception to, read [Keys, values, and types](/peios/using-peios/registry-concepts/keys-values-and-types.md).

For how a layer can redirect or break a link, read [Layers](/peios/using-peios/registry-layers/layers.md).

---

# Files and directories

_Peios / Using Peios / Peiosutils / Files and directories_

> The commands that create, copy, move, remove, and resize files and directories — and how each one handles a file's security descriptor.

This topic covers the commands that *change* the file system: creating files and directories, copying and moving them, removing them, and adjusting their size. If a command brings a file into existence, takes one out of it, or duplicates one, it is here.

This page names every command, and then explains the one theme that runs through the whole topic on Peios: what happens to a file's **security descriptor** when the file is created or copied.

## The commands

| Command | Purpose |
|---|---|
| [`cp`](/peios/using-peios/peiosutils/files-and-directories/cp.md) | Copy files and directories. |
| [`mv`](/peios/using-peios/peiosutils/files-and-directories/mv.md) | Move or rename files and directories. |
| [`rm`](/peios/using-peios/peiosutils/files-and-directories/rm.md) | Remove files, and with `-r`, directories. |
| [`rmdir`](/peios/using-peios/peiosutils/files-and-directories/rmdir.md) | Remove directories, but only empty ones. |
| [`mkdir`](/peios/using-peios/peiosutils/files-and-directories/mkdir.md) | Create directories. |
| [`mkfifo`](/peios/using-peios/peiosutils/files-and-directories/mkfifo.md) | Create named pipes (FIFOs). |
| [`mknod`](/peios/using-peios/peiosutils/files-and-directories/mknod.md) | Create special files — device nodes and FIFOs. |
| [`touch`](/peios/using-peios/peiosutils/files-and-directories/touch.md) | Create empty files, or update a file's timestamps. |
| [`ln`](/peios/using-peios/peiosutils/files-and-directories/ln.md) | Create hard links and symbolic links. |
| [`link` and `unlink`](/peios/using-peios/peiosutils/files-and-directories/link-and-unlink.md) | The single-purpose, low-level versions of "make a hard link" and "remove a file". |
| [`shred`](/peios/using-peios/peiosutils/files-and-directories/shred.md) | Destroy a file's contents by overwriting them, so they cannot be recovered. |
| [`dd`](/peios/using-peios/peiosutils/files-and-directories/dd.md) | Copy data block by block, converting it on the way. |
| [`df`](/peios/using-peios/peiosutils/files-and-directories/df.md) | Report how much space each file system has, and how much is free. |
| [`du`](/peios/using-peios/peiosutils/files-and-directories/du.md) | Report how much space files and directories use. |
| [`truncate`](/peios/using-peios/peiosutils/files-and-directories/truncate.md) | Shrink or extend a file to an exact size. |
| [`mktemp`](/peios/using-peios/peiosutils/files-and-directories/mktemp.md) | Create a temporary file or directory with a safely unique name. |

## Every file has a security descriptor

On Peios, every file and directory carries a **security descriptor** — the record of who owns it and who may do what to it. Access to a file is decided from its descriptor; see [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md) for the full picture.

That fact shapes two groups of commands in this topic.

### Creating a file gives it a descriptor

When `mkdir`, `mkfifo`, `mknod`, or `touch` creates a new object, that object needs a security descriptor. By default it gets one **inherited from the directory it is created in** — the new object picks up the access rules its parent directory hands down.

All four of these commands share one set of options — the **creation flags** — that let you set the new object's descriptor explicitly instead of taking the inherited default: name an owner, set a different group, lock the object so it does *not* inherit, give it an integrity label, or supply a complete descriptor. The flags are documented in full on the [`mkdir`](/peios/using-peios/peiosutils/files-and-directories/mkdir.md) page; `mkfifo`, `mknod`, and `touch` each link back to it.

### Copying a file makes a fresh descriptor — unless you preserve

`cp` creates new files, and a cross-file-system `mv` does too. A brand-new copy gets a brand-new descriptor inherited from its destination directory — it does **not** carry the source file's security across by default.

When you *do* want the copy to keep the source's owner, access rules, timestamps, or other attributes, that is the job of the `--preserve` family of options. `cp` and `mv` share an identical `--preserve` surface; it is documented on the [`cp`](/peios/using-peios/peiosutils/files-and-directories/cp.md) page.

### Removing a file checks whether you may write it

`rm` and `shred` both make a decision based on whether you can write a file. `rm` prompts before removing a file you cannot write to; `shred`'s `--force` overrides a file's protection so it can be destroyed. Both judge "can you write this?" by a live access check against the file's security descriptor — not by any decorative metadata on the inode. Each command's page explains exactly where that check is made.

## Where to start

For copying — and for the whole `--preserve` model — read [`cp`](/peios/using-peios/peiosutils/files-and-directories/cp.md).

For creating files and directories, and the shared creation flags, read [`mkdir`](/peios/using-peios/peiosutils/files-and-directories/mkdir.md).

For removing things safely, read [`rm`](/peios/using-peios/peiosutils/files-and-directories/rm.md).

---

# cp

_Peios / Using Peios / Peiosutils / Files and directories_

> Copy files and directories — including how a copy gets its security descriptor and the --preserve family that carries attributes from the source.

`cp` copies files and directories.

```
cp [options] source dest
cp [options] source... directory
cp [options] -t directory source...
```

In the first form, `cp` copies one `source` to one `dest`. In the other two, it copies any number of sources *into* a directory, keeping their names.

```
$ cp report.txt report-backup.txt
$ cp report.txt notes.txt /home/jack/archive/
```

## Copying directories

`cp` refuses to copy a directory unless you ask it to descend into one:

| Option | Effect |
|---|---|
| `-r`, `-R`, `--recursive` | Copy directories, and everything inside them, recursively. |
| `-a`, `--archive` | A faithful recursive copy: copy the whole tree, follow no symbolic links, and preserve every attribute. Equivalent to `-d -R --preserve-all`. |

`-a` is the option to use when you want the copy to be as close to the original as possible — same structure, same security, same timestamps. A plain `-r` copies the *contents* but, as the next section explains, gives the copies fresh security of their own.

## What a copy's security descriptor is

This is the part of `cp` that is specific to Peios, so it is worth being precise.

Every file carries a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) — the record of who owns it and who may do what to it. When `cp` copies a file, it creates a *new* file, and that new file needs a descriptor.

**By default, a copy gets a fresh descriptor — it does not inherit the source's.** Specifically, a plain `cp`:

- gives the copy a descriptor **inherited from the destination directory**, exactly as if you had created a new file there;
- makes **you** the owner of the copy;
- gives the copy **fresh timestamps** — the copy's modification time is "now".

This is usually what you want. A file copied into your home directory should be governed by your home directory's rules and owned by you, regardless of where it came from. But when you need the copy to keep something from the source, you ask for it explicitly — with the `--preserve` family.

### The `--preserve` family

`--preserve` carries chosen attributes from the source onto the copy. The attributes you can preserve:

| Attribute | What it carries from the source |
|---|---|
| `owner` | The owner SID. |
| `dacl` | The whole DACL — every access rule on the file, inherited rules included. |
| `sacl` | The whole SACL — the audit rules and the integrity label. |
| `daclni` | The DACL with **inherited** rules stripped out — only the rules set explicitly on the source. |
| `saclni` | The SACL with inherited rules stripped out. |
| `timestamps` | The access and modification times. |
| `links` | The hard-link structure — files hard-linked together in the source stay hard-linked in the copy. |
| `security` | The `security.peios.*` extended attributes (other than the descriptor itself). |
| `xattrs` | All other extended attributes. |

You rarely name those individually. These options select sensible sets:

| Option | Preserves |
|---|---|
| `-p` | Timestamps only. |
| `--preserve` | Timestamps only — the same as `-p`. |
| `--preserve=LIST` | Exactly the comma-separated attributes you name. |
| `--preserve-all` | Every attribute in the table above. |
| `--sd` | `owner`, `dacl`, `sacl` — the **full security descriptor**, copied verbatim. |
| `--sd-explicit` | `owner`, `daclni`, `saclni` — the source's **explicitly set** rules only. |
| `--no-preserve=LIST` | Turns the named attributes back *off* — useful to subtract from a broader option. |

### `--sd` versus `--sd-explicit`

Both carry the source's security across; they differ in how they treat *inherited* rules.

- **`--sd`** copies the descriptor exactly — inherited rules and all. The copy ends up with precisely the access rules the source had, even if it lands in a directory that would have handed down something different. Use it when the copy must be a security-exact duplicate.
- **`--sd-explicit`** copies only the rules that were set *explicitly* on the source, and lets the destination directory supply inheritance for the rest. Use it when copying a file into a *different* directory and you want the file's own tailored rules to come along while still picking up the new location's inherited rules.

For what "inherited" versus "explicit" means, see [Inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md).

### When a preserve cannot be honoured

A requested preserve is a firm instruction. If `cp` cannot carry an attribute across — for example, writing the copy's SACL requires the `SeSecurityPrivilege` privilege, and the caller does not have it — `cp` treats that as a hard error and the copy fails. It does not quietly produce a copy that is missing what you asked to preserve.

There is one exception, and it is a matter of definition rather than a softening of the rule. `security` and `xattrs` name extended attributes, and some filesystems have no extended attributes **at all** — FAT, 9p, and a squashfs image built without an attribute table are the ones you will meet. Copying *from* such a filesystem carries nothing, because there was nothing there: the preserve is satisfied vacuously, and the copy succeeds.

That is narrow on purpose. It applies only to reading the source. If the source does have extended attributes and the *destination* filesystem cannot hold them, the preserve genuinely failed and `cp` still stops:

```
# cp -a /home/jack/notes /mnt/usb-fat/
cp: failed to set extended attributes on '/mnt/usb-fat/notes': Operation not supported
```

The distinction matters most for `-a`, which requests both attribute classes on every file it touches. Without it, one xattr-less filesystem anywhere in a tree would abort the whole copy partway through.

## Overwriting an existing destination

By default, if the destination already exists, `cp` overwrites it. These options change that.

| Option | Effect |
|---|---|
| `-i`, `--interactive` | Ask before overwriting an existing file. |
| `-n`, `--no-clobber` | Never overwrite an existing file; skip it silently. |
| `-u`, `--update[=WHICH]` | Overwrite only when the source is newer than the destination. `WHICH` can be `all`, `none`, or `older`. |
| `-f`, `--force` | If an existing destination cannot be opened for writing, remove it and try the copy again. |
| `--remove-destination` | Remove the destination *before* opening it, always — contrast `--force`, which only removes on failure. |
| `-b`, `--backup[=CONTROL]` | Before overwriting, make a backup of the destination. |

## Symbolic links in the source

When a source is a symbolic link, `cp` can copy the link itself or the file it points to.

| Option | Effect |
|---|---|
| `-L`, `--dereference` | Always follow symbolic links — copy the file they point to. |
| `-P`, `--no-dereference` | Never follow symbolic links — copy the link itself. |
| `-H` | Follow only the symbolic links named directly on the command line, not links found while recursing. |
| `-d` | Copy links as links, and preserve hard-link structure. Short for `--no-dereference --preserve=links`. |

## Other ways to copy

`cp` can also produce something other than a plain duplicate.

| Option | Effect |
|---|---|
| `-l`, `--link` | Create a hard link to the source instead of copying its data. |
| `-s`, `--symbolic-link` | Create a symbolic link to the source instead of copying. |
| `--reflink[=WHEN]` | Make a lightweight copy-on-write clone where the file system supports it. `WHEN` is `always`, `auto`, or `never`. |
| `--sparse[=WHEN]` | Control whether runs of zero bytes are stored as holes rather than written out. `WHEN` is `always`, `auto`, or `never`. |
| `--attributes-only` | Create the destination and copy its attributes, but not its data. |

## Other options

| Option | Effect |
|---|---|
| `-t`, `--target-directory=DIR` | Copy every source into `DIR`. Useful when the directory is not the last argument. |
| `-T`, `--no-target-directory` | Treat `dest` as a plain file even if it is a directory. |
| `--parents` | Recreate the source's leading directories under the destination. |
| `-x`, `--one-file-system` | Do not cross into a different file system while recursing. |
| `--strip-trailing-slashes` | Strip any trailing slashes from each source argument before using it. |
| `-v`, `--verbose` | Print each file as it is copied. |
| `--debug` | Explain in detail how each file was copied. Implies `-v`. |
| `--progress` | Show a progress bar. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was copied successfully. |
| `1` | A file could not be copied — including a requested `--preserve` that could not be honoured. |

---

# mv

_Peios / Using Peios / Peiosutils / Files and directories_

> Move or rename files and directories — including how a move handles a file's security descriptor depending on whether it crosses a file system.

`mv` moves files and directories from one place to another. Moving a file within the same directory, under a new name, is how you *rename* it.

```
mv [options] source dest
mv [options] source... directory
mv [options] -t directory source...
```

```
$ mv draft.txt report.txt                 # rename
$ mv report.txt notes.txt /home/jack/done/ # move into a directory
```

## A move is either a rename or a copy-and-delete

`mv` does its job in one of two ways, and which one decides what happens to the file's security.

**Within one file system, a move is a rename.** Nothing is copied: the file stays exactly where it physically is, and only its name and directory entry change. The file keeps its inode, and so it keeps its **security descriptor unchanged** — every owner, access rule, audit rule, and timestamp is exactly as before. A rename does not re-evaluate inheritance; a file does not pick up new rules just because it landed in a new directory.

**Across file systems, a move is a copy followed by a delete.** When the destination is on a different file system, `mv` cannot just rename — it copies the data to a new file on the destination, then removes the original. That new file is a genuinely new object, so, exactly as with [`cp`](/peios/using-peios/peiosutils/files-and-directories/cp.md), it gets a **fresh security descriptor inherited from the destination directory** unless you ask otherwise.

So: rename a file and its security is untouched; move it to another file system and the copy starts fresh.

## Preserving security on a cross-file-system move

For the copy-and-delete case, `mv` carries the same `--preserve` family that `cp` does — an identical set of options, behaving the same way. They let a cross-file-system move carry the source's owner, access rules, timestamps, and other attributes onto the new file instead of taking the destination directory's inherited default.

The full `--preserve` reference — the attribute list, `--sd`, `--sd-explicit`, `--preserve-all`, `--no-preserve`, and what happens when a preserve cannot be honoured — is on the [`cp`](/peios/using-peios/peiosutils/files-and-directories/cp.md) page. Everything there applies to `mv` unchanged.

On a same-file-system move the `--preserve` options have nothing to do: a rename already keeps every attribute, because it is the same file.

## Overwriting an existing destination

By default `mv` overwrites an existing destination. These options change that. When more than one of `-i`, `-f`, `-n` is given, the last one wins.

| Option | Effect |
|---|---|
| `-i`, `--interactive` | Ask before overwriting an existing file. |
| `-f`, `--force` | Do not prompt before overwriting. |
| `-n`, `--no-clobber` | Never overwrite an existing file. |
| `-u`, `--update[=WHICH]` | Overwrite only when the source is newer. `WHICH` can be `all`, `none`, or `older`. |
| `-b`, `--backup[=CONTROL]` | Make a backup of the destination before overwriting it. |

## Other options

| Option | Effect |
|---|---|
| `-t`, `--target-directory=DIR` | Move every source into `DIR`. |
| `-T`, `--no-target-directory` | Treat `dest` as a plain file even if it is a directory. |
| `--strip-trailing-slashes` | Strip any trailing slashes from each source argument. |
| `-v`, `--verbose` | Print each file as it is moved. |
| `--debug` | Explain in detail how each file was moved. Implies `-v`. |
| `--progress` | Show a progress bar. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was moved successfully. |
| `1` | A file could not be moved. |

---

# rm

_Peios / Using Peios / Peiosutils / Files and directories_

> Remove files and directories — including how rm decides a file is write-protected and worth prompting about.

`rm` removes files. With `-r`, it removes directories and everything inside them.

```
rm [options] file...
```

```
$ rm draft.txt old-notes.txt
$ rm -r build/
```

Removal is not reversible — once `rm` removes a file, its directory entry is gone. `rm` has a few safety behaviours, below, but the basic rule stands: think before you confirm.

## Removing directories

`rm` will not remove a directory unless you say so:

| Option | Effect |
|---|---|
| `-r`, `-R`, `--recursive` | Remove directories and all of their contents, recursively. |
| `-d`, `--dir` | Remove a directory, but only if it is already empty. |

## The write-protected prompt

By default — when you have not passed `-f` and `rm` is running interactively — `rm` prompts before removing a file you cannot write to:

```
$ rm policy.db
rm: remove write-protected regular file 'policy.db'?
```

The point of that prompt is to catch mistakes: a file you cannot write is one you most likely did not mean to delete.

**"Write-protected" here means a real access check.** `rm` decides it by asking the kernel whether your token may write the file — a live check against the file's [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md). It is not read off any decorative metadata on the inode; it is the same authority question that any write to the file would face. So the prompt is honest: if `rm` says a file is write-protected, it is a file the access model would genuinely stop you writing.

(The check is about *write* access to the file's contents. Whether you may actually *remove* the file is a separate question, decided by its own access right — so you can be prompted about a write-protected file and still be allowed to delete it. The prompt is a courtesy check, not the authorisation.)

## Controlling the prompts

| Option | Effect |
|---|---|
| `-f`, `--force` | Never prompt. Also ignore files that do not exist instead of reporting them. |
| `-i` | Prompt before *every* removal. |
| `-I` | Prompt just *once* before removing more than three files or before a recursive removal. Less intrusive than `-i`, but still a check against a slip. |
| `--interactive[=WHEN]` | Set the prompting explicitly: `never`, `once`, or `always`. |

## The root failsafe

`rm` refuses to recursively remove `/` — the root of the file system — because doing so by accident would be catastrophic.

| Option | Effect |
|---|---|
| `--preserve-root` | Refuse to recurse on `/`. This is the default; you do not need to ask for it. |
| `--no-preserve-root` | Disable the failsafe. Required, in full and unabbreviated, if you genuinely intend a recursive operation on `/`. |

## Other options

| Option | Effect |
|---|---|
| `-v`, `--verbose` | Print each file as it is removed. |
| `--progress` | Show a progress bar. |

## `rm` and `shred`

`rm` unlinks a file — it removes the name, and the space becomes free. The file's *data* may still be physically present on the device until something else overwrites it. When you need the contents to be genuinely unrecoverable, use [`shred`](/peios/using-peios/peiosutils/files-and-directories/shred.md), which overwrites the data before removing the file.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every requested removal succeeded (or, with `-f`, the file did not exist). |
| `1` | A file could not be removed. |

---

# rmdir

_Peios / Using Peios / Peiosutils / Files and directories_

> Remove directories, but only when they are already empty.

`rmdir` removes directories — but only **empty** ones. If a directory still has anything in it, `rmdir` leaves it alone and reports the failure.

```
rmdir [options] directory...
```

```
$ rmdir old-cache/
```

That refusal to touch a non-empty directory is the whole point of `rmdir`: it is the safe way to remove a directory you *believe* is empty. If it is not empty, you find out instead of losing the contents. To remove a directory together with everything inside it, use [`rm -r`](/peios/using-peios/peiosutils/files-and-directories/rm.md).

## Options

| Option | Effect |
|---|---|
| `-p`, `--parents` | Remove the directory and then its ancestors, as long as each becomes empty in turn. `rmdir -p a/b/c` removes `a/b/c`, then `a/b`, then `a`. |
| `--ignore-fail-on-non-empty` | Do not treat "directory not empty" as a failure. Other failures are still reported. Useful when clearing out whatever directories happen to be empty and leaving the rest. |
| `-v`, `--verbose` | Print a line for each directory as it is removed. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every directory was removed. |
| `1` | A directory could not be removed — it was not empty, did not exist, or removal was refused. |

---

# mkdir

_Peios / Using Peios / Peiosutils / Files and directories_

> Create directories — and the shared creation flags that set a new object's security descriptor, used by mkdir, mkfifo, mknod, and touch.

`mkdir` creates directories.

```
mkdir [options] directory...
```

```
$ mkdir reports
$ mkdir -p projects/2026/q2
```

## Creating parent directories

By default `mkdir` creates exactly the directories you name, and fails if a parent is missing. `-p` removes that restriction:

| Option | Effect |
|---|---|
| `-p`, `--parents` | Create any missing parent directories along the way. Also makes `mkdir` succeed quietly if the directory already exists. |
| `-v`, `--verbose` | Print a line for each directory as it is created. |

## The creation flags

A new directory needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) — the record of who owns it and who may do what to it. By default, **a new directory inherits its descriptor from the directory it is created in**: it picks up the access rules its parent hands down. For most directories that is exactly right, and you pass no extra options at all.

When you need the new directory to have a *different* descriptor, the **creation flags** set it explicitly. These five flags are shared, unchanged, by `mkdir`, [`mkfifo`](/peios/using-peios/peiosutils/files-and-directories/mkfifo.md), [`mknod`](/peios/using-peios/peiosutils/files-and-directories/mknod.md), and [`touch`](/peios/using-peios/peiosutils/files-and-directories/touch.md) — whenever one of those commands creates a new object, it accepts this same set. This page is the full reference for them.

| Flag | Argument | Effect |
|---|---|---|
| `--owner` | SID | Make this principal the owner, instead of you. |
| `--group` | SID | Set the object's group. |
| `--label` | level | Give the object a mandatory integrity label. |
| `--no-inherit` | — | Do **not** inherit rules from the parent directory; lock the object to its owner. |
| `--sddl` | SDDL string | Supply the entire security descriptor directly. |

A SID argument is either a literal `S-1-…` value or a well-known alias — `BA` for the built-in administrators, for example.

`--label` takes one of these levels, lowest to highest: `untrusted`, `low`, `medium`, `medium-plus`, `high`, `system`, `protected`. The label applies the standard rule for files and directories — a principal below the object's level cannot write to it.

### Inherited, or explicit

The default and `--no-inherit` are the two ends of a choice.

- **Inherited (the default)** — the new directory tracks its parent. Rules the parent hands down apply to it, and as the parent's rules change the directory follows along. This keeps a tree of directories consistent without per-directory effort.
- **`--no-inherit`** — the new directory takes nothing from its parent. Its access is locked to its owner, and it does not follow the parent's rules. Use it to carve out a directory whose security stands apart from the tree around it.

See [Inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) for the full model.

### `--sddl` for a complete descriptor

The four flags above are shortcuts for common adjustments. When you need to specify the *whole* descriptor — a particular set of access rules, audit rules, owner, and group all at once — `--sddl` takes it as a single string in the security-descriptor definition language:

```
$ mkdir --sddl 'O:BAG:BAD:(A;OICI;FA;;;BA)' secure-area
```

`--sddl` is mutually exclusive with `--owner`, `--group`, `--label`, and `--no-inherit` — it already says everything those flags would say, so combining them is rejected.

### How the descriptor is applied

`mkdir` creates the directory first and then applies the descriptor to it. The two steps are not separately visible — `mkdir` either finishes with the directory created and secured as asked, or fails as a whole. If applying the descriptor fails, the half-created directory is not left behind.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every directory was created. |
| `1` | A directory could not be created, or a creation flag could not be applied. |

---

# mkfifo

_Peios / Using Peios / Peiosutils / Files and directories_

> Create named pipes (FIFOs).

`mkfifo` creates **named pipes**, also called FIFOs.

```
mkfifo [options] name...
```

```
$ mkfifo events
```

A named pipe is a file that two programs use to pass a stream of data: one writes into it, the other reads from it, and the data flows through in order — first in, first out, which is what "FIFO" stands for. Unlike an ordinary pipe between two commands, a named pipe has a name on the file system, so the two programs do not have to be started together or be related to each other.

## Setting the new pipe's security

A FIFO is a file, and a new file needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md). By default a new FIFO inherits its descriptor from the directory it is created in.

`mkfifo` accepts the shared **creation flags** — `--owner`, `--group`, `--label`, `--no-inherit`, and `--sddl` — to set that descriptor explicitly instead. They behave exactly as they do for `mkdir`; the full reference is on the [`mkdir`](/peios/using-peios/peiosutils/files-and-directories/mkdir.md) page.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every FIFO was created. |
| `1` | A FIFO could not be created — for example, the name already exists. |

---

# mknod

_Peios / Using Peios / Peiosutils / Files and directories_

> Create special files — device nodes and named pipes.

`mknod` creates **special files** — files that are not data but an interface to something else: a device, or a named pipe.

```
mknod [options] name type [major minor]
```

```
$ mknod /dev/mydisk b 8 0
$ mknod backlog p
```

## The type argument

The `type` says what kind of special file to create:

| Type | Creates |
|---|---|
| `b` | A **block device** — a device addressed in fixed-size blocks, such as a disk. |
| `c` or `u` | A **character device** — a device addressed as a stream of bytes, such as a terminal. |
| `p` | A **named pipe** (FIFO). |

For a block or character device (`b`, `c`, `u`) you must also give a **major** and a **minor** number. The major number selects which driver handles the device; the minor number tells that driver which specific device is meant. For a pipe (`p`), the major and minor numbers are omitted — a pipe has no driver behind it.

A major or minor number is read as hexadecimal if it begins with `0x`, as octal if it begins with `0`, and as decimal otherwise.

`mknod NAME p` and [`mkfifo NAME`](/peios/using-peios/peiosutils/files-and-directories/mkfifo.md) do the same thing; `mkfifo` exists as the clearer way to ask for just a pipe.

## Setting the new file's security

A special file needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) like any other file, and by default a new one inherits its descriptor from the directory it is created in.

`mknod` accepts the shared **creation flags** — `--owner`, `--group`, `--label`, `--no-inherit`, and `--sddl` — to set that descriptor explicitly. They behave exactly as they do for `mkdir`; the full reference is on the [`mkdir`](/peios/using-peios/peiosutils/files-and-directories/mkdir.md) page.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The special file was created. |
| `1` | It could not be created — a missing or invalid type, missing device numbers, or a name that already exists. |

---

# touch

_Peios / Using Peios / Peiosutils / Files and directories_

> Create empty files, or update the timestamps of existing ones.

`touch` does one of two things, depending on whether the file already exists:

- if the file **does not exist**, `touch` creates it, empty;
- if the file **does exist**, `touch` updates its timestamps to the current time.

```
touch [options] [file...]
```

```
$ touch notes.txt        # create notes.txt if absent, else bump its times
```

The "create an empty file" behaviour is the everyday use. The "update timestamps" behaviour matters to tools that decide what work to do by comparing file times — `touch` is how you tell such a tool that a file should be considered freshly changed.

## Which timestamps, and to what

By default `touch` sets both the access time and the modification time to the current moment. These options narrow or redirect that.

| Option | Effect |
|---|---|
| `-a` | Change only the access time. |
| `-m` | Change only the modification time. |
| `--time=WORD` | Choose which time to change by name: `access`, `atime`, or `use` for the access time; `modify` or `mtime` for the modification time. |
| `-d`, `--date=STRING` | Use the time described by `STRING` instead of the current time. |
| `-t STAMP` | Use the time given as `[[CC]YY]MMDDhhmm[.ss]` instead of the current time. |
| `-r`, `--reference=FILE` | Use the timestamps of `FILE` instead of the current time. |
| `-h`, `--no-dereference` | If the named file is a symbolic link, change the link's own timestamps rather than those of the file it points to. |

## Not creating a file

To use `touch` purely to update timestamps, and never to create anything:

| Option | Effect |
|---|---|
| `-c`, `--no-create` | Do not create a file that does not exist. A named file that is absent is silently skipped. |

## Setting a created file's security

When `touch` *creates* a file, that new file needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md), and by default it inherits one from the directory it is created in.

`touch` accepts the shared **creation flags** — `--owner`, `--group`, `--label`, `--no-inherit`, and `--sddl` — to set that descriptor explicitly. They behave exactly as they do for `mkdir`; the full reference is on the [`mkdir`](/peios/using-peios/peiosutils/files-and-directories/mkdir.md) page.

These flags apply **only on the create path**. If `touch` is updating the timestamps of a file that already exists, there is no new descriptor to set, and the creation flags have nothing to do — they do not alter an existing file's security. Pair them with `-c` and they never take effect at all, since `-c` means nothing is ever created.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was created or updated as requested. |
| `1` | A file could not be created or updated. |

---

# ln

_Peios / Using Peios / Peiosutils / Files and directories_

> Create hard links and symbolic links between files.

`ln` creates **links** — extra ways to reach a file.

```
ln [options] target link_name
ln [options] target
ln [options] target... directory
ln [options] -t directory target...
```

```
$ ln -s /opt/app/bin/app /lcl/bin/app   # a symbolic link
$ ln data.csv data-archive.csv                # a hard link
```

## Hard links and symbolic links

There are two kinds of link, and they are genuinely different things.

A **hard link** is another name for the *same file*. The file's data exists once; a hard link is an additional directory entry pointing at it. Every hard link to a file is equal — none is "the original" — and the file's data stays as long as at least one hard link remains. Hard links cannot span file systems, and cannot be made to directories.

A **symbolic link** is a small file that simply *contains a path*. When something opens a symbolic link, it is redirected to whatever that path names. The symbolic link and its target are separate files: if the target is moved or removed, the link still exists but now points at nothing — it "dangles". Symbolic links can point anywhere, across file systems and at directories.

`ln` creates **hard links by default**, and symbolic links with `-s`. When in doubt, `-s` — a symbolic link's behaviour is the easier one to reason about.

The two also differ in how they are secured. A hard link is not a new file, so creating one involves no new [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) — every name for the file shares the file's existing one. A symbolic link *is* a new file, and like any new file it inherits its own descriptor from the directory it is created in.

## The forms

- `ln target link_name` — create one link named `link_name`.
- `ln target` — create a link to `target` in the current directory, with the same final name.
- `ln target... directory` — create a link to each `target` inside `directory`.
- `ln -t directory target...` — the same, with the directory named first.

## Options

| Option | Effect |
|---|---|
| `-s`, `--symbolic` | Make symbolic links instead of hard links. |
| `-f`, `--force` | Remove an existing destination file so the link can be created. |
| `-i`, `--interactive` | Ask before removing an existing destination. |
| `-r`, `--relative` | For a symbolic link, write the target as a path relative to the link's own location, rather than an absolute path. |
| `-n`, `--no-dereference` | If `link_name` is itself a symbolic link to a directory, treat it as an ordinary file — replace the link rather than create something inside the directory it points to. |
| `-L`, `--logical` | When the target is a symbolic link, link to what it points to. |
| `-P`, `--physical` | Make a hard link to a symbolic link itself, rather than to its target. |
| `-t`, `--target-directory=DIR` | Create all links inside `DIR`. |
| `-T`, `--no-target-directory` | Treat `link_name` as a plain file, never as a directory to create links inside. |
| `-b`, `--backup[=CONTROL]` | Back up an existing destination before replacing it. |
| `-S`, `--suffix=SUFFIX` | Use `SUFFIX` for backup file names. |
| `-v`, `--verbose` | Print the name of each link as it is created. |

## Inspecting links afterwards

[`readlink`](/peios/using-peios/peiosutils/listing-and-paths/readlink.md) prints where a symbolic link points. The low-level [`link`](/peios/using-peios/peiosutils/files-and-directories/link-and-unlink.md) command makes a single hard link with no options, for scripts that want exactly that and nothing else.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every link was created. |
| `1` | A link could not be created. |

---

# link and unlink

_Peios / Using Peios / Peiosutils / Files and directories_

> The single-purpose, low-level commands for creating one hard link and for removing one file.

`link` and `unlink` are the bare, single-purpose versions of two operations that [`ln`](/peios/using-peios/peiosutils/files-and-directories/ln.md) and [`rm`](/peios/using-peios/peiosutils/files-and-directories/rm.md) also perform. Each does exactly one thing, takes exactly its operands, and has no options. They exist for scripts that want one precise operation with no surrounding behaviour — no prompting, no link kinds to choose, no recursion.

## `link`

```
link file1 file2
```

`link` creates one hard link: a new name, `file2`, for the existing file `file1`. `file1` must exist; `file2` must not.

```
$ link data.csv data-archive.csv
```

That is the whole command. It performs the link operation directly and reports whether it succeeded. For anything more — symbolic links, replacing an existing destination, linking into a directory — use [`ln`](/peios/using-peios/peiosutils/files-and-directories/ln.md).

## `unlink`

```
unlink file
```

`unlink` removes one file: it deletes the directory entry `file`. It takes a single name and removes it directly.

```
$ unlink stale.lock
```

`unlink` does not prompt, does not recurse, and does not remove directories. For removing several files, removing directories, or any of the safety prompts, use [`rm`](/peios/using-peios/peiosutils/files-and-directories/rm.md).

## Why they exist

`ln` and `rm` are the commands to use day to day — they are flexible and have the safety behaviours. `link` and `unlink` are deliberately rigid: a script that calls `unlink` will *only ever* remove a single file, and a reviewer can see that at a glance. The narrowness is the feature.

## Exit status

Both commands:

| Code | Meaning |
|---|---|
| `0` | The operation succeeded. |
| `1` | The operation failed — a missing or already-existing operand, or a refused request. |

---

# shred

_Peios / Using Peios / Peiosutils / Files and directories_

> Destroy a file's contents by overwriting them repeatedly, so they cannot be recovered — including how --force overrides a file's protection.

`shred` destroys a file's contents. It does not just unlink the file — it **overwrites the data**, repeatedly, so that what was there cannot be read back even with effort and specialised equipment.

```
shred [options] file...
```

```
$ shred -u -v secret-keys.txt
```

[`rm`](/peios/using-peios/peiosutils/files-and-directories/rm.md) removes a file's *name*; the data may sit on the device, recoverable, until something else happens to overwrite it. `shred` overwrites the data on purpose. Use it when a file held something that must not be recoverable.

By default `shred` overwrites a file three times and then **leaves the file in place** — empty of its old contents but still present. That default exists because `shred` is often pointed at device files, which usually should not be removed. To remove the file after shredding it, pass `-u`.

## When shredding actually works

`shred` relies on one assumption: that writing to the file overwrites the *same physical storage* the old data occupied. On a traditional file system that holds. Several common file system designs break it — anything that writes new data to a fresh location instead of in place, keeps snapshots, journals file data, caches copies elsewhere, or compresses. On those, `shred` cannot guarantee the old data is gone.

Backups and remote mirrors are a separate matter entirely: `shred` cannot reach a copy of the file that lives somewhere else.

Treat `shred` as effective on a plain local file system and uncertain otherwise.

## Options

| Option | Effect |
|---|---|
| `-n`, `--iterations=N` | Overwrite `N` times instead of the default of 3. |
| `-u`, `--remove[=HOW]` | Remove the file after overwriting it. `HOW` controls how thoroughly the *name* itself is obscured before the file is unlinked. |
| `-z`, `--zero` | Add a final pass that writes zeros, so the file does not visibly look shredded afterwards. |
| `-s`, `--size=N` | Shred `N` bytes, rather than the whole file. Accepts size suffixes such as `K`, `M`, `G`. |
| `-x`, `--exact` | Do not round the shredded size up to the next full block. Without it, `shred` rounds up so the block's slack space is overwritten too. |
| `--random-source=FILE` | Take the random bytes for the overwrite passes from `FILE`. |
| `-v`, `--verbose` | Show progress as each pass runs. |
| `-f`, `--force` | Override a file's protection: when a live access check shows you cannot write the file, rewrite its security descriptor so the overwrite can proceed (see below). |

## `--force` and a file's protection

To overwrite a file, `shred` has to be able to write it. If the file's [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) does not grant you write access, an ordinary `shred` cannot proceed.

`-f` (`--force`) handles that case. When `--force` is given and a live access check shows you cannot currently write the file, `shred` **rewrites the file's security descriptor** to one that grants full access, and then overwrites the file. The reasoning is deliberate: the file is about to be destroyed anyway, and `--force` is you saying "override this file's protection."

`--force` can do this only when you are allowed to change the file's security in the first place — that is, when you own the file or hold the right to rewrite its access rules. If you cannot change the descriptor, `--force` cannot grant itself write access, and the shred still fails. `--force` overrides a file's *protection*; it does not manufacture authority you do not have.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was shredded successfully. |
| `1` | A file could not be shredded. |

---

# dd

_Peios / Using Peios / Peiosutils / Files and directories_

> Copy data block by block between files, optionally converting and reshaping it on the way.

`dd` copies data from one place to another, a block at a time, and can transform the data as it goes. It is the tool for low-level copying — writing a disk image to a device, extracting an exact byte range out of a file, copying from or to a raw device.

```
dd [operand]...
```

```
$ dd if=disk.img of=/dev/mydisk bs=4M status=progress
```

## How dd is told what to do

`dd` does not take options in the usual `-x` form. It takes **operands**, each written `name=value`, in any order. With no `if=` operand it reads from standard input; with no `of=` operand it writes to standard output.

## The operands

| Operand | Meaning |
|---|---|
| `if=FILE` | Read input from `FILE`. |
| `of=FILE` | Write output to `FILE`. |
| `bs=BYTES` | Read and write `BYTES` at a time. Sets both the input and output block size at once. |
| `ibs=BYTES` | Input block size (default 512). |
| `obs=BYTES` | Output block size (default 512). |
| `cbs=BYTES` | Conversion block size — used by the `block` and `unblock` conversions. |
| `count=N` | Stop after `N` input blocks, rather than reading to the end. |
| `skip=N` | Skip `N` input blocks before copying. Also spelled `iseek=N`. |
| `seek=N` | Skip `N` output blocks before writing. Also spelled `oseek=N`. |
| `conv=LIST` | Apply the comma-separated conversions below. |
| `iflag=LIST` | Comma-separated input flags — how the input is opened and read. |
| `oflag=LIST` | Comma-separated output flags — how the output is opened and written. |
| `status=LEVEL` | How much to report: `progress` (periodic stats while copying), `noxfer` (final counts only), `none` (silent). |

A size value accepts a unit suffix (`K`, `M`, `G`, …). So `bs=4M` is four mebibytes per block.

## Conversions (`conv=`)

| Value | Effect |
|---|---|
| `ucase` / `lcase` | Convert the data to upper-case / lower-case. |
| `swab` | Swap every adjacent pair of bytes. |
| `sync` | Pad each input block out to its full size — with zeros, or with spaces alongside `block`/`unblock`. |
| `block` / `unblock` | Convert between newline-terminated lines and fixed-size `cbs` records. |
| `sparse` | Where an output block is all zeros, seek past it instead of writing it. |
| `excl` | Fail if the output file already exists. |
| `nocreat` | Fail if the output file does *not* already exist. |
| `notrunc` | Do not truncate the output file when opening it. |
| `noerror` | Continue past read errors instead of stopping. |
| `fdatasync` / `fsync` | Flush the data — or the data and metadata — to storage before finishing. |

When `dd` **creates** the output file — an `of=` file that does not already exist — the new file needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md), and it inherits one from the directory it is created in.

## Input and output flags (`iflag=`, `oflag=`)

| Flag | Applies to | Effect |
|---|---|---|
| `count_bytes` | input | Interpret `count=N` as a number of bytes, not blocks. |
| `skip_bytes` | input | Interpret `skip=N` as a number of bytes. |
| `fullblock` | input | Wait for a full `ibs` of data on each read. |
| `seek_bytes` | output | Interpret `seek=N` as a number of bytes. |
| `append` | output | Open the output in append mode. |
| `direct` | both | Use direct I/O, bypassing the cache. |
| `dsync` / `sync` | both | Use synchronised I/O — for data, or for data and metadata. |
| `nonblock` | both | Use non-blocking I/O. |
| `noatime` | both | Do not update the file's access time. |
| `nocache` | both | Ask the system to drop the file's cached pages. |
| `directory` | both | Fail unless the file is a directory. |

## What dd prints

Unless `status=none` is set, `dd` prints a summary when it finishes:

```
16+0 records in
16+0 records out
67108864 bytes (67 MB, 64 MiB) copied, 1.234 s, 54.4 MB/s
```

The `records in` / `records out` counts are written `complete+partial` — full-sized blocks plus any short final block. `status=progress` prints the last line periodically during a long copy.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The copy completed. |
| `1` | The copy failed — a bad operand, an I/O error, or a file that could not be opened. |

---

# df

_Peios / Using Peios / Peiosutils / Files and directories_

> Report how much space each file system has and how much of it is free.

`df` — "disk free" — reports, for each file system, how much space it has and how much is still available.

```
df [options] [file...]
```

With no arguments, `df` reports on every mounted file system. Given a `file`, it reports on the one file system that file lives on.

```
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda2       456G  198G  235G  46% /
/dev/sda1       511M   12M  499M   3% /boot
```

## The default columns

Each line is one file system:

- **Filesystem** — the device or source backing it.
- **Size** — its total capacity.
- **Used** — space in use.
- **Avail** — space still available.
- **Use%** — used space as a percentage.
- **Mounted on** — where it is attached in the directory tree.

## Choosing the units

By default sizes are shown in blocks. These options change that.

| Option | Effect |
|---|---|
| `-h`, `--human-readable` | Show sizes with unit suffixes — `456G`, `12M` — using powers of 1024. |
| `-H`, `--si` | Likewise, but using powers of 1000. |
| `-B`, `--block-size=SIZE` | Show sizes scaled to `SIZE` — `-BM` reports in megabytes. |
| `-k` | Use 1024-byte blocks. |

## Choosing what to report

| Option | Effect |
|---|---|
| `-a`, `--all` | Include dummy and otherwise-hidden file systems that `df` would normally skip. |
| `-l`, `--local` | Report only local file systems, skipping network-mounted ones. |
| `-t`, `--type=TYPE` | Report only file systems of type `TYPE`. |
| `-x`, `--exclude-type=TYPE` | Report everything *except* file systems of type `TYPE`. |
| `--total` | Add a final line giving the grand total across everything reported. |

## Changing the output

| Option | Effect |
|---|---|
| `-i`, `--inodes` | Report inode counts — total, used, and free — instead of block space. A file system can run out of inodes while it still has space. |
| `-T`, `--print-type` | Add a column showing each file system's type. |
| `--output[=LIST]` | Replace the default columns with exactly the fields named in `LIST`. With no list, print every available field. |
| `-P`, `--portability` | Use a fixed, simple column layout that does not wrap a long device name onto its own line. |

## Sync behaviour

| Option | Effect |
|---|---|
| `--sync` | Flush pending writes before reading the usage figures, for the most up-to-date numbers. |
| `--no-sync` | Do not flush first. This is the default and is faster. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The report was produced. |
| `1` | A failure — for example, a named file does not exist, or the table of file systems could not be read. |

---

# du

_Peios / Using Peios / Peiosutils / Files and directories_

> Report how much disk space files and directories use.

`du` — "disk usage" — reports how much space files and directories occupy. Where [`df`](/peios/using-peios/peiosutils/files-and-directories/df.md) tells you about a whole file system, `du` tells you where the space has actually gone.

```
du [options] [file...]
```

```
$ du -sh /home/jack/projects
4.2G    /home/jack/projects
```

With no arguments, `du` works on the current directory. By default it walks the directory tree, prints the cumulative size of **each directory** (a directory's size includes everything under it), and ends with the total for the argument itself. Individual files are not listed unless you ask for them.

## What to count and how deep

| Option | Effect |
|---|---|
| `-a`, `--all` | List every file, not only directories. |
| `-s`, `--summarize` | Print only a single total for each argument — no per-subdirectory breakdown. |
| `-d`, `--max-depth=N` | Show totals only down to `N` levels below each argument. `-d 0` is the same as `-s`. |
| `-S`, `--separate-dirs` | Give each directory's *own* size, excluding its subdirectories. |
| `-c`, `--total` | Add a final grand-total line. |
| `--inodes` | Count inodes used rather than space used. |

## What "size" means

| Option | Effect |
|---|---|
| `-h`, `--human-readable` | Sizes with unit suffixes — `4.2G` — using powers of 1024. |
| `--si` | Likewise, using powers of 1000. |
| `-B`, `--block-size=SIZE` | Scale sizes to `SIZE`. |
| `-k` / `-m` | Use 1024-byte / 1024×1024-byte units. |
| `--apparent-size` | Report the apparent size of files — how much data they contain — rather than the disk space they occupy. The two differ for sparse files and because of block rounding. |
| `-b`, `--bytes` | Report plain byte counts. Equivalent to `--apparent-size --block-size=1`. |

## Following symbolic links

By default `du` does not follow symbolic links.

| Option | Effect |
|---|---|
| `-L`, `--dereference` | Follow all symbolic links and count what they point to. |
| `-D`, `-H`, `--dereference-args` | Follow only the symbolic links named directly on the command line. |
| `-P`, `--no-dereference` | Follow no symbolic links. This is the default. |
| `-x`, `--one-file-system` | Do not cross into a different file system while walking. |
| `-l`, `--count-links` | Count a hard-linked file every time it is encountered, instead of once. |

## Limiting the output

| Option | Effect |
|---|---|
| `--exclude=PATTERN` | Skip files and directories whose name matches `PATTERN`. |
| `--exclude-from=FILE` | Skip everything matching any pattern listed in `FILE`. |
| `-t`, `--threshold=SIZE` | Show only entries at least `SIZE` (or, with a negative `SIZE`, at most that big). |
| `--files0-from=F` | Take the list of files to measure from `F`, NUL-separated. `-` means standard input. |
| `--time[=WORD]` | Also show a timestamp — by default the latest modification time anywhere in the directory. |
| `-0`, `--null` | End each output line with a NUL character instead of a newline. |
| `-v`, `--verbose` | Report extra detail about what is being processed. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The report was produced. |
| `1` | A file or directory could not be accessed. |

---

# truncate

_Peios / Using Peios / Peiosutils / Files and directories_

> Shrink or extend a file to an exact size.

`truncate` sets a file's size to an exact figure — shrinking it, or extending it.

```
truncate [options] file...
```

```
$ truncate -s 0 logfile        # empty the file
$ truncate -s 1G disk.img      # make a 1 GiB file
```

Shrinking a file discards everything past the new size. Extending it adds space that reads back as zero bytes; that added space is a *hole* — it costs no actual storage until something writes real data into it, which is how `truncate -s 1G` can create a "1 GiB file" instantly.

By default, `truncate` creates a file that does not yet exist (as an empty file, then sized as asked). A file created this way needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md), and it inherits one from the directory it is created in.

## Specifying the size

`-s` (`--size`) takes the size. A plain number sets the size outright. A unit suffix scales it: `K`, `M`, `G`, `T`, … where the bare letter is a power of 1024 (`K` = 1024) and the letter with `B` is a power of 1000 (`KB` = 1000).

A size may also begin with a prefix, which makes it *relative to the file's current size*:

| Prefix | Meaning |
|---|---|
| `+` | Extend by this much. |
| `-` | Reduce by this much. |
| `<` | Shrink to this size only if the file is currently larger ("at most"). |
| `>` | Extend to this size only if the file is currently smaller ("at least"). |
| `/` | Round the size *down* to a multiple of this number. |
| `%` | Round the size *up* to a multiple of this number. |

```
$ truncate -s +4K notes.txt    # 4 KiB larger than it is now
$ truncate -s '<1M' notes.txt  # cap it at 1 MiB, leave smaller files alone
```

## Options

| Option | Effect |
|---|---|
| `-s`, `--size=SIZE` | Set or adjust the size to `SIZE`, as described above. |
| `-r`, `--reference=RFILE` | Base the size on the current size of `RFILE` instead of giving a number. Combine with a relative `-s` to mean "the size of `RFILE`, adjusted". |
| `-c`, `--no-create` | Do not create a file that does not exist; skip it instead. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was resized. |
| `1` | A file could not be resized — a bad size, or a file that could not be opened. |

---

# mktemp

_Peios / Using Peios / Peiosutils / Files and directories_

> Create a temporary file or directory with a safely unique name.

`mktemp` creates a temporary file — or, with `-d`, a temporary directory — gives it a name that is not already taken, and prints that name.

```
mktemp [options] [template]
```

```
$ mktemp
/tmp/tmp.QV8x3kZ1aR
$ mktemp -d
/tmp/tmp.7bNc0wPe2L
```

The reason to use `mktemp` rather than inventing a name yourself is **safety**. A script that picks a fixed name like `/tmp/work` has two problems: two copies of the script collide, and a name an attacker can predict is a name an attacker can interfere with before the script gets to it. `mktemp` chooses an unpredictable name *and* creates the file in the same step, so nothing can slip in between. It prints the name it used, for the script to capture:

```
work=$(mktemp)
# ...use "$work"...
rm "$work"
```

The temporary file or directory `mktemp` creates is a new object, and a new object needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md); it inherits one from the directory it is created in.

## Templates

The name comes from a **template** — a name with a run of trailing `X` characters, each of which `mktemp` replaces with a random character. More `X`s means a wider range of possible names.

```
$ mktemp report-XXXXXX.txt
report-a8Kp2Q.txt
```

With no template, `mktemp` uses a built-in default that places the file in the temporary directory. A template needs at least three `X`s.

## Options

| Option | Effect |
|---|---|
| `-d`, `--directory` | Create a directory instead of a file. |
| `-p`, `--tmpdir[=DIR]` | Create the temporary object inside `DIR`. With no `DIR`, use the directory named by the `TMPDIR` environment variable, or the system temporary directory. The template is then just the final name component. |
| `--suffix=SUFFIX` | Append `SUFFIX` after the random part of the name — for giving the file an extension. |
| `-u`, `--dry-run` | Do not create anything; just print a name that *would* be free. This reintroduces the race the command exists to avoid — use it only when you genuinely cannot create the object yet. |
| `-q`, `--quiet` | Print no error message if creation fails; rely on the exit status. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The temporary file or directory was created; its name was printed. |
| `1` | Creation failed — a bad template, or a directory that does not exist. |

---

# sd

_Peios / Using Peios / Peiosutils / Files and directories_

> The sd command reads and changes a file's security descriptor — owner, access rules, audit rules, integrity label, and inheritance.

`sd` is the command-line tool for working with **security descriptors** on files. Everything this topic describes — owners, DACLs, ACEs, the SACL, integrity labels, inheritance — `sd` is how you read it and change it from a shell.

```
sd subcommand path [arguments]
```

```
$ sd show ./report.txt
$ sd allow ./report.txt alice:read
$ sd owner ./report.txt BA
```

Where [`ls -l`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) shows a file's owner and a summary, and [`cp --preserve`](/peios/using-peios/peiosutils/files-and-directories/cp.md) carries a descriptor across, `sd` is the tool that edits the descriptor directly.

## The subcommands

| Group | Subcommand | Does |
|---|---|---|
| Inspect | `show` | Print the descriptor on a path. |
| | `check` | Simulate an access check against the path. |
| DACL | `allow` | Add an allow rule for one or more principals. |
| | `deny` | Add a deny rule for one or more principals. |
| | `remove` | Drop every DACL rule for the named principals. |
| Auditing | `audit` | Add an audit rule to the SACL. |
| | `unaudit` | Drop every SACL rule for the named principals. |
| Ownership | `owner` | Set the descriptor's owner. |
| | `group` | Set the descriptor's group. |
| Integrity | `integrity` | Set the mandatory integrity label. |
| Inheritance | `inherit` | Turn inheritance protection on or off. |
| | `reset` | Drop the file's own rules and re-inherit from the parent. |
| | `propagate` | Push inheritance down to descendants. |
| Wholesale | `set` | Replace the entire descriptor at once. |

## Naming a principal

Wherever a subcommand takes a `PRINCIPAL`, it accepts any of:

| Form | Example | Meaning |
|---|---|---|
| `@self` | `@self` | The user SID of the token running `sd`. |
| `@owner` | `@owner` | A placeholder that the access check substitutes with the file's own owner. |
| A well-known label | `Everyone`, `Administrators`, `LocalSystem` | A named built-in principal. |
| A two-letter alias | `WD`, `BA`, `SY` | The short alias for a well-known principal. |
| A raw SID | `S-1-5-32-544` | Any SID, written out in full. |

See [SIDs](/peios/security-fundamentals/identity/sids.md) for what these are.

## Naming permissions

Wherever a subcommand takes `PERMS`, several notations are accepted, and may be mixed:

| Form | Example | Meaning |
|---|---|---|
| Single letters | `rwx`, `r`, `m` | `r` read, `w` write, `x` execute, `d` delete, `m` modify, `f` full, `c` change-permissions, `o` take-ownership. |
| Words | `read,write`, `modify` | The same set, spelled out. |
| Fine-grained names | `read-data,append,traverse` | Individual low-level rights, for precise rules. |
| Raw hex | `0x1F01FF` | An access mask written directly. |

Run letters together (`rwx`) or separate names with commas (`read,write,execute`).

## Inspecting

### `sd show`

Prints the descriptor on a path — the owner, the group, the DACL, the SACL.

```
$ sd show ./report.txt
```

| Flag | Effect |
|---|---|
| `--sddl` | Render the descriptor as an SDDL string. |
| `--raw` | Render SIDs in raw `S-1-…` form only. |
| `--label` | Render SIDs as their labels where known. |
| `--all` | Verbose — decode every flag and show raw masks alongside. |
| `--json` | Emit JSON. |

### `sd check`

Simulates an [access decision](/peios/security-fundamentals/access-decisions/overview.md): "would this access be allowed?" — without performing it.

```
$ sd check ./report.txt write
$ sd check ./report.txt read --pid 4821 --explain
```

| Argument / flag | Effect |
|---|---|
| `PERMS` | The access to test for. |
| `--pid PID` | Check against process `PID`'s token instead of your own. |
| `--explain` | Show why the decision came out as it did — the rule-by-rule walk. |

`sd check` is the first thing to reach for when an access is denied and you do not know why. `--explain` walks the descriptor the same way the kernel does.

## Changing the DACL

The DACL is the list of allow and deny rules. These three subcommands edit it.

### `sd allow` and `sd deny`

Add an allow (or deny) rule for one or more `PRINCIPAL:PERMS` pairs.

```
$ sd allow ./report.txt alice:read bob:rw
$ sd deny  ./report.txt Everyone:write
```

| Flag | Effect |
|---|---|
| `--flags LIST` | ACE inheritance flags — `CI` container-inherit, `OI` object-inherit, `NP` no-propagate, `IO` inherit-only; `none` clears them. |
| `--if EXPR` | Make it a [conditional rule](/peios/security-fundamentals/security-descriptors/conditional-aces.md), applied only when `EXPR` is true. |
| `--replace` | Drop any existing rules for this principal and kind first, instead of appending. |
| `--recursive`, `-r` | Apply to every descendant of the path. |

Remember that a **deny** rule, when it matches, wins over any allow — see [DACL evaluation](/peios/security-fundamentals/security-descriptors/dacl-evaluation.md).

### `sd remove`

Drops every DACL rule — allow and deny — for the named principals.

```
$ sd remove ./report.txt bob carol
```

| Flag | Effect |
|---|---|
| `--allow-empty` | Permit the result to be a present-but-empty DACL, which denies everyone. Without this, `sd` refuses to produce one. |
| `--recursive`, `-r` | Apply to every descendant. |

## Auditing

### `sd audit` and `sd unaudit`

The SACL holds audit rules — see [The SACL](/peios/security-fundamentals/security-descriptors/the-sacl.md). `sd audit` adds one; `sd unaudit` drops every SACL rule for the named principals.

```
$ sd audit ./secrets.db Everyone:write:failure
```

An audit spec is `PRINCIPAL:PERMS:WHEN`, where `WHEN` is `success`, `failure`, or `both` — which outcomes to log. `sd audit` takes the same `--flags`, `--if`, `--replace`, and `-r` options as `sd allow`.

## Ownership

### `sd owner` and `sd group`

Set the descriptor's owner or group SID.

```
$ sd owner ./report.txt alice
$ sd group ./report.txt Administrators
```

Both take a single `PRINCIPAL` and accept `-r`. Changing an owner is itself an access-controlled act — see [Ownership](/peios/security-fundamentals/security-descriptors/ownership.md).

## Integrity

### `sd integrity`

Sets the file's mandatory integrity label.

```
$ sd integrity ./report.txt high
```

The level is one of `untrusted`, `low`, `medium`, `medium-plus`, `high`, `system`, `protected`. The standard catalog is five levels (`untrusted`, `low`, `medium`, `high`, `system`); `medium-plus` (RID 8448) and `protected` (RID 20480) are non-standard Windows-compatibility levels — the kernel compares any `S-1-16-<rid>` numerically.

| Flag | Effect |
|---|---|
| `--policy BITS` | The label's policy bits, comma-separated: `NW` no write-up, `NR` no read-up, `NX` no execute-up. |
| `--recursive`, `-r` | Apply to every descendant. |

For what the label does, see [Mandatory integrity control](/peios/security-fundamentals/access-decisions/mandatory-integrity-control.md).

## Inheritance

### `sd inherit`

Turns inheritance protection on or off — the `+` mark `ls -l` shows.

```
$ sd inherit off ./report.txt    # lock the file; stop inheriting
$ sd inherit on  ./report.txt    # let it inherit from its parent again
```

`inherit on` lets the file inherit rules from its parent directory. `inherit off` protects the file — it keeps its current rules and stops tracking the parent.

| Flag | Effect |
|---|---|
| `--strip-inherited` | When turning protection off, also drop the inherited rules already on the file. |
| `--recursive`, `-r` | Apply to every descendant. |

### `sd reset`

Drops the file's own explicit rules and rebuilds its DACL purely from what the parent directory hands down — returning the file to "inherits everything".

### `sd propagate`

Pushes this directory's inheritable rules down into its descendants, refreshing what they inherit. Use it after changing a directory's rules so the children pick up the change.

See [Inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) for the full model.

## Replacing the whole descriptor

### `sd set`

Replaces the entire descriptor in one step.

```
$ sd set ./report.txt 'O:BAG:BAD:P(A;;FA;;;BA)(A;;0x1200a9;;;BU)'
```

| Argument / flag | Effect |
|---|---|
| `SDDL` | The new descriptor as an SDDL string. `-` reads it from standard input. |
| `--binary FILE` | Instead of SDDL, read the raw descriptor bytes from `FILE` (`-` for standard input). |
| `--components LIST` | Override which parts of the descriptor (owner, group, DACL, SACL) the operation writes. |

## Common flags

These apply across the subcommands:

| Flag | Effect |
|---|---|
| `--recursive`, `-r` | Apply the change to every descendant of the path. |
| `--no-follow-symlinks`, `-P` | Operate on a symbolic link itself, not the file it points to. |
| `--json` | Emit JSON instead of human-readable output. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded — or, for `sd check`, the access would be **allowed**. |
| non-zero | The operation failed, the path was unreachable, or — for `sd check` — the access would be **denied**. |

---

# Listing and paths

_Peios / Using Peios / Peiosutils / Listing and paths_

> The commands that show you what a directory contains and that inspect, resolve, and manipulate path names — ls, stat, pwd, readlink, realpath, and the rest.

Before you can do anything with a file you usually have to *find* it — see what a directory holds, learn one file's details, turn a relative name into an absolute one. This topic covers the commands for that: listing directories, inspecting a single file, and resolving path names.

This page names every command in the topic, says in one line what each is for, and points you at the page that covers it in full.

## The commands

| Command | Purpose |
|---|---|
| [`ls`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) | List the contents of a directory. The everyday command for "what's in here?". |
| [`dir` and `vdir`](/peios/using-peios/peiosutils/listing-and-paths/dir-and-vdir.md) | `ls` with a fixed output style. `dir` lists in columns; `vdir` lists in long format. |
| [`stat`](/peios/using-peios/peiosutils/listing-and-paths/stat.md) | Show the detailed status of one file — its type, size, timestamps, and raw inode metadata. |
| [`pwd`](/peios/using-peios/peiosutils/listing-and-paths/pwd.md) | Print the directory you are currently working in. |
| [`readlink`](/peios/using-peios/peiosutils/listing-and-paths/readlink.md) | Print where a symbolic link points. |
| [`realpath`](/peios/using-peios/peiosutils/listing-and-paths/realpath.md) | Resolve a path to its canonical, absolute form — every symlink followed, every `.` and `..` removed. |
| [`basename`](/peios/using-peios/peiosutils/listing-and-paths/basename.md) | Strip the directory part off a path, leaving just the final name. |
| [`dirname`](/peios/using-peios/peiosutils/listing-and-paths/dirname.md) | The opposite of `basename` — strip the final name, leaving the directory. |
| [`pathchk`](/peios/using-peios/peiosutils/listing-and-paths/pathchk.md) | Check whether a path name is valid and portable before you try to create it. |
| [`dircolors`](/peios/using-peios/peiosutils/listing-and-paths/dircolors.md) | Produce the colour settings that `ls` uses when it colours its output. |

## These commands and file security

Most commands in this topic work purely on path strings — they never touch the file the path names. `basename`, `dirname`, `realpath`, and `pathchk` are all string operations.

Two report on real files, and they relate to file security differently:

- **`ls -l` surfaces security-descriptor information directly.** On Peios a file's access is governed by its **security descriptor** — the record of who owns the file and who may do what to it. The long listing shows the owner and the parts of the descriptor that fit in a one-line-per-file format.
- **`stat` is a low-level diagnostic.** It dumps the raw contents of a file's inode. The inode carries some numeric fields — an owner id, a group id, a mode value — that the Peios access model does **not** consult. `stat` reports them because it reports the inode verbatim, but they are not the file's access policy.

Neither command prints the full security descriptor; inspecting that is a job for the dedicated security tooling. If a line of `ls -l` looks unfamiliar, or you want to know why `stat` shows fields that "don't count", the explanation is in [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md).

## Where to start

If you want the everyday "what's in this directory" command and the meaning of every column it prints, read [`ls`](/peios/using-peios/peiosutils/listing-and-paths/ls.md).

If you need the full detail of one specific file, read [`stat`](/peios/using-peios/peiosutils/listing-and-paths/stat.md).

If you are writing a script and need to take a path apart or canonicalise it, the string commands — [`basename`](/peios/using-peios/peiosutils/listing-and-paths/basename.md), [`dirname`](/peios/using-peios/peiosutils/listing-and-paths/dirname.md), [`realpath`](/peios/using-peios/peiosutils/listing-and-paths/realpath.md) — are what you want.

---

# ls

_Peios / Using Peios / Peiosutils / Listing and paths_

> List the contents of a directory — the default listing, the Peios long format and its security-descriptor columns, and the sorting and formatting options.

`ls` lists the contents of a directory. It is the command you use to answer "what is in here?" — and, with the right options, "who owns these files, how big are they, and when were they last changed?".

```
ls [options] [file...]
```

Each `file` is a directory to list the contents of, or a single file to report on. With no `file` argument, `ls` lists the directory you are currently in.

```
$ ls
build.sh  notes.txt  projects
```

## The default listing

Run on its own, `ls` prints the names of the entries in a directory and nothing else. Three rules shape that default:

- **Hidden entries are skipped.** A name beginning with a dot (`.config`, `.cache`) is hidden. `ls` leaves hidden entries out unless you ask for them with [`-a` or `-A`](#choosing-what-to-list).
- **Entries are sorted by name.** Alphabetical order, case-sensitive. Other [sort orders](#sorting) are available.
- **The layout adapts to where output goes.** When `ls` is writing to a terminal it arranges names into columns sized to the terminal width. When output is redirected to a file or a pipe it prints one name per line, so the result is easy for another program to read.

Everything past this default is opt-in. The rest of this page is the options, grouped by what they do.

## The long format

`-l` (or `--long`) switches `ls` into the **long format**: one entry per line, each with its mode, owner, size, modification time, and name.

```
$ ls -l
total 28
d-- S-1-5-21-9f3a-1c4e-7b20-1001   4096 May 14 09:12 projects
-x- S-1-5-21-9f3a-1c4e-7b20-1001   8344 May 16 17:40 build.sh
--- S-1-5-21-9f3a-1c4e-7b20-1001    219 May 15 11:03 notes.txt
--+ S-1-5-18                      12048 May 10 22:15 policy.db
```

The `total` line at the top reports the combined disk space used by the listed files, measured in blocks.

Each entry line has five columns: **mode**, **owner**, **size**, **modification time**, **name**. There is no permission-bits column and no group column — the reason is below.

### The mode column

The mode column is three characters: `[type][executable][protected]`.

The **first character** is the file's type:

| Character | Type |
|---|---|
| `-` | Regular file |
| `d` | Directory |
| `l` | Symbolic link |
| `p` | Named pipe (FIFO) |
| `s` | Socket |
| `c` | Character device |
| `b` | Block device |

The **second character** is the executable mark. It is `x` when the entry is a regular file that is marked as an executable, and `-` otherwise (it is always `-` for directories, links, and other non-regular entries).

The executable mark describes the *file*, not your relationship to it. `x` means "this file is runnable code." It does **not** mean that you, specifically, are allowed to run it — whether a given principal may execute a file is a separate access decision made against the file's security descriptor. A file can show `x` and still be one you cannot run, and the reverse. See [Access decisions](/peios/security-fundamentals/access-decisions/overview.md) for how execution is actually authorised.

The **third character** is the protected mark. It is `+` when the file's access control is **inheritance-protected**, and `-` otherwise.

A `-` here means the file's access control is inherited from the directory it lives in: it tracks its parent. A `+` means the file's access control has been set deliberately and locked, so it no longer tracks the parent directory. The `+` is the signal that someone has tailored this file's security on purpose. See [Inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md).

So in the example above: `projects` is a directory (`d--`); `build.sh` is an executable file (`-x-`); `notes.txt` is an ordinary file inheriting its security from its directory (`---`); and `policy.db` is an ordinary file whose access control has been protected (`--+`).

### Why there are no permission bits

A long listing does not carry a fixed block of permission characters. On Peios, what a principal may do to a file is decided by the file's **security descriptor** — a record that can contain any number of entries granting or denying specific rights to specific principals. There is no small, fixed set of bits that captures that, so `ls -l` does not pretend there is.

The mode column tells you the file's *type* and two *facts about it* (is it executable, is its security protected). To see *who may do what*, look at the security descriptor itself with [`stat`](/peios/using-peios/peiosutils/listing-and-paths/stat.md) or the dedicated security tooling. The concepts are in [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md).

### The owner column

The owner column is the **SID** of the file's owner — shown in full, in the `S-1-…` form. `ls` does not translate SIDs into account names; it prints the identifier exactly as the security descriptor stores it. A well-known owner such as the system itself appears as its fixed SID (`S-1-5-18`); an ordinary account appears as a longer domain-style SID.

For what a SID is and how to read one, see [SIDs](/peios/security-fundamentals/identity/sids.md). For what "owner" means and what it grants, see [Ownership](/peios/security-fundamentals/security-descriptors/ownership.md).

If `ls` cannot read a file's descriptor — for a dangling symlink, say — the owner column shows `?` and the mode column shows the type character followed by `??`.

## Choosing what to list

By default `ls` shows non-hidden entries, and for a directory argument it shows the directory's *contents*. These options change that.

| Option | Effect |
|---|---|
| `-a`, `--all` | Show hidden entries too, including `.` (the directory itself) and `..` (its parent). |
| `-A`, `--almost-all` | Show hidden entries, but leave out `.` and `..`. |
| `-d`, `--directory` | List a directory as an entry in its own right, instead of listing its contents. Useful with `-l` to see a directory's own owner and mode. |
| `-R`, `--recursive` | Descend into every subdirectory and list it too. |
| `-B`, `--ignore-backups` | Skip entries whose name ends in `~`. |
| `--ignore=PATTERN` | Skip entries whose name matches the shell pattern. May be given more than once. |
| `--hide=PATTERN` | Like `--ignore`, but overridden by `-a` or `-A` — so a hide rule can be cancelled by asking to see everything. |

## Sorting

`ls` sorts by name by default. These options change the sort key; `-r` reverses whatever order is in effect.

| Option | Sort order |
|---|---|
| `-t` | By modification time, newest first. |
| `-S` | By size, largest first. |
| `-X` | By file extension, alphabetically. |
| `-v` | By version: runs of digits in the name sort numerically, so `f2` comes before `f10`. |
| `-U` | No sorting at all — entries appear in the order the directory stores them. Fast for very large directories. |
| `-r`, `--reverse` | Reverse the current sort. |
| `--sort=WORD` | Choose the sort key by name: `none`, `time`, `size`, `extension`, `version`, or `width`. |
| `--group-directories-first` | List directories before other entries, with each group sorted normally. |

## Output layout

When several entries fit on a line, `ls` has to decide how to arrange them.

| Option | Layout |
|---|---|
| `-C` | Columns, filled top-to-bottom. The default when writing to a terminal. |
| `-x` | Columns, filled left-to-right (across the rows) instead. |
| `-1` | One entry per line. The default when output is not a terminal. |
| `-m` | All entries on as few lines as possible, separated by commas. |
| `-l`, `--long` | The long format described above. |
| `--format=WORD` | Choose the layout by name: `across`, `commas`, `horizontal`, `long`, `single-column`, or `vertical`. |
| `-w`, `--width=COLS` | Assume the terminal is `COLS` columns wide instead of detecting it. `0` means unlimited. |

## File-type indicators and colour

These options make a listing easier to scan by marking entries with a symbol or a colour.

| Option | Effect |
|---|---|
| `-F`, `--classify` | Append a type symbol to each name: `/` for a directory, `@` for a symlink, `\|` for a FIFO, `=` for a socket, and `*` for an executable file. |
| `--file-type` | The same, but without the `*` on executables. |
| `-p` | Append only the `/` on directories. |
| `--indicator-style=WORD` | Choose the indicator set: `none`, `slash`, `file-type`, or `classify`. |
| `--color[=WHEN]` | Colour entries by type. `WHEN` is `auto` (colour only when writing to a terminal — the usual choice), `always`, or `never`. |

The colours `ls` uses are configurable. [`dircolors`](/peios/using-peios/peiosutils/listing-and-paths/dircolors.md) generates the colour settings and explains how to install them.

## File sizes

In the long format, and with `-s`, sizes are printed in bytes by default. These options rescale them.

| Option | Effect |
|---|---|
| `-h`, `--human-readable` | Print sizes with a unit suffix — `4.0K`, `234M`, `56G` — using powers of 1024. |
| `--si` | Like `-h`, but using powers of 1000, so the suffixes are decimal. |
| `-s`, `--size` | Print the disk space each entry occupies, in blocks, before its name. |
| `-k`, `--kibibytes` | Use 1024-byte blocks for the size figures and directory totals. |
| `--block-size=SIZE` | Scale every size by `SIZE` (for example `--block-size=1M` to count in megabytes). |

## Timestamps

The long format shows the modification time by default. These options change which timestamp is shown — and, when sorting by time, which one is sorted on.

| Option | Timestamp |
|---|---|
| `-u` | Access time — when the file was last read. |
| `-c` | Status-change time — when the file's metadata last changed. |
| `--time=WORD` | Choose the timestamp by name: `atime`/`access`, `ctime`/`status`, `mtime`/`modification`, or `birth`/`creation`. |
| `--time-style=STYLE` | Choose the date format: `full-iso`, `long-iso`, `iso`, `locale`, or `+FORMAT` for a custom layout. |
| `--full-time` | Shorthand for the long format with `--time-style=full-iso` — a complete, unabbreviated timestamp. |

## Symbolic links

By default `ls` reports on a symbolic link itself — its type is `l`, its size is the length of the link text. These options make it report on the link's *target* instead.

| Option | Effect |
|---|---|
| `-L`, `--dereference` | Always report on the file a link points to, not the link. |
| `-H`, `--dereference-command-line` | Dereference only the links named directly on the command line, not links found inside a listed directory. |
| `--dereference-command-line-symlink-to-dir` | Dereference a command-line link only when it points to a directory. |

[`readlink`](/peios/using-peios/peiosutils/listing-and-paths/readlink.md) and [`realpath`](/peios/using-peios/peiosutils/listing-and-paths/realpath.md) are the dedicated tools for inspecting and resolving links.

## Other options

The long-tail options, each in one line.

| Option | Effect |
|---|---|
| `-i`, `--inode` | Print each entry's index number (its inode number). |
| `-Z`, `--context` | Print each entry's security context. Available only when the build enables it. |
| `-b`, `--escape` | Print non-printable characters in names using C-style backslash escapes. |
| `-q`, `--hide-control-chars` | Replace non-printable characters in names with `?`. The default when writing to a terminal. |
| `--show-control-chars` | Print names verbatim, control characters and all. |
| `-N`, `--literal` | Print names exactly, with no quoting. |
| `-Q`, `--quote-name` | Wrap each name in double quotes. |
| `--quoting-style=WORD` | Choose the quoting scheme: `literal`, `shell`, `shell-always`, `shell-escape`, `c`, `escape`, and others. |
| `-T`, `--tabsize=COLS` | Assume tab stops every `COLS` columns when laying out output. |
| `-f` | List everything, unsorted, including hidden entries — equivalent to `-aU`. Also turns colour off unless `--color` is given explicitly. |
| `--hyperlink[=WHEN]` | Emit terminal hyperlinks for file names, so a capable terminal can make them clickable. |
| `-D`, `--dired` | Emit extra position markers designed for the Emacs *dired* editing mode. |
| `--zero` | End each line with a NUL character instead of a newline, and list one entry per line. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success — everything requested was listed. |
| `1` | A minor problem — for example, a named file could not be accessed. The rest of the listing still printed. |
| `2` | A serious problem — for example, a directory could not be read, or an option was invalid. |

---

# dir and vdir

_Peios / Using Peios / Peiosutils / Listing and paths_

> Two commands that list directory contents with a fixed output style — dir always lists in columns, vdir always lists in long format.

`dir` and `vdir` list the contents of a directory. They are [`ls`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) with one decision already made for you: the output style is fixed instead of adapting to where the output is going.

```
dir [options] [file...]
vdir [options] [file...]
```

## What they fix

`ls` chooses its layout based on whether it is writing to a terminal — columns for a terminal, one name per line for a pipe or file. `dir` and `vdir` each pick one layout and always use it:

| Command | Layout | Equivalent `ls` |
|---|---|---|
| `dir` | Columns, regardless of where output goes. | `ls -C` |
| `vdir` | Long format, regardless of where output goes. | `ls -l` |

Both also default to a quoting style that escapes unusual characters in names without wrapping every name in quotes.

That is the *only* difference. The fixed style is just a default — pass an explicit format option (`-1`, `-m`, `-l`, `-C`, …) and it overrides the built-in choice, so `dir -l` produces a long listing and `vdir -C` produces columns.

## Everything else is `ls`

`dir` and `vdir` accept every option `ls` accepts and behave identically in every other respect — sorting, filtering, the long-format columns, colour, indicators, sizes, timestamps. `vdir`'s long format is the same Peios long format documented for `ls`, with the `[type][executable][protected]` mode column and the owner SID.

For the full option set and the meaning of every long-format column, see [`ls`](/peios/using-peios/peiosutils/listing-and-paths/ls.md). This page exists only to explain how `dir` and `vdir` differ from it — and the answer is "they pin the output style."

## Exit status

The same as `ls`:

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A minor problem — a named file could not be accessed. |
| `2` | A serious problem — a directory could not be read, or an option was invalid. |

---

# stat

_Peios / Using Peios / Peiosutils / Listing and paths_

> Display the detailed status of a file or a file system — type, size, timestamps, and raw inode metadata — with an optional custom output format.

`stat` displays the detailed status of a file. Where [`ls`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) gives you a line per file, `stat` gives you everything the system records about *one* file: its type, its size, its timestamps, and the low-level metadata stored in its inode.

```
stat [options] file...
```

`stat` is a diagnostic tool. It dumps what a file's inode physically contains, verbatim. That makes it the right command for "tell me exactly what is recorded about this file" — and it means some of what it prints needs a word of explanation, below.

## The default output

With no options, `stat` prints a labelled, multi-line block per file:

```
$ stat notes.txt
  File: notes.txt
  Size: 219             Blocks: 8          IO Block: 4096   regular file
Device: 8,2     Inode: 1572931     Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/    jack)   Gid: ( 1000/    jack)
Access: 2026-05-15 11:03:42.000000000 +0000
Modify: 2026-05-15 11:03:42.000000000 +0000
Change: 2026-05-15 11:03:18.000000000 +0000
 Birth: 2026-05-15 11:03:18.000000000 +0000
```

Reading it line by line:

- **File** — the name, and for a symbolic link, what it points to.
- **Size / Blocks / IO Block / type** — the size in bytes, the number of allocated disk blocks, the preferred I/O block size, and the file type in words.
- **Device / Inode / Links** — the device the file lives on, its inode number (its unique index within that device), and the number of hard links to it.
- **Access / Uid / Gid** — a numeric owner id, a group id, and a mode value. See the caveat below — these are not the access policy.
- **Access / Modify / Change / Birth** — four timestamps: when the file was last read, last written, last had its metadata changed, and first created.

## The Uid, Gid, and mode fields are decorative

The `Access: (0644/-rw-r--r--)  Uid: (…)  Gid: (…)` line needs care.

A file's inode carries a numeric owner id, a numeric group id, and a mode value, and `stat` reports them because `stat` reports the inode verbatim. But **the Peios access model does not consult these fields.** What a principal may do to a file is decided by the file's **security descriptor** — see [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md). The numbers on the `Uid`/`Gid`/mode line are inert metadata: they are stored, they are reported, and they have no effect on any access decision.

Do not read that line as a permission summary. A file showing `0644` is not "world-readable" in any meaningful sense — whether anyone can read it depends entirely on its security descriptor. `stat` shows these fields for completeness as a low-level diagnostic; it does not claim they mean anything.

To see the parts of a file's security that *do* count, use [`ls -l`](/peios/using-peios/peiosutils/listing-and-paths/ls.md), which shows the owner SID and the mode column built from the security descriptor.

## Custom output formats

Two options replace the default block with output you control.

| Option | Effect |
|---|---|
| `-c FORMAT`, `--format=FORMAT` | Print `FORMAT` for each file, substituting `%` directives. A newline is added after each file. |
| `--printf=FORMAT` | Like `--format`, but interpret backslash escapes (`\n`, `\t`) in `FORMAT` and add no trailing newline. Include `\n` yourself if you want one. |

```
$ stat --format='%n is %s bytes' notes.txt
notes.txt is 219 bytes
```

### File directives

Used in the format string when stating files (the default mode):

| Directive | Substitutes |
|---|---|
| `%n` | File name. |
| `%N` | Quoted file name; for a symlink, with the target shown. |
| `%F` | File type in words (`regular file`, `directory`, …). |
| `%s` | Total size, in bytes. |
| `%b` | Number of allocated blocks. |
| `%B` | Size in bytes of each block counted by `%b`. |
| `%o` | Preferred I/O transfer block size. |
| `%i` | Inode number. |
| `%h` | Number of hard links. |
| `%d` / `%D` | Device number, in decimal / in hexadecimal. |
| `%t` / `%T` | For a device file, the major / minor device type, in hexadecimal. |
| `%m` | Mount point of the file system the file is on. |
| `%f` | Raw mode value, in hexadecimal. |
| `%x` / `%X` | Last access time — human-readable / seconds since the epoch. |
| `%y` / `%Y` | Last modification time — human-readable / seconds since the epoch. |
| `%z` / `%Z` | Last status-change time — human-readable / seconds since the epoch. |
| `%w` / `%W` | File creation (birth) time — human-readable / seconds since the epoch. `-` or `0` if unknown. |

The directives `%a` and `%A` (the mode in octal and in symbolic form) and `%u`, `%U`, `%g`, `%G` (the owner and group, numeric and by name) report the decorative inode fields described above. They are available for completeness; they are not the file's access policy.

The `%C` directive (security context) is inactive — it produces no meaningful value on a standard Peios system.

### File-system directives

With `-f`, `stat` reports on the *file system* a file lives on rather than the file itself, and the format string uses a different directive set:

| Directive | Substitutes |
|---|---|
| `%n` | File name. |
| `%i` | File-system ID, in hexadecimal. |
| `%t` / `%T` | File-system type — in hexadecimal / in words. |
| `%l` | Maximum length of a file name. |
| `%s` | Block size, for fast transfers. |
| `%S` | Fundamental block size, used for the block counts. |
| `%b` | Total data blocks in the file system. |
| `%f` | Free blocks. |
| `%a` | Free blocks available to an ordinary principal. |
| `%c` | Total inodes (file nodes). |
| `%d` | Free inodes. |

## Options

| Option | Effect |
|---|---|
| `-f`, `--file-system` | Report on the file system containing each file, instead of the file. |
| `-L`, `--dereference` | Follow symbolic links — report on the link's target rather than the link itself. |
| `-t`, `--terse` | Print the information on a single line, as bare values with no labels. Useful for scripts. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was stated successfully. |
| `1` | A file could not be stated, or an option or format directive was invalid. |

---

# pwd

_Peios / Using Peios / Peiosutils / Listing and paths_

> Print the absolute path of the directory you are currently working in.

`pwd` — "print working directory" — prints the absolute path of the directory you are currently in.

```
pwd [options]
```

```
$ pwd
/home/jack/projects
```

Every process has a working directory: the directory that relative path names are interpreted against. `pwd` tells you what yours is.

## Physical and logical paths

There are two honest answers to "where am I", and they differ only when symbolic links are involved.

Suppose `/home/jack/work` is a symbolic link pointing at `/data/projects/jack`, and you move into it. The **physical** path — the real location, with every link resolved — is `/data/projects/jack`. The **logical** path — the route you took to get there — is `/home/jack/work`.

| Option | Path printed |
|---|---|
| `-P`, `--physical` | The physical path: every symbolic link resolved to its target. |
| `-L`, `--logical` | The logical path: the route recorded as you navigated, taken from the `PWD` environment variable when it is accurate. |

By default `pwd` prints the **physical** path — `-P` is the default, and naming it explicitly just makes that choice visible. Use `-L` when you want the path as you navigated it, links and all.

If `-L` is requested but the recorded `PWD` value is missing or does not actually match the current directory, `pwd` falls back to the physical path rather than print something wrong.

## A note on shells

Many command shells provide their own built-in `pwd`, and the shell's version is what runs when you type `pwd` at a prompt. The two behave the same for everyday use. To be certain you are running this command rather than the shell built-in, invoke it by its full path.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The working directory was printed. |
| `1` | The working directory could not be determined or could not be printed. |

---

# readlink

_Peios / Using Peios / Peiosutils / Listing and paths_

> Print the target of a symbolic link, or canonicalise a path by resolving every link in it.

`readlink` prints where a symbolic link points.

```
readlink [options] file...
```

A symbolic link is a file whose contents are another path. `readlink` reads that path and prints it:

```
$ readlink /home/jack/work
/data/projects/jack
```

By default `readlink` resolves exactly one level: it prints the link's immediate target, whether or not that target is itself a link, and whether or not it exists. If the named file is not a symbolic link, `readlink` prints nothing and reports failure.

## Canonicalising a whole path

The canonicalise options change `readlink` from "read this one link" into "resolve this entire path to its real, absolute form" — following every symbolic link in every component, collapsing `.` and `..`. The three differ only in how strict they are about components existing:

| Option | Resolves | Existence requirement |
|---|---|---|
| `-f`, `--canonicalize` | Every link in the path. | Every component except the last must exist. |
| `-e`, `--canonicalize-existing` | Every link in the path. | Every component must exist. |
| `-m`, `--canonicalize-missing` | Every link in the path. | No component need exist. |

With any of these, `readlink` succeeds on an ordinary (non-link) file too — it simply returns the canonical path. [`realpath`](/peios/using-peios/peiosutils/listing-and-paths/realpath.md) is the dedicated command for this canonicalising job and has more options for it; the canonicalise flags here exist so `readlink` can do it without a second tool.

## Output and error control

| Option | Effect |
|---|---|
| `-n`, `--no-newline` | Do not print the trailing newline. Ignored, with a warning, when more than one file is given. |
| `-z`, `--zero` | End each output line with a NUL character instead of a newline. |
| `-q`, `--quiet` / `-s`, `--silent` | Suppress most error messages. This is the default. |
| `-v`, `--verbose` | Report error messages that the quiet default would suppress. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every named file was resolved and printed. |
| `1` | A file was not a symbolic link, or a path could not be resolved under the chosen strictness. |

---

# realpath

_Peios / Using Peios / Peiosutils / Listing and paths_

> Resolve a path to its canonical, absolute form — every symbolic link followed and every . and .. component removed.

`realpath` resolves a path to its **canonical** form: absolute, with every symbolic link followed and every `.` and `..` component removed. Given any path, it tells you the one true location it refers to.

```
realpath [options] file...
```

```
$ realpath ../work/./notes.txt
/data/projects/jack/notes.txt
```

A single file can be named many ways — through links, through relative paths, with redundant components. `realpath` reduces all of them to the single canonical path, which makes it the right tool for comparing two paths, or for turning a path a user typed into one a script can rely on.

## How much must exist

By default `realpath` requires every component of the path *except the last* to exist. These options change that requirement:

| Option | Existence requirement |
|---|---|
| (default) | Every component except the last must exist. |
| `-e`, `--canonicalize-existing` | Every component must exist — including the final one. |
| `-m`, `--canonicalize-missing` | No component need exist. The path is canonicalised purely as text where it cannot be walked. |

## How symbolic links are handled

| Option | Effect |
|---|---|
| `-P`, `--physical` | Resolve each symbolic link as it is encountered. This is the default. |
| `-L`, `--logical` | Resolve `..` components *before* resolving the symbolic links they follow. |
| `-s`, `--strip`, `--no-symlinks` | Do not resolve symbolic links at all — only remove `.` and `..` components. The result is canonical as text, but a link in the path is left as-is. |

## Output options

| Option | Effect |
|---|---|
| `--relative-to=DIR` | Print the result as a path relative to `DIR` instead of as an absolute path. |
| `--relative-base=DIR` | Print an absolute path, unless the result lies inside `DIR`, in which case print it relative to `DIR`. |
| `-z`, `--zero` | End each output line with a NUL character instead of a newline. |
| `-q`, `--quiet` | Do not print a warning when a path is invalid. The exit status still reports the failure. |

## `realpath` and `readlink`

[`readlink`](/peios/using-peios/peiosutils/listing-and-paths/readlink.md) with `-f`/`-e`/`-m` does the same canonicalising job. The difference is focus: `readlink` is primarily for reading a single link's target, with canonicalising as an extra; `realpath` is built for canonicalising and carries the richer option set — relative output, the symlink-handling modes, the strip-only mode. Use `realpath` when canonicalising is the actual goal.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every path was resolved and printed. |
| `1` | A path could not be resolved under the chosen options. |

---

# basename

_Peios / Using Peios / Peiosutils / Listing and paths_

> Strip the directory part off a path, leaving just the final name — and optionally remove a trailing suffix.

`basename` takes a path and prints just its final component — the file name, with all the leading directories removed.

```
basename name [suffix]
basename [options] name...
```

```
$ basename /home/jack/projects/notes.txt
notes.txt
```

It is a pure text operation: `basename` never looks at the file system. It works on the string you give it, so the path need not exist.

## Removing a suffix

Give a second argument and `basename` also strips that suffix from the end of the name — handy for turning a file name into a bare stem:

```
$ basename /home/jack/projects/notes.txt .txt
notes
```

The suffix is only removed if it is actually there, and never if it would leave an empty string.

## Options

`basename` takes one name by default. The options let it process several at once.

| Option | Effect |
|---|---|
| `-a`, `--multiple` | Treat every argument as a name to process, instead of treating the second argument as a suffix. Required to pass more than one name. |
| `-s`, `--suffix=SUFFIX` | Remove `SUFFIX` from each name. Implies `-a`, so it applies to every name given. |
| `-z`, `--zero` | End each output line with a NUL character instead of a newline. |

```
$ basename -s .txt notes.txt readme.txt changelog.txt
notes
readme
changelog
```

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A usage error — a missing or extra operand. |

---

# dirname

_Peios / Using Peios / Peiosutils / Listing and paths_

> Strip the final component off a path, leaving the directory part.

`dirname` takes a path and prints everything *except* its final component — the directory the name lives in. It is the counterpart of [`basename`](/peios/using-peios/peiosutils/listing-and-paths/basename.md).

```
dirname [options] name...
```

```
$ dirname /home/jack/projects/notes.txt
/home/jack/projects
```

Like `basename`, it is a pure text operation — `dirname` never touches the file system, so the path need not exist.

## How it handles edge cases

- If the name has no `/` in it at all, there is no directory part, so `dirname` prints `.` — the current directory.
- Trailing slashes on the name are ignored before the final component is removed.

```
$ dirname notes.txt
.
$ dirname /home/jack/projects/
/home
```

## Multiple names

`dirname` accepts any number of names and prints the directory part of each on its own line:

```
$ dirname /etc/hosts /var/log/messages report.txt
/etc
/var/log
.
```

## Options

| Option | Effect |
|---|---|
| `-z`, `--zero` | End each output line with a NUL character instead of a newline. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A usage error — no name was given. |

---

# pathchk

_Peios / Using Peios / Peiosutils / Listing and paths_

> Check whether path names are valid and usable before you try to create the files they name.

`pathchk` checks whether a path name is valid and usable. It tells you, *before* you try to create a file, whether the name would be rejected — because it is too long, contains an unusable component, or could not be reached.

```
pathchk [options] name...
```

```
$ pathchk /home/jack/projects/notes.txt
$ echo $?
0
```

`pathchk` prints nothing when a name is fine; it prints a diagnostic and fails when a name is not. It is meant for scripts that build up path names and want to fail early, with a clear message, instead of discovering the problem halfway through an operation.

Like the other name commands in this topic, `pathchk` does not require the file to exist — it is checking the *name*, not looking for the file.

## What the default check covers

With no options, for each name `pathchk` checks:

- that the name is not empty;
- that no part of the name exceeds the length limits of the file systems the path would actually touch;
- that the leading directories of the name can be searched.

## Stricter checks

The options replace "valid on *this* system" with "valid across a wide range of systems" — useful when a name has to work somewhere other than where you are checking it.

| Option | Adds |
|---|---|
| `-p` | Check against a fixed, conservative set of length limits instead of the local file system's, and flag characters that are not broadly portable. |
| `-P` | Reject an empty name, and reject any component that begins with `-`. |
| `--portability` | Apply both `-p` and `-P` — the strictest check. |

A name with a leading `-` on a component is worth catching: many commands would read it as an option rather than a file name. `-P` flags exactly that class of trouble.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every name passed every requested check. |
| `1` | At least one name failed. A diagnostic naming the problem was printed. |

---

# dircolors

_Peios / Using Peios / Peiosutils / Listing and paths_

> Produce the shell commands that set LS_COLORS — the variable that controls how ls colours its output.

When [`ls`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) colours its output, it reads the colours from an environment variable named `LS_COLORS`. `dircolors` is the command that produces the value of that variable.

```
dircolors [options] [file]
```

`dircolors` does not set the variable itself — a command cannot change its parent shell's environment. Instead it *prints shell commands* that, when run by the shell, set `LS_COLORS`. The usual way to use it is to have the shell evaluate that output:

```
eval "$(dircolors)"
```

Put that line in a shell startup file and every `ls --color` afterwards picks up the colours. With no arguments, `dircolors` uses a built-in default colour scheme.

## Choosing the shell syntax

The commands `dircolors` prints have to match the shell that will run them. By default it guesses the syntax from the `SHELL` environment variable; these options state it outright.

| Option | Output syntax |
|---|---|
| `-b`, `--sh`, `--bourne-shell` | Bourne-shell-family syntax. |
| `-c`, `--csh`, `--c-shell` | C-shell-family syntax. |

If no shell option is given and `SHELL` is not set, `dircolors` cannot guess and reports an error.

## Customising the colours

To change the colours, you start from the default scheme, edit it, and feed it back.

| Option | Effect |
|---|---|
| `-p`, `--print-database` | Print the built-in colour database in its editable source form, instead of shell commands. |
| `--print-ls-colors` | Print the colours fully escaped, one per line, for inspection. |

The workflow is:

```
dircolors -p > ~/.dircolors      # save the default scheme to a file
# ...edit ~/.dircolors to taste...
eval "$(dircolors ~/.dircolors)" # load the edited scheme
```

When `dircolors` is given a `file` argument, it reads the colour definitions from that file instead of using the built-in database. The file maps file types and name extensions to colours; `dircolors -p` shows the format, with each line commented.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A usage error, or a colour-definition file that could not be read or parsed. |

---

# Viewing and joining text

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> The commands that print files to the screen, show parts of them, and combine or split text by line and by column.

Much of the work at a command line is reading text — looking at a file, checking the start or end of one, pulling a column out, stitching two files together. This topic covers the commands for that: viewing files, showing parts of them, and joining or splitting text by line and by column.

This page names every command in the topic. The companion topic, [Transforming text](/peios/using-peios/peiosutils/transforming-text/overview.md), covers the commands that *reshape* text — sorting, de-duplicating, substituting characters.

## The commands

**Viewing a whole file**

| Command | Purpose |
|---|---|
| [`cat`](/peios/using-peios/peiosutils/viewing-and-joining-text/cat.md) | Print one or more files straight through — and join several into one. |
| [`tac`](/peios/using-peios/peiosutils/viewing-and-joining-text/tac.md) | Print a file with its lines in reverse — last line first. |
| [`more`](/peios/using-peios/peiosutils/viewing-and-joining-text/more.md) | Show a file one screen at a time, pausing between screens. |
| [`od`](/peios/using-peios/peiosutils/viewing-and-joining-text/od.md) | Dump a file's raw bytes in octal, hex, decimal, or character form. |

**Viewing part of a file**

| Command | Purpose |
|---|---|
| [`head`](/peios/using-peios/peiosutils/viewing-and-joining-text/head.md) | Print the first lines (or bytes) of a file. |
| [`tail`](/peios/using-peios/peiosutils/viewing-and-joining-text/tail.md) | Print the last lines of a file — and, with `-f`, keep printing as the file grows. |
| [`nl`](/peios/using-peios/peiosutils/viewing-and-joining-text/nl.md) | Print a file with its lines numbered. |

**Cutting and combining**

| Command | Purpose |
|---|---|
| [`cut`](/peios/using-peios/peiosutils/viewing-and-joining-text/cut.md) | Pull selected columns — byte ranges or delimited fields — out of each line. |
| [`paste`](/peios/using-peios/peiosutils/viewing-and-joining-text/paste.md) | Join files side by side, line for line. |
| [`join`](/peios/using-peios/peiosutils/viewing-and-joining-text/join.md) | Join two files on a shared field, like a database join. |
| [`comm`](/peios/using-peios/peiosutils/viewing-and-joining-text/comm.md) | Compare two sorted files and report which lines they share. |

**Splitting a file into files**

| Command | Purpose |
|---|---|
| [`split`](/peios/using-peios/peiosutils/viewing-and-joining-text/split.md) | Break a file into equal-sized pieces. |
| [`csplit`](/peios/using-peios/peiosutils/viewing-and-joining-text/csplit.md) | Break a file into pieces at lines that match a pattern. |

## A note on input

Almost every command here follows the same convention for where its text comes from. Name one or more files and it reads those; name none and it reads from standard input, so it can sit in the middle of a pipeline. Where a command accepts files, the name `-` stands for standard input, so you can mix files and piped input in one invocation.

## Where to start

To simply print a file, or join a few together, read [`cat`](/peios/using-peios/peiosutils/viewing-and-joining-text/cat.md) — the most-used command in the topic.

---

# cat

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Print files to standard output, and concatenate several files into one stream.

`cat` prints the contents of files. Its name is short for "concatenate" — given several files, it prints them one after another, as a single stream.

```
cat [options] [file...]
```

```
$ cat notes.txt
$ cat part1.txt part2.txt part3.txt > whole.txt
```

With no file — or with the name `-` — `cat` reads standard input, which is what makes it useful in a pipeline or for capturing typed input into a file.

## Plain use

Most of the time `cat` is run with no options at all: it copies its input to its output, byte for byte, unchanged. The options exist for the times you want to *see* something about the text that is normally invisible, or to adjust spacing and numbering.

## Numbering lines

| Option | Effect |
|---|---|
| `-n`, `--number` | Number every output line. |
| `-b`, `--number-nonblank` | Number only the non-empty lines. Overrides `-n`. |

## Making invisible characters visible

These options reveal characters that normally print as nothing, or as whitespace — useful when a file is not behaving and you suspect a stray tab or control character.

| Option | Effect |
|---|---|
| `-E`, `--show-ends` | Print a `$` at the end of each line, so trailing spaces become visible. |
| `-T`, `--show-tabs` | Print tab characters as `^I` instead of as whitespace. |
| `-v`, `--show-nonprinting` | Print control and other non-printing characters using `^` and `M-` notation (leaving newline and tab as they are). |
| `-e` | Shorthand for `-vE`. |
| `-t` | Shorthand for `-vT`. |
| `-A`, `--show-all` | Shorthand for `-vET` — show ends, tabs, and all non-printing characters at once. |

## Adjusting spacing

| Option | Effect |
|---|---|
| `-s`, `--squeeze-blank` | Collapse runs of blank lines into a single blank line. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was printed. |
| `1` | A file could not be read. |

---

# tac

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Print files with their lines in reverse order — last line first.

`tac` prints a file with its lines in reverse — the last line first, the first line last. The name is `cat` spelled backwards, which is exactly what it does.

```
tac [options] [file...]
```

```
$ tac events.log
```

Reversing a log file so the newest entries come first is the everyday use. With no file, `tac` reads standard input.

## Reversing on something other than lines

`tac` does not have to split on newlines. You can tell it to treat some other string as the separator, and it will reverse the order of the pieces between those separators.

| Option | Effect |
|---|---|
| `-s`, `--separator=STRING` | Use `STRING` as the separator between records, instead of a newline. |
| `-r`, `--regex` | Treat the separator as a regular expression rather than a literal string. |
| `-b`, `--before` | Expect the separator *before* each record rather than after it. This matters for how the separator is reattached when the records are reordered. |

```
$ tac -s ', ' -r names.csv
```

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was printed. |
| `1` | A file could not be read, or the separator regular expression was invalid. |

---

# head

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Print the first lines or bytes of a file.

`head` prints the **beginning** of a file — by default, the first 10 lines.

```
head [options] [file...]
```

```
$ head config.toml
```

It is the quick way to glance at the top of a file without printing the whole thing — a header, the first few records, the start of a log. With no file, `head` reads standard input.

## Choosing how much

| Option | Effect |
|---|---|
| `-n`, `--lines=NUM` | Print the first `NUM` lines instead of 10. |
| `-c`, `--bytes=NUM` | Print the first `NUM` bytes instead of counting lines. |

`NUM` accepts a unit suffix (`K`, `M`, …) when counting bytes.

### Counting from the end

If `NUM` is given a leading `-`, the meaning flips: `head` prints **everything except the last `NUM`**.

```
$ head -n -5 report.txt    # all but the final 5 lines
```

## Headers for multiple files

When given more than one file, `head` prints a header line before each one so you can tell them apart. Two options override that:

| Option | Effect |
|---|---|
| `-q`, `--quiet` | Never print the file-name headers. |
| `-v`, `--verbose` | Always print the header, even for a single file. |

## Other options

| Option | Effect |
|---|---|
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter instead of newline. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was printed. |
| `1` | A file could not be read. |

---

# tail

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Print the last lines of a file — and keep printing as the file grows.

`tail` prints the **end** of a file — by default, the last 10 lines.

```
tail [options] [file...]
```

```
$ tail server.log
```

It is the counterpart of [`head`](/peios/using-peios/peiosutils/viewing-and-joining-text/head.md): the quick way to see the most recent lines of a file. With no file, `tail` reads standard input.

## Choosing how much

| Option | Effect |
|---|---|
| `-n`, `--lines=NUM` | Print the last `NUM` lines instead of 10. |
| `-c`, `--bytes=NUM` | Print the last `NUM` bytes instead of counting lines. |

If `NUM` is given a leading `+`, the meaning flips: `tail` prints **everything from line `NUM` onward**.

```
$ tail -n +20 report.txt    # from line 20 to the end
```

## Following a growing file

`tail`'s most useful trick is `-f`. Instead of printing the end and exiting, it stays running and prints new lines as they are appended — the standard way to watch a log file live.

| Option | Effect |
|---|---|
| `-f`, `--follow` | Keep the file open and print new data as it is added. |
| `-F` | Follow the file *by name*, and keep retrying if it is missing. Equivalent to `--follow=name --retry`. This survives log rotation — when the file is replaced, `-F` picks up the new one. |
| `--retry` | Keep trying to open a file that is not yet accessible. |
| `--pid=PID` | While following, stop once process `PID` exits. |
| `-s`, `--sleep-interval=N` | Wait `N` seconds between checks of the file. |

Press Ctrl-C to stop a `tail -f`.

## Headers for multiple files

As with `head`, when given more than one file `tail` prints a header before each.

| Option | Effect |
|---|---|
| `-q`, `--quiet` | Never print the file-name headers. |
| `-v`, `--verbose` | Always print the header, even for a single file. |

## Other options

| Option | Effect |
|---|---|
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter instead of newline. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read. |

---

# more

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> View a file one screen at a time, pausing between screens.

`more` displays a file one screenful at a time. Where [`cat`](/peios/using-peios/peiosutils/viewing-and-joining-text/cat.md) prints a whole file at once — and a long file scrolls straight past — `more` shows the first screen, then waits for you before showing the next.

```
more [options] file...
```

```
$ more long-report.txt
```

## Moving through the file

While `more` is paused, it is waiting for a keypress:

| Key | Action |
|---|---|
| Space | Show the next screenful. |
| Return | Show the next line. |
| `q` | Quit. |
| `/` | Search forward for a string. |
| `h` | Show the built-in help. |

`more` moves *forward* through a file. It is the simple, always-available pager — enough for reading something through once.

## Options

| Option | Effect |
|---|---|
| `-n`, `--lines=NUM` | Show `NUM` lines per screenful instead of filling the terminal. `--number=NUM` is the same. |
| `-F`, `--from-line=NUM` | Begin displaying at line `NUM`. |
| `-P`, `--pattern=STRING` | Search for `STRING` and begin displaying at the first match. |
| `-e`, `--exit-on-eof` | Exit automatically at the end of the file, rather than waiting. |
| `-s`, `--squeeze` | Collapse runs of blank lines into one. |
| `-u`, `--plain` | Suppress underlining in the displayed text. |
| `-p`, `--print-over` | Clear the screen and print the next page, instead of scrolling. |
| `-c`, `--clean-print` | Redraw each page in place, cleaning line ends, instead of scrolling. |
| `-d`, `--silent` | When an unrecognised key is pressed, show a short hint instead of ringing the terminal bell. |
| `-l`, `--logical` | Do not pause when a line contains a form-feed character. |
| `-f`, `--no-pause` | Count logical lines rather than screen lines when deciding where to pause. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The file was displayed. |
| `1` | A file could not be opened. |

---

# nl

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Print files with their lines numbered, with control over which lines are counted and how the number is formatted.

`nl` prints a file with its lines numbered. [`cat -n`](/peios/using-peios/peiosutils/viewing-and-joining-text/cat.md) also numbers lines; `nl` is the command for when you need *control* over the numbering — which lines count, how the number looks, where it restarts.

```
nl [options] [file...]
```

```
$ nl chapter.txt
     1  Once the system is installed,
     2  the first boot brings up
       
     3  the configuration service.
```

## Which lines get a number

By default `nl` numbers only the non-empty lines. `-b` sets the rule:

| `-b STYLE` | Numbers… |
|---|---|
| `-b a` | every line. |
| `-b t` | only non-empty lines. This is the default. |
| `-b n` | no lines. |
| `-b pBRE` | only lines that match the basic regular expression `BRE`. |

| Option | Effect |
|---|---|
| `-b`, `--body-numbering=STYLE` | The numbering rule, as above. |
| `-l`, `--join-blank-lines=N` | Count a run of `N` blank lines as one numbered line. |

## How the number looks

| Option | Effect |
|---|---|
| `-n`, `--number-format=FORMAT` | `ln` = left-justified; `rn` = right-justified; `rz` = right-justified with leading zeros. |
| `-w`, `--number-width=N` | Use `N` columns for the number. |
| `-s`, `--number-separator=STRING` | Put `STRING` between the number and the line text. |
| `-v`, `--starting-line-number=N` | Start counting from `N`. |
| `-i`, `--line-increment=N` | Increase the number by `N` at each counted line. |

## Logical pages

`nl` can treat one file as a sequence of **logical pages**, each with a header, a body, and a footer, and number the three parts by different rules. Pages are separated by special delimiter lines in the input.

| Option | Effect |
|---|---|
| `-h`, `--header-numbering=STYLE` | Numbering style for header sections. |
| `-f`, `--footer-numbering=STYLE` | Numbering style for footer sections. |
| `-d`, `--section-delimiter=CC` | The characters that mark a section boundary in the input. |
| `-p`, `--no-renumber` | Do not reset the line number at the start of each logical page. |

The styles for `-h` and `-f` are the same `a` / `t` / `n` / `pBRE` set as `-b`. For an ordinary file with no delimiter lines, the whole file is one body and only `-b` matters.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or an option value was invalid. |

---

# cut

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Pull selected columns — byte ranges or delimited fields — out of each line of input.

`cut` extracts **columns** from text. For each line of input it prints only the parts you select, dropping the rest.

```
cut option... [file...]
```

```
$ cut -d , -f 1,3 people.csv     # the 1st and 3rd comma-separated field
$ cut -c 1-8 log.txt             # the first 8 characters of each line
```

Every run of `cut` needs two decisions: **what counts as a column**, and **which columns to keep**.

## What counts as a column

Choose exactly one mode:

| Option | A column is… |
|---|---|
| `-b`, `--bytes` | a single byte. |
| `-c`, `--characters` | a single character. |
| `-f`, `--fields` | a field — a stretch of text between delimiters. |

`-b` and `-c` cut by position. `-f` cuts by *field*, which is what you want for tabular data — a CSV, a table, the columns of a report.

## Which columns to keep

The mode option takes a **sequence**: numbers and inclusive ranges, separated by commas.

| Sequence | Selects |
|---|---|
| `3` | column 3 only. |
| `2,5,9` | columns 2, 5, and 9. |
| `5-7` | columns 5 through 7. |
| `3-` | column 3 to the end of the line. |
| `-4` | the start of the line through column 4. |
| `1,4-6,9` | any mixture of the above. |

| Option | Effect |
|---|---|
| `--complement` | Invert the selection — keep every column *except* those named. |

## Field mode: the delimiter

In field mode, `cut` needs to know what separates the fields.

| Option | Effect |
|---|---|
| `-d`, `--delimiter=CHAR` | The character that separates fields. The default is a tab. |
| `-w` | Separate fields on runs of whitespace (spaces and tabs) instead of a single character. Cannot be combined with `-d`. |
| `-s`, `--only-delimited` | Print only lines that actually contain the delimiter; drop lines that have no fields to cut. |
| `--output-delimiter=STRING` | Put `STRING` between the kept fields in the output, instead of repeating the input delimiter — handy for converting one delimited format to another. |

## Other options

| Option | Effect |
|---|---|
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter, so a "line" may itself contain newlines. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A mode was missing or given twice, an option was misused, or a file could not be read. |

---

# paste

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Join files side by side, line for line.

`paste` joins files **side by side**. It takes the first line of each file and prints them on one line, then the second line of each, and so on — merging files into columns.

```
paste [options] [file...]
```

```
$ paste names.txt ages.txt
Ada      36
Grace    41
Linus    29
```

By default the merged pieces are separated by a tab. `paste` is the column-wise counterpart of [`cat`](/peios/using-peios/peiosutils/viewing-and-joining-text/cat.md), which joins files end to end.

## Pasting one file at a time

| Option | Effect |
|---|---|
| `-s`, `--serial` | Paste each file's lines onto a *single line*, one file at a time, instead of merging files in parallel. A file's lines become a row. |

```
$ paste -s -d , names.txt
Ada,Grace,Linus
```

## Choosing the separator

| Option | Effect |
|---|---|
| `-d`, `--delimiters=LIST` | Use the characters in `LIST` as separators instead of a tab. When `LIST` has more than one character, `paste` cycles through them. |
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter instead of newline. |

## `paste` and `join`

`paste` merges by *position* — line 1 with line 1, line 2 with line 2, blind to content. When you want to merge by a *matching value* — pairing rows that share a key — that is [`join`](/peios/using-peios/peiosutils/viewing-and-joining-text/join.md).

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or the delimiter list was malformed. |

---

# join

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Join two files on a shared field, pairing lines that have the same key.

`join` merges two files on a **shared field**. For every pair of lines — one from each file — that have the same value in their join field, it prints a combined line. It is the command-line form of a database join.

```
join [options] file1 file2
```

```
$ join employees.txt salaries.txt
```

If `employees.txt` has `101 Ada` and `salaries.txt` has `101 80000`, `join` pairs them on the shared `101` and prints `101 Ada 80000`.

## The input must be sorted

`join` works by stepping through both files together, so **both files must be sorted on the join field**. If they are not, `join` will miss matches. Sort them first with [`sort`](/peios/using-peios/peiosutils/transforming-text/sort.md), on the same field `join` will use.

`join` checks the ordering as it goes and reports a file that is out of order; `--nocheck-order` turns that check off, and `--check-order` forces it on.

## Choosing the join field

By default `join` joins on the **first** field of each file, and fields are separated by whitespace.

| Option | Effect |
|---|---|
| `-1 FIELD` | Join on `FIELD` of file 1. |
| `-2 FIELD` | Join on `FIELD` of file 2. |
| `-j FIELD` | Join on `FIELD` of *both* files — shorthand for `-1 FIELD -2 FIELD`. |
| `-t CHAR` | Use `CHAR` as the field separator for input and output. |

## Unmatched lines

By default `join` prints only the lines that *paired*. A line with no match on the other side is dropped. These options bring unmatched lines back:

| Option | Effect |
|---|---|
| `-a FILENUM` | Also print unpaired lines from file `FILENUM` (`1` or `2`). |
| `-v FILENUM` | Print *only* the unpaired lines from file `FILENUM`, and suppress the joined output. |
| `-e EMPTY` | Fill in any missing field with the string `EMPTY`. |

## Shaping the output

| Option | Effect |
|---|---|
| `-o FORMAT` | Build each output line from the field list `FORMAT`, rather than the default layout. |
| `-i`, `--ignore-case` | Ignore letter case when comparing join fields. |
| `--header` | Treat the first line of each file as column headers — print them, paired, without trying to match them. |
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, was not sorted, or an option was invalid. |

---

# comm

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Compare two sorted files line by line and report which lines they share.

`comm` compares two sorted files and tells you which lines are **unique to each** and which are **common to both**.

```
comm [options] file1 file2
```

By default it prints three columns:

```
$ comm list-a.txt list-b.txt
            apple
banana
                        cherry
date
```

- **Column 1** — lines only in `file1`.
- **Column 2** — lines only in `file2`.
- **Column 3** — lines in both.

Each column is indented past the one before, so a line's column position tells you where it was found.

## The input must be sorted

`comm` steps through both files together, so **both must be sorted**. On unsorted input the comparison is meaningless. Sort the files first with [`sort`](/peios/using-peios/peiosutils/transforming-text/sort.md).

`comm` checks the ordering and reports a file that is out of order. `--check-order` forces the check; `--nocheck-order` disables it.

## Showing only the columns you want

Each column can be switched off. The remaining columns close up to fill the gap.

| Option | Effect |
|---|---|
| `-1` | Suppress column 1 — hide lines unique to `file1`. |
| `-2` | Suppress column 2 — hide lines unique to `file2`. |
| `-3` | Suppress column 3 — hide lines common to both. |

These combine to answer specific questions. `comm -12` shows *only* the common lines — the intersection of the two files. `comm -23` shows lines in `file1` that are not in `file2` — the difference.

## Other options

| Option | Effect |
|---|---|
| `--output-delimiter=STR` | Separate the columns with `STR` instead of spaces. |
| `--total` | Print a final summary line with the count in each column. |
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or was not sorted. |

---

# split

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Break a file into equal-sized pieces, each written to its own output file.

`split` breaks one file into several smaller files of roughly equal size.

```
split [options] [input [prefix]]
```

```
$ split -l 1000 big.log
$ ls
xaa  xab  xac  xad
```

By default `split` writes 1000 lines per piece, names the pieces `xaa`, `xab`, `xac`, … — a `prefix` of `x` followed by a two-letter suffix — and reads its input from a named file or from standard input.

To put the file back together, concatenate the pieces in order: [`cat`](/peios/using-peios/peiosutils/viewing-and-joining-text/cat.md)` xaa xab xac … > original`.

Each piece is a newly created file, and a new file needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md); each piece inherits one from the directory it is created in.

## How big each piece is

Choose one of these to set the piece size:

| Option | Each piece holds… |
|---|---|
| `-l`, `--lines=N` | `N` lines. This is the default behaviour, with `N` = 1000. |
| `-b`, `--bytes=SIZE` | `SIZE` bytes. |
| `-C`, `--line-bytes=SIZE` | up to `SIZE` bytes, but only whole lines — never a line split across two pieces. |
| `-n`, `--number=CHUNKS` | the input divided into a fixed number of pieces (see below). |

A `SIZE` accepts a unit suffix: `K`, `M`, `G`, … as powers of 1024, or `KB`, `MB`, … as powers of 1000.

### Fixed number of chunks (`-n`)

`-n` divides the input into a set number of pieces rather than fixing each piece's size. `CHUNKS` may be:

| Form | Effect |
|---|---|
| `N` | Split into `N` pieces by size. |
| `l/N` | Split into `N` pieces without splitting any line across pieces. |
| `r/N` | Like `l/N`, but distribute lines round-robin across the pieces. |
| `K/N` | Write only the `K`th of `N` pieces, to standard output. |

## Naming the pieces

| Option | Effect |
|---|---|
| `prefix` (operand) | The leading part of each output name. Default `x`. |
| `-a`, `--suffix-length=N` | Use `N` characters for the suffix. Default 2. |
| `-d`, `--numeric-suffixes[=START]` | Use numeric suffixes (`00`, `01`, …) instead of letters, optionally from `START`. |
| `-x`, `--hex-suffixes[=START]` | Use hexadecimal suffixes. |
| `--additional-suffix=SUFFIX` | Append a fixed `SUFFIX` to every output name — for giving the pieces an extension. |

## Other options

| Option | Effect |
|---|---|
| `-e`, `--elide-empty-files` | With `-n`, do not write out pieces that would be empty. |
| `-t`, `--separator=SEP` | Use `SEP` as the line separator instead of newline. |
| `--verbose` | Print a line as each output file is opened. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The file was split. |
| `1` | A failure — the input could not be read, or the suffixes were exhausted. |

---

# csplit

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Break a file into pieces at lines chosen by line number or by a matching pattern.

`csplit` — "context split" — breaks a file into pieces at points *you* choose: a line number, or a line that matches a pattern. Where [`split`](/peios/using-peios/peiosutils/viewing-and-joining-text/split.md) cuts a file into equal sizes, `csplit` cuts it at meaningful boundaries.

```
csplit [options] file pattern...
```

```
$ csplit report.txt '/^Chapter/' '{*}'
```

That splits `report.txt` into one piece per chapter, cutting at every line beginning with `Chapter`. The pieces are written to `xx00`, `xx01`, `xx02`, … and `csplit` prints the byte size of each.

Each piece is a newly created file, and a new file needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md); each piece inherits one from the directory it is created in.

## Patterns

Each `pattern` argument marks a cut point. `csplit` copies everything up to that point into the next output file, then continues.

| Pattern | Cut where… |
|---|---|
| `N` (a number) | line `N` is reached — the piece is lines up to `N-1`. |
| `/REGEX/` | a line matches `REGEX`; that line starts the *next* piece. |
| `%REGEX%` | a line matches `REGEX` — but **skip** the text up to it instead of writing it to a piece. |
| `/REGEX/+N` or `/REGEX/-N` | an offset of `N` lines from the matching line. |
| `{N}` | repeat the *previous* pattern `N` more times. |
| `{*}` | repeat the previous pattern as many times as the file allows. |

A pattern that finds no match is an error, and by default `csplit` removes the pieces it had already written. `-k` keeps them.

## Naming the pieces

| Option | Effect |
|---|---|
| `-f`, `--prefix=PREFIX` | Use `PREFIX` instead of `xx` at the start of each output name. |
| `-n`, `--digits=N` | Use `N` digits in the numeric part of the name instead of 2. |
| `-b`, `--suffix-format=FORMAT` | Use a custom `printf`-style `FORMAT` for the numeric suffix. |

## Other options

| Option | Effect |
|---|---|
| `-k`, `--keep-files` | Do not delete the output files if `csplit` fails partway. |
| `-z`, `--elide-empty-files` | Do not write out pieces that would be empty. |
| `--suppress-matched` | Drop the lines that the patterns matched, rather than keeping them in a piece. |
| `-s`, `--quiet` | Do not print the byte size of each piece. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The file was split. |
| `1` | A pattern did not match, a line number was out of range, or the input could not be read. |

---

# od

_Peios / Using Peios / Peiosutils / Viewing and joining text_

> Dump the raw bytes of a file in octal, hexadecimal, decimal, floating-point, or character form — a low-level view of exactly what a file contains.

`od` — short for "octal dump" — prints the raw bytes of a file. Where [`cat`](/peios/using-peios/peiosutils/viewing-and-joining-text/cat.md) shows you a file as text, `od` shows you the actual bytes underneath: each one rendered as a number, a character, or a floating-point value, in whatever base you ask for. It is the tool for looking at binary files, checking exactly which bytes a text file contains, or finding a stray control character that isn't visible on screen.

```
od [options] [file...]
```

```
$ od hello.txt
0000000 062510 066154 020157 067527 066162 020144 000012
0000015
```

With no file — or with the name `-` — `od` reads standard input, so it can sit at the end of a pipeline. Given several files, it reads them one after another as a single continuous stream, and the byte offsets run straight through from one file into the next.

## How the output is laid out

Every line of output has two parts:

- An **offset** on the left: the position, counted in bytes from the start of the input, of the first byte on that line. By default it is printed in octal (see [`-A`](#the-offset-column) to change the base).
- One or more **format columns** on the right: the bytes of that line rendered in the format(s) you chose.

With no format option, `od` prints the input as **octal, two bytes at a time** — the traditional default. The very last line of output is a lone offset marking the total length of the input.

If you ask for more than one format at once, each format is printed on its own line, stacked under a single shared offset. Only the first line of the group carries the offset; the rest are indented to line up beneath it.

## Choosing what the bytes look like

There are two ways to say how bytes should be rendered: a set of single-letter **named shortcuts** for the common cases, and the general **`-t` / `--format`** option for full control. You can give several at once, and they are applied in the order they appear on the command line.

### Named format shortcuts

Each of these selects one output format. Combine them freely (`od -cx` prints characters and hex together).

| Option | Renders each unit as |
|---|---|
| `-a` | Named characters, ignoring the high-order bit (control characters shown by name, e.g. `nul`, `sp`, `del`). |
| `-b` | Octal, one byte at a time. |
| `-c` | Printable ASCII / UTF-8 characters, with backslash escapes (`\n`, `\t`, …) for the rest. |
| `-d` | Unsigned decimal, 2-byte units. |
| `-D` | Unsigned decimal, 4-byte units. |
| `-o` | Octal, 2-byte units. |
| `-O` | Octal, 4-byte units. |
| `-s` | Signed decimal, 2-byte units. |
| `-i` | Signed decimal, 4-byte units. |
| `-l` | Signed decimal, 8-byte units. |
| `-I`, `-L` | Signed decimal, 8-byte units. |
| `-x`, `-h` | Hexadecimal, 2-byte units. |
| `-X`, `-H` | Hexadecimal, 4-byte units. |
| `-f` | Floating point, single precision (32-bit). |
| `-e`, `-F` | Floating point, double precision (64-bit). |

### Type specifications (`-t`, `--format`)

`-t` takes a **type specification** and gives you every combination the named shortcuts cover and more. Repeat `-t` (or list several specs in one argument) to print multiple formats.

A type specification is a **type letter**, an optional **size**, and an optional **`z`** suffix:

| Type letter | Meaning |
|---|---|
| `a` | Printable 7-bit ASCII, named (like `-a`). |
| `c` | UTF-8 characters, with octal escapes for undefined bytes (like `-c`). |
| `d` | Signed decimal. |
| `u` | Unsigned decimal. |
| `o` | Octal. |
| `x` | Hexadecimal. |
| `f` | Floating point. |

For the numeric types (`d`, `u`, `o`, `x`, `f`) a **size** says how many bytes make up one unit. It can be a number, or a letter:

| Size | Applies to | Bytes per unit |
|---|---|---|
| `1` / `C` | integer types | 1 |
| `2` / `S` | integer types | 2 |
| `4` / `I` | integer types | 4 |
| `8` / `L` | integer types | 8 |
| `4` / `F` | floating point | 4 (single precision) |
| `8` / `D` | floating point | 8 (double precision) |
| `16` / `L` | floating point | 16 (extended / long double) |
| `2` / `H` | floating point | 2 (IEEE half precision) |
| `2` / `B` | floating point | 2 (bfloat16) |

If you leave the size off, integer types default to **4 bytes** and floating point defaults to **8 bytes**. For the character types `a` and `c`, a size is not allowed.

Add a **`z`** at the end of a spec to append an ASCII dump — the bytes of that line shown as text, `.` for anything non-printable — to the right of the numbers. For example, `od -t x1z` gives the familiar hex-plus-text layout:

```
$ od -A x -t x1z file.bin
000000 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 0a           >Hello, world!.<
```

Examples: `od -t d2` is signed decimal in 2-byte units; `od -t x1` is hex byte-by-byte; `od -t fD` is double-precision floats; `od -t c` is characters. `od -t x1 -t c` prints hex and characters as two stacked lines.

### Byte order (`--endian`)

For multi-byte numeric formats, `--endian` sets the byte order used to assemble each unit. Without it, `od` uses the machine's native byte order. Use `--endian=big` or `--endian=little` to force one regardless of the host.

## The offset column

| Option | Effect |
|---|---|
| `-A RADIX`, `--address-radix=RADIX` | Print offsets in the given base. `RADIX` is one of `o` (octal, the default), `d` (decimal), `x` (hexadecimal), or `n` (none — omit the offset column entirely). |

Only the first character of the value is examined, so `od -A n` and `od -Anone` mean the same thing.

## Which bytes to read

By default `od` dumps the whole input. Two options narrow that to a window:

| Option | Effect |
|---|---|
| `-j BYTES`, `--skip-bytes=BYTES` | Skip `BYTES` bytes of input before starting to dump. When several files are given, the skip runs across the concatenated stream. |
| `-N BYTES`, `--read-bytes=BYTES` | Dump at most `BYTES` bytes, then stop. |

In both, `BYTES` is decimal by default, octal if it starts with `0`, or hexadecimal if it starts with `0x`. A suffix scales it: `b` = ×512, `KB`/`K` = ×1000 / ×1024, `MB`/`M` = ×1000² / ×1024², `GB`/`G` = ×1000³ / ×1024³.

## Collapsing repeated lines

When several output lines in a row would be identical, `od` prints the first one, then a single line containing just `*` to stand for the run, and resumes at the next line that differs. This keeps a dump of a large block of identical bytes (a file full of zeros, say) short.

| Option | Effect |
|---|---|
| `-v`, `--output-duplicates` | Turn the collapsing off — print every line, including repeats, with no `*` markers. |

## Output width

| Option | Effect |
|---|---|
| `-w[BYTES]`, `--width[=BYTES]` | Print `BYTES` input bytes per output line. The default is 16; giving `-w` with no value uses 32. |

The width must be a whole multiple of the size of the largest format unit in play; if it isn't, `od` warns and rounds it to a usable value.

## Finding printable strings

| Option | Effect |
|---|---|
| `-S[BYTES]`, `--strings[=BYTES]` | Instead of dumping bytes, scan the input and print only runs of at least `BYTES` printable characters, one per line, each preceded by its offset. `BYTES` defaults to 3 when omitted. |

This turns `od` into a quick way to pull the human-readable text out of a binary file. The offset in front of each string honours `-A` (and is dropped entirely under `-A n`). `-j` and `-N` still apply, so you can restrict the scan to a window.

## The traditional offset form

`od` also accepts the historical calling convention, where an **offset** (and, in `--traditional` mode, a **label**) is given as a bare operand rather than an option:

```
od [named-format-options] [file] [[+]offset[.][b]]
od --traditional [options] [file] [[+]offset[.][b] [[+]label[.][b]]]
```

The **offset** says where in the input to begin, exactly like `-j`. It is read as **octal by default**, hexadecimal if prefixed with `0x`, or decimal if it carries a trailing `.`; a trailing `b` multiplies it by 512. The optional leading `+` is allowed everywhere and is required when there is no filename (so `od +20` reads standard input starting at octal offset 20).

The **label** (only in `--traditional` mode) is a second such number: the dump still begins at `offset`, but the offset column is *displayed* as if it started at `label` — useful for making a partial dump's offsets match the original file.

`od` only reads an operand as an offset in the plain form when none of `-A`, `-j`, `-N`, `-t`, `-v`, or `-w` are present. So if a real filename happens to look like a number, add a harmless option such as `-j0` to force it to be treated as a filename.

## A note on reading files

`od` has no access rules of its own — it simply opens a file and reads its bytes. That read is subject to the same check as every read on Peios: whether you may open the file for reading is decided by the file's **security descriptor** (see [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md)). `od` never reveals bytes you could not already read another way; it only presents the bytes you can read in a different form.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The input was dumped successfully. |
| `1` | A file could not be read, or an option, format, or offset was invalid. |

---

# Transforming text

_Peios / Using Peios / Peiosutils / Transforming text_

> The commands that reshape text — reordering lines, removing duplicates, substituting characters, reflowing paragraphs, and counting.

This topic covers the commands that **reshape** text: putting lines in order, dropping duplicates, swapping characters, reflowing paragraphs to a width, counting what is there. Its companion, [Viewing and joining text](/peios/using-peios/peiosutils/viewing-and-joining-text/overview.md), covers printing and combining files; this topic is about changing the text itself.

## The commands

**Ordering lines**

| Command | Purpose |
|---|---|
| [`sort`](/peios/using-peios/peiosutils/transforming-text/sort.md) | Sort lines — alphabetically, numerically, and many other ways. |
| [`shuf`](/peios/using-peios/peiosutils/transforming-text/shuf.md) | The opposite of sorting: put lines into a random order. |
| [`tsort`](/peios/using-peios/peiosutils/transforming-text/tsort.md) | Topological sort — order items so that each comes after the things it depends on. |

**Filtering lines**

| Command | Purpose |
|---|---|
| [`uniq`](/peios/using-peios/peiosutils/transforming-text/uniq.md) | Collapse or report adjacent repeated lines. |

**Substituting characters**

| Command | Purpose |
|---|---|
| [`tr`](/peios/using-peios/peiosutils/transforming-text/tr.md) | Translate, squeeze, or delete individual characters. |

**Whitespace**

| Command | Purpose |
|---|---|
| [`expand`](/peios/using-peios/peiosutils/transforming-text/expand.md) | Convert tabs into spaces. |
| [`unexpand`](/peios/using-peios/peiosutils/transforming-text/unexpand.md) | Convert runs of spaces back into tabs. |

**Reflowing and paginating**

| Command | Purpose |
|---|---|
| [`fmt`](/peios/using-peios/peiosutils/transforming-text/fmt.md) | Reflow paragraphs to a target width. |
| [`fold`](/peios/using-peios/peiosutils/transforming-text/fold.md) | Hard-wrap long lines at a fixed width. |
| [`pr`](/peios/using-peios/peiosutils/transforming-text/pr.md) | Paginate and columnate text for printing. |

**Indexing and counting**

| Command | Purpose |
|---|---|
| [`ptx`](/peios/using-peios/peiosutils/transforming-text/ptx.md) | Produce a permuted index of the words in a file. |
| [`wc`](/peios/using-peios/peiosutils/transforming-text/wc.md) | Count the lines, words, and bytes in a file. |

## Two commands that need sorted input

A theme worth knowing before you start: [`uniq`](/peios/using-peios/peiosutils/transforming-text/uniq.md) only collapses *adjacent* duplicate lines, so duplicates scattered through a file are not caught unless the file is sorted first. `sort` and `uniq` are almost always used together — and `sort -u` does both jobs in one step.

## Where to start

[`sort`](/peios/using-peios/peiosutils/transforming-text/sort.md) is the workhorse of the topic and the one with the most depth — start there.

---

# sort

_Peios / Using Peios / Peiosutils / Transforming text_

> Sort the lines of files — alphabetically, numerically, by field, and many other ways.

`sort` reads lines and writes them out in order.

```
sort [options] [file...]
```

```
$ sort names.txt
```

With no file, `sort` reads standard input. Given several files, it sorts all of their lines together as one stream. By default it compares lines as plain text, character by character.

## How lines are compared

The default text comparison is often not the order you want — `10` sorts before `9`, and case matters. These options change the comparison:

| Option | Compares lines as… |
|---|---|
| `-n`, `--numeric-sort` | numbers. `9` sorts before `10`. |
| `-g`, `--general-numeric-sort` | numbers, including scientific notation. Slower than `-n`; use it only when `-n` is not enough. |
| `-h`, `--human-numeric-sort` | human-readable sizes — `2K` before `1M` before `3G`. |
| `-M`, `--month-sort` | month names — `JAN` before `FEB`. |
| `-V`, `--version-sort` | version numbers — `1.2.10` after `1.2.9`. |
| `-R`, `--random-sort` | a random order (a shuffle that groups equal lines together). |

And these adjust *what* is compared:

| Option | Effect |
|---|---|
| `-f`, `--ignore-case` | Treat lower and upper case as the same. |
| `-d`, `--dictionary-order` | Consider only letters, digits, and blanks. |
| `-i`, `--ignore-nonprinting` | Ignore non-printing characters. |
| `-b`, `--ignore-leading-blanks` | Ignore blanks at the start of a line (or field). |

## Sort order options

| Option | Effect |
|---|---|
| `-r`, `--reverse` | Reverse the result. |
| `-u`, `--unique` | Output only the first line of each run of equal lines — sort and de-duplicate in one step. |
| `-s`, `--stable` | Keep equal lines in their original relative order. |

## Sorting by a field

By default `sort` compares whole lines. `-k` sorts on a **key** — a chosen part of each line — which is what you need for tabular data.

A line is split into fields (by default at whitespace). A key is written `FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]]` — a start field, an optional start character within it, and an optional end. The single-letter comparison options above can be appended to a key to apply only to it.

```
$ sort -k 2 -n scores.txt        # sort on the 2nd field, numerically
$ sort -t , -k 3 people.csv      # sort a CSV on the 3rd field
```

| Option | Effect |
|---|---|
| `-k`, `--key=KEYDEF` | Sort on the key `KEYDEF`. May be given more than once; keys are tried in order. |
| `-t`, `--field-separator=SEP` | Use `SEP` to split fields, instead of the whitespace default. |

## Checking and merging

| Option | Effect |
|---|---|
| `-c`, `--check` | Do not sort — just check whether the input is already sorted, and report the first line that is out of order. |
| `-C`, `--check=silent` | Check, but report only through the exit status. |
| `-m`, `--merge` | Treat the inputs as already-sorted files and merge them, which is faster than a full sort. |

## Output and resources

| Option | Effect |
|---|---|
| `-o`, `--output=FILE` | Write to `FILE` instead of standard output. `FILE` may be one of the inputs. |
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter. |
| `--parallel=N` | Use `N` threads. |
| `-S`, `--buffer-size=SIZE` | Set the size of the in-memory sort buffer. |
| `-T`, `--temporary-directory=DIR` | Put temporary files in `DIR` rather than the default location. |
| `--files0-from=F` | Take the list of input files from `F`, NUL-separated. |
| `--debug` | Underline the part of each line that was actually used as the sort key — invaluable when a `-k` key is not behaving. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The lines were sorted — or, with `-c`/`-C`, the input was already sorted. |
| `1` | With `-c`/`-C`, the input was *not* sorted. |
| `2` | An error — a file could not be read, or an option was invalid. |

---

# uniq

_Peios / Using Peios / Peiosutils / Transforming text_

> Collapse or report adjacent repeated lines in a file.

`uniq` deals with repeated lines: it can collapse a run of identical lines into one, count them, or print only the ones that repeated.

```
uniq [options] [input [output]]
```

```
$ uniq events.txt
```

`input` and `output` may be given as operands; with neither, `uniq` reads standard input and writes standard output.

## uniq only sees adjacent duplicates

This is the one thing to know about `uniq`: it only compares **each line with the one before it**. It collapses *adjacent* repeats. Duplicate lines scattered through a file, with other lines between them, are not caught.

So `uniq` is almost always paired with [`sort`](/peios/using-peios/peiosutils/transforming-text/sort.md), which brings every copy of a line together first:

```
$ sort events.txt | uniq
```

If collapsing duplicates is all you need, `sort -u` does both jobs in one command. Use `uniq` itself when you want what `sort -u` cannot do — counting repeats, or printing only the duplicated lines.

## What to output

By default `uniq` prints every line, with each run of adjacent duplicates reduced to a single copy. These options change that:

| Option | Effect |
|---|---|
| `-c`, `--count` | Prefix each line with the number of times it occurred. |
| `-d`, `--repeated` | Print only lines that *were* repeated — one copy of each. |
| `-D` | Print *all* copies of every repeated line, not just one. |
| `-u`, `--unique` | Print only lines that were *not* repeated. |
| `--group[=WHICH]` | Print every line, with runs separated by a blank line. `WHICH` is `separate`, `prepend`, `append`, or `both`. |

## What counts as "the same"

| Option | Effect |
|---|---|
| `-i`, `--ignore-case` | Treat lines differing only in letter case as the same. |
| `-f`, `--skip-fields=N` | Ignore the first `N` fields when comparing. |
| `-s`, `--skip-chars=N` | Ignore the first `N` characters when comparing. |
| `-w`, `--check-chars=N` | Compare at most `N` characters of each line. |

## Other options

| Option | Effect |
|---|---|
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read or written. |

---

# tr

_Peios / Using Peios / Peiosutils / Transforming text_

> Translate, squeeze, or delete individual characters in a stream of text.

`tr` — "translate" — works on text one **character** at a time. It can replace characters with other characters, remove characters, or collapse runs of a character into one.

```
tr [options] set1 [set2]
```

`tr` reads standard input and writes standard output — it has no file arguments, so it always sits in a pipeline.

```
$ tr 'a-z' 'A-Z' < notes.txt        # lower-case to upper-case
$ tr -d ' ' < spaced.txt            # delete every space
$ tr -s ' ' < padded.txt            # collapse runs of spaces to one
```

## The three jobs

What `tr` does depends on the options and how many sets you give it.

- **Translate** — give two sets. Each character in `set1` is replaced by the character at the same position in `set2`. `tr 'abc' 'xyz'` turns every `a` into `x`, `b` into `y`, `c` into `z`.
- **Delete** (`-d`) — give one set. Every character in `set1` is removed.
- **Squeeze** (`-s`) — each run of a repeated character that appears in the relevant set is collapsed to a single occurrence.

`-d` and `-s` can be combined: delete one set of characters, then squeeze another.

## Writing a set

A set is a string of characters, with a few shorthands:

| Notation | Means |
|---|---|
| `a-z` | A range — every character from `a` to `z`. |
| `[:alpha:]`, `[:digit:]`, `[:space:]`, … | A named character class. |
| `[=c=]` | An equivalence class — every character that sorts the same as `c`. |
| `[c*n]` | The character `c` repeated `n` times — for padding a set to a length. |
| `\n`, `\t`, `\\`, `\NNN` | Escapes — newline, tab, backslash, an octal byte. |

## Options

| Option | Effect |
|---|---|
| `-d`, `--delete` | Delete the characters in `set1` instead of translating. |
| `-s`, `--squeeze-repeats` | Collapse each run of a repeated character listed in the last set into one. |
| `-c`, `-C`, `--complement` | Operate on the *complement* of `set1` — every character it does **not** list. |
| `-t`, `--truncate-set1` | Before translating, shorten `set1` to the length of `set2`, rather than reusing `set2`'s last character to cover the surplus. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A usage error — the wrong number of sets, or a malformed set. |

---

# expand

_Peios / Using Peios / Peiosutils / Transforming text_

> Convert tab characters in a file into spaces.

`expand` converts tab characters into spaces, so that text lines up the same way no matter what tab width something is viewed with.

```
expand [options] [file...]
```

```
$ expand source.txt > source-spaces.txt
```

A tab is not a fixed number of spaces — it jumps to the next tab stop, and where those stops are is a display setting. `expand` removes the ambiguity: it replaces each tab with exactly the number of spaces needed to reach the same column, baking the layout in. With no file, it reads standard input.

[`unexpand`](/peios/using-peios/peiosutils/transforming-text/unexpand.md) is the reverse operation.

## Options

| Option | Effect |
|---|---|
| `-t`, `--tabs=N` | Set tab stops every `N` columns instead of the default 8. |
| `-t`, `--tabs=LIST` | Given a comma-separated list, place tab stops at those explicit column positions. |
| `-i`, `--initial` | Convert only the tabs that come before the first non-blank character on each line — leave tabs *within* the text alone. Useful for re-indenting code without disturbing aligned tabs inside it. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or a tab specification was invalid. |

---

# unexpand

_Peios / Using Peios / Peiosutils / Transforming text_

> Convert runs of spaces in a file back into tab characters.

`unexpand` is the reverse of [`expand`](/peios/using-peios/peiosutils/transforming-text/expand.md): it converts runs of spaces back into tab characters.

```
unexpand [options] [file...]
```

```
$ unexpand spaced.txt > tabbed.txt
```

Where a run of spaces reaches a tab stop, `unexpand` replaces it with a tab. By default it only converts the **leading** spaces on each line — the indentation — and leaves spacing within the text alone. With no file, it reads standard input.

## Options

| Option | Effect |
|---|---|
| `-t`, `--tabs=N` | Set tab stops every `N` columns instead of the default 8. A comma-separated list places stops at explicit column positions. Using `-t` also enables `-a`. |
| `-a`, `--all` | Convert *all* runs of spaces that reach a tab stop, not only the leading indentation. |
| `--first-only` | Convert only the leading run of blanks on each line. Overrides `-a`. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or a tab specification was invalid. |

---

# fmt

_Peios / Using Peios / Peiosutils / Transforming text_

> Reflow paragraphs of text to a target line width.

`fmt` reflows paragraphs: it rejoins the lines of a paragraph and re-breaks them so each comes out close to a target width. Words move freely between lines to make the result fit.

```
fmt [options] [file...]
```

```
$ fmt -w 72 draft.txt
```

`fmt` works on **paragraphs** — runs of non-blank lines separated by blank lines — and treats word boundaries as places it may break. The result reads naturally. This is what distinguishes it from [`fold`](/peios/using-peios/peiosutils/transforming-text/fold.md), which simply cuts every line at a fixed column without regard to words.

With no file, `fmt` reads standard input.

## Setting the width

| Option | Effect |
|---|---|
| `-w`, `--width=WIDTH` | Fill lines up to at most `WIDTH` columns. The default is 75. |
| `-g`, `--goal=WIDTH` | Aim for `WIDTH` columns — the width `fmt` targets, while `-w` is the hard maximum. Defaults to about 93% of the maximum. |

## Controlling the reflow

| Option | Effect |
|---|---|
| `-s`, `--split-only` | Only split lines that are too long; never join short lines together. |
| `-u`, `--uniform-spacing` | Put exactly one space between words and two between sentences. |
| `-c`, `--crown-margin` | Preserve the indentation of a paragraph's first two lines; indent the rest to match the second. |
| `-t`, `--tagged-paragraph` | Like `-c`, but the first line *must* be indented differently from the second, or the two are treated as separate paragraphs. |
| `-q`, `--quick` | Break lines faster, accepting a more ragged right edge. |

## Selecting which lines to format

| Option | Effect |
|---|---|
| `-p`, `--prefix=PREFIX` | Reformat only lines that begin with `PREFIX`; reattach `PREFIX` afterwards. Useful for reflowing comment blocks in source code. |
| `-m`, `--preserve-headers` | Detect and preserve mail-style header lines rather than reflowing them. |
| `--tab-width=N` | Treat a tab as `N` columns when measuring line length. Tabs are kept in the output; this affects measurement only. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or an option value was invalid. |

---

# fold

_Peios / Using Peios / Peiosutils / Transforming text_

> Hard-wrap long lines at a fixed width.

`fold` breaks long lines so that none exceeds a fixed width. Every line longer than the limit is cut into pieces.

```
fold [options] [file...]
```

```
$ fold -w 80 wide.txt
```

By default `fold` wraps at 80 columns, and it cuts at exactly that column — even in the middle of a word. That hard cut is the difference between `fold` and [`fmt`](/peios/using-peios/peiosutils/transforming-text/fmt.md): `fmt` reflows paragraphs and breaks between words; `fold` just enforces a width, mechanically. Use `fold` when you need a guarantee that no line is wider than `N`; use `fmt` when you want the result to read well.

With no file, `fold` reads standard input.

## Options

| Option | Effect |
|---|---|
| `-w`, `--width=WIDTH` | Wrap at `WIDTH` columns instead of 80. |
| `-s`, `--spaces` | Break at a space within the width where one exists, rather than mid-word — a gentler wrap. |
| `-b`, `--bytes` | Count bytes rather than display columns. Control characters such as tab and newline then count as ordinary bytes. |
| `-c`, `--characters` | Count character positions rather than display columns. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or the width was invalid. |

---

# pr

_Peios / Using Peios / Peiosutils / Transforming text_

> Paginate and columnate text for printing.

`pr` prepares text for printing on paper. It breaks a file into **pages**, puts a header and a trailer on each, and can arrange the text into columns.

```
pr [options] [file...]
```

```
$ pr report.txt
```

Run plainly, `pr` divides the input into 66-line pages, each beginning with a five-line header — the date, the file name, and a page number — and ending with a trailing margin. With no file, it reads standard input.

## Columns

`pr` can lay text out in several columns per page.

| Option | Effect |
|---|---|
| `-COLUMN` | Produce `COLUMN` columns per page (e.g. `-3` for three). Text fills down the first column, then the next. |
| `-a`, `--across` | Fill the columns *across* — line 1 in column 1, line 2 in column 2, and so on — rather than down. |
| `-m`, `--merge` | Print several files side by side, one per column. |
| `-w`, `--width=WIDTH` | Set the page width, for multi-column output. |

## The header and the page

| Option | Effect |
|---|---|
| `-h`, `--header=TEXT` | Use `TEXT` in the header instead of the file name. |
| `-t`, `--omit-header` | Print neither the header nor the trailer. |
| `-T`, `--omit-pagination` | Drop the header, the trailer, and all pagination — just the text. |
| `-l`, `--length=LINES` | Set the page length to `LINES` instead of 66. |
| `-o`, `--indent=N` | Indent every line by `N` spaces. |
| `-D`, `--date-format=FORMAT` | Format the header's date with `FORMAT`. |
| `-F`, `-f`, `--form-feed` | Separate pages with a form-feed character rather than blank lines. |

## Selecting and numbering

| Option | Effect |
|---|---|
| `--pages=FIRST[:LAST]` | Print only pages `FIRST` through `LAST`. |
| `-n`, `--number-lines[=SEP[WIDTH]]` | Number every line. |
| `-N`, `--first-line-number=N` | Begin line numbering at `N`. |
| `-d`, `--double-space` | Double-space the output. |

## Other options

| Option | Effect |
|---|---|
| `-s`, `--separator-char[=CHAR]` | Separate columns with a single `CHAR` (default tab) rather than padding with spaces. |
| `-S`, `--sep-string[=STRING]` | Separate columns with `STRING`. |
| `-e`, `--expand-tabs[=CHAR[WIDTH]]` | Expand input tabs to spaces. |
| `-J`, `--join-lines` | Merge full lines, turning off width truncation. |
| `-r`, `--no-file-warnings` | Do not warn when a file cannot be opened. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or an option was invalid. |

---

# ptx

_Peios / Using Peios / Peiosutils / Transforming text_

> Produce a permuted index of the words in a file.

`ptx` produces a **permuted index** — a concordance. For each significant word in the input, it prints a line showing that word together with the text around it, so the same passage appears once per keyword it contains.

```
ptx [options] [input...]
```

```
$ ptx manual.txt
```

A permuted index is the kind of index found at the back of a reference book: every keyword, in alphabetical order, each shown in its context. `ptx` builds one from plain text. With no file, it reads standard input.

## Choosing the keywords

| Option | Effect |
|---|---|
| `-W`, `--word-regexp=REGEXP` | Treat anything matching `REGEXP` as a keyword. |
| `-b`, `--break-file=FILE` | Take the set of word-break characters from `FILE`. |
| `-i`, `--ignore-file=FILE` | Read a list of words to *exclude* from `FILE`. |
| `-o`, `--only-file=FILE` | Index *only* the words listed in `FILE`. |
| `-f`, `--ignore-case` | Fold case together when sorting. |

## Output format

| Option | Effect |
|---|---|
| `-w`, `--width=N` | Set the output width, in columns. |
| `-g`, `--gap-size=N` | Set the gap, in columns, between the output fields. |
| `-A`, `--auto-reference` | Generate a reference (file name and line number) for each entry automatically. |
| `-r`, `--references` | Treat the first field of each input line as a reference for that line. |
| `-R`, `--right-side-refs` | Put references on the right, and exclude them from the width count. |
| `-F`, `--flag-truncation=STRING` | Use `STRING` to mark where a line was truncated. |
| `-O`, `--format=roff` | Emit the index as `roff` typesetting directives. |
| `-T`, `--format=tex` | Emit the index as TeX typesetting directives. |
| `-M`, `--macro-name=NAME` | Use `NAME` as the macro name in `roff`/TeX output. |
| `-G`, `--traditional` | Behave like the older System V `ptx`, without the extended features. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or an option value was invalid. |

---

# tsort

_Peios / Using Peios / Peiosutils / Transforming text_

> Topologically sort items so that each comes after the things it depends on.

`tsort` performs a **topological sort**. Given a set of "this must come before that" rules, it produces an order in which everything appears after the things it depends on.

```
tsort [file]
```

```
$ tsort dependencies.txt
```

With no file, `tsort` reads standard input.

## The input

`tsort` reads its input as a flat sequence of whitespace-separated tokens, taken **in pairs**. Each pair `A B` means "A must come before B" — an edge in a dependency graph.

```
compiler  linker
linker    installer
compiler  installer
```

That input says the linker depends on the compiler, the installer on the linker, and the installer on the compiler. `tsort` reads the pairs and prints the items in a valid order:

```
compiler
linker
installer
```

A token paired with itself (`A A`) simply introduces `A` with no dependency — a way to list an item that nothing else mentions.

## Cycles

A topological order only exists if the dependencies form no **cycle**. If A must come before B and B must come before A, there is no valid order. `tsort` detects this, reports the loop it found, and exits with a failure status — it still prints an order, but the cycle means that order cannot satisfy every rule.

## Where it is used

`tsort` answers "what order do I do these in?" — building components in dependency order, scheduling tasks, sequencing anything where some steps must precede others. It is the dependency-aware counterpart to [`sort`](/peios/using-peios/peiosutils/transforming-text/sort.md), which orders by value rather than by dependency.

## Exit status

| Code | Meaning |
|---|---|
| `0` | A valid order was produced. |
| `1` | The input contained a cycle, had an odd number of tokens, or could not be read. |

---

# wc

_Peios / Using Peios / Peiosutils / Transforming text_

> Count the lines, words, and bytes in files.

`wc` — "word count" — counts what is in a file: its lines, its words, and its bytes.

```
wc [options] [file...]
```

```
$ wc report.txt
  214  1832 12044 report.txt
```

By default `wc` prints three numbers per file — **lines, words, bytes** — followed by the file name. Given several files, it adds a `total` line. With no file, it reads standard input.

## Choosing what to count

Pass any of these and `wc` prints only the counts you ask for, in the fixed order lines, words, characters/bytes, longest-line:

| Option | Counts |
|---|---|
| `-l`, `--lines` | The number of lines. |
| `-w`, `--words` | The number of words — runs of non-whitespace. |
| `-c`, `--bytes` | The number of bytes. |
| `-m`, `--chars` | The number of characters. This differs from `-c` for multi-byte text, where one character is several bytes. |
| `-L`, `--max-line-length` | The length of the longest line. |

```
$ wc -l report.txt        # just the line count
214 report.txt
```

## Other options

| Option | Effect |
|---|---|
| `--files0-from=F` | Take the list of files to count from `F`, as NUL-terminated names. `-` reads the list from standard input. |
| `--total=WHEN` | Control the `total` line: `auto` (the default — shown for more than one file), `always`, `only` (just the total, no per-file lines), or `never`. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every file was counted. |
| `1` | A file could not be read. |

---

# shuf

_Peios / Using Peios / Peiosutils / Transforming text_

> Output the input lines in a random order.

`shuf` shuffles: it outputs its input lines in a random order. Every possible ordering is equally likely.

```
shuf [options] [file]
shuf -e [options] arg...
shuf -i lo-hi [options]
```

```
$ shuf playlist.txt
```

`shuf` is the opposite of [`sort`](/peios/using-peios/peiosutils/transforming-text/sort.md) — instead of imposing an order, it removes one. With no file, it reads standard input.

## Where the lines come from

`shuf` has three ways of getting its input, one per usage form:

| Option | Input is… |
|---|---|
| (a file, or stdin) | the lines of the file. |
| `-e`, `--echo` | the command-line arguments, each treated as one line. |
| `-i`, `--input-range=LO-HI` | the integers from `LO` to `HI`, each treated as one line. |

```
$ shuf -e red green blue          # shuffle three given words
$ shuf -i 1-100                   # the numbers 1..100 in random order
```

## Shaping the output

| Option | Effect |
|---|---|
| `-n`, `--head-count=COUNT` | Output at most `COUNT` lines — a random *sample* rather than the whole shuffle. |
| `-r`, `--repeat` | Allow lines to be repeated, so output can be longer than the input. Pair with `-n` to set how many. |
| `-o`, `--output=FILE` | Write to `FILE` instead of standard output. |
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter. |

`shuf -n 1` is a common idiom — pick one random line.

## Reproducible shuffles

A shuffle is random by default. To get the *same* shuffle every time — for a repeatable test, say — fix the randomness:

| Option | Effect |
|---|---|
| `--random-seed=STRING` | Seed the shuffle with `STRING`. The same seed gives the same shuffle. |
| `--random-source=FILE` | Take the random bytes from `FILE`. The same file gives the same shuffle. Cannot be combined with `--random-seed`. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A failure — the input could not be read, or `-r` was used with no lines to repeat. |

---

# Output and evaluation

_Peios / Using Peios / Peiosutils / Output and evaluation_

> The commands that produce text, work with the environment, convert numbers, and evaluate expressions and conditions.

This topic gathers the small, sharp commands that scripts are built from: the ones that **print** something, that read or set the **environment**, that **evaluate** an expression or a condition, and that simply **succeed or fail**.

## The commands

**Producing text**

| Command | Purpose |
|---|---|
| [`echo`](/peios/using-peios/peiosutils/output-and-evaluation/echo.md) | Print its arguments as a line of text. |
| [`printf`](/peios/using-peios/peiosutils/output-and-evaluation/printf.md) | Print text from a format string, with precise control over layout. |
| [`yes`](/peios/using-peios/peiosutils/output-and-evaluation/yes.md) | Print a line over and over, without stopping. |
| [`seq`](/peios/using-peios/peiosutils/output-and-evaluation/seq.md) | Print a sequence of numbers. |
| [`tee`](/peios/using-peios/peiosutils/output-and-evaluation/tee.md) | Copy standard input to standard output and to each named file. |

**The environment**

| Command | Purpose |
|---|---|
| [`env`](/peios/using-peios/peiosutils/output-and-evaluation/env.md) | Run a command with a modified environment — or print the current one. |
| [`printenv`](/peios/using-peios/peiosutils/output-and-evaluation/printenv.md) | Print environment variables. |

**Numbers**

| Command | Purpose |
|---|---|
| [`numfmt`](/peios/using-peios/peiosutils/output-and-evaluation/numfmt.md) | Convert numbers to and from human-readable forms (`1.5G`, `1000000`). |
| [`factor`](/peios/using-peios/peiosutils/output-and-evaluation/factor.md) | Print the prime factors of a number. |

**Evaluating**

| Command | Purpose |
|---|---|
| [`expr`](/peios/using-peios/peiosutils/output-and-evaluation/expr.md) | Evaluate an arithmetic, string, or comparison expression. |
| [`test`](/peios/using-peios/peiosutils/output-and-evaluation/test.md) | Evaluate a condition — a file check or a comparison — as a true/false result. |

**Exit status**

| Command | Purpose |
|---|---|
| [`true` and `false`](/peios/using-peios/peiosutils/output-and-evaluation/true-and-false.md) | Do nothing, and succeed — or do nothing, and fail. |

## A note on exit status

Several commands here exist to be used for their **exit status** rather than their output. Every command, when it finishes, leaves behind a number: `0` means success, anything else means a kind of failure. A shell's `if`, `&&`, and `||` all act on that number.

[`test`](/peios/using-peios/peiosutils/output-and-evaluation/test.md) is the clearest case — it prints nothing and exists *only* to set an exit status from a condition. [`true` and `false`](/peios/using-peios/peiosutils/output-and-evaluation/true-and-false.md) are the most minimal: fixed success and fixed failure. Where this topic's pages give an exit-status table, that number is often the whole point of the command.

## Where to start

For everyday printing, [`echo`](/peios/using-peios/peiosutils/output-and-evaluation/echo.md); for anything that needs columns, padding, or precise formatting, [`printf`](/peios/using-peios/peiosutils/output-and-evaluation/printf.md).

---

# echo

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Print arguments as a line of text to standard output.

`echo` prints its arguments, separated by single spaces, followed by a newline.

```
echo [options] [string...]
```

```
$ echo Hello, Peios
Hello, Peios
```

It is the simplest way to put a line of text on standard output — printing a message, or feeding a fixed string into a pipe.

## Options

| Option | Effect |
|---|---|
| `-n` | Do not print the trailing newline. |
| `-e` | Interpret backslash escape sequences in the arguments (see below). |
| `-E` | Do **not** interpret escape sequences. This is the default. |

## Escape sequences

With `-e`, `echo` recognises these backslash sequences and prints the character they stand for:

| Sequence | Character |
|---|---|
| `\\` | A literal backslash. |
| `\n` | Newline. |
| `\t` | Horizontal tab. |
| `\r` | Carriage return. |
| `\b` | Backspace. |
| `\f` | Form feed. |
| `\v` | Vertical tab. |
| `\a` | Alert (the terminal bell). |
| `\e` | Escape. |
| `\0NNN` | The byte with octal value `NNN`. |
| `\xHH` | The byte with hexadecimal value `HH`. |
| `\c` | Stop — produce no further output. |

## `echo` and `printf`

`echo` is fixed: arguments, spaces, a newline. The moment you need *control* over the output — columns, padding, a number formatted a particular way, output with no spaces between pieces — use [`printf`](/peios/using-peios/peiosutils/output-and-evaluation/printf.md) instead. `printf` is also more predictable across environments, since `echo`'s handling of escapes and `-n` varies.

## A note on shells

Many command shells provide their own built-in `echo`, and the shell's version is what runs when you type `echo` at a prompt. Built-in versions vary — especially in how they treat `-n`, `-e`, and escape sequences — so their behaviour may differ from what is described here. To be certain you are running this command rather than the shell built-in, invoke it by its full path.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The output was written. |
| `1` | The output could not be written. |

---

# printf

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Print formatted text from a format string and a list of arguments.

`printf` prints text built from a **format string**. The format string is printed literally, except that escape sequences become characters and substitution fields are replaced by the arguments that follow.

```
printf format [argument...]
```

```
$ printf 'the letter %X comes before %X\n' 10 11
the letter A comes before B
```

Where [`echo`](/peios/using-peios/peiosutils/output-and-evaluation/echo.md) prints arguments as-is, `printf` gives you control: padding, column widths, number bases, fixed decimal places. It is the command for output that has to *line up* or *be exact*.

`printf` writes only what the format string says — there is no automatic trailing newline. Include `\n` yourself where you want one.

## Escape sequences

The format string interprets these backslash sequences:

| Sequence | Character |
|---|---|
| `\\` | Backslash. |
| `\n` `\t` `\r` | Newline, tab, carriage return. |
| `\b` `\f` `\v` `\a` | Backspace, form feed, vertical tab, alert. |
| `\e` | Escape. |
| `\NNN` | The byte with octal value `NNN`. |
| `\xHH` | The byte with hexadecimal value `HH`. |
| `\uHHHH`, `\UHHHHHHHH` | A Unicode character by hexadecimal code point. |
| `%%` | A literal `%`. |

## Substitution fields

A field begins with `%` and is replaced by the next argument, formatted accordingly.

| Field | Formats the argument as… |
|---|---|
| `%s` | a string. |
| `%b` | a string, with backslash escapes in it interpreted. |
| `%q` | a string, quoted so it is safe to reuse as shell input. |
| `%c` | a single character. |
| `%d`, `%i` | a signed integer. |
| `%u` | an unsigned integer. |
| `%x`, `%X` | an unsigned integer in hexadecimal (lower- or upper-case). |
| `%o` | an unsigned integer in octal. |
| `%f`, `%F` | a decimal floating-point number. |
| `%e`, `%E` | a number in scientific notation. |
| `%g`, `%G` | whichever of decimal or scientific notation is shorter. |

## Width and precision

Between the `%` and the field letter you may put two numbers — `%[width][.precision]field`:

- **width** — the minimum number of columns. Output narrower than this is padded with leading spaces; a negative width pads on the right instead.
- **precision** — after a `.`; its meaning depends on the field. For a string it is a *maximum length*; for an integer, a minimum digit count (zero-padded); for a float, the number of decimal places.

```
$ printf '%-10s %5.2f\n' apples 3.1
apples         3.10
```

## Reusing the format string

If more arguments are supplied than the format string has fields, `printf` **repeats the format string** until the arguments run out. If there are too few, the leftover fields default to an empty string (or `0` for a number).

```
$ printf '%s: %d\n' alpha 1 beta 2 gamma 3
alpha: 1
beta: 2
gamma: 3
```

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A missing operand, a bad format string, or a write failure. |

---

# yes

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Print a line repeatedly until stopped.

`yes` prints a line over and over, without stopping.

```
yes [string...]
```

```
$ yes
y
y
y
...
```

With no argument it prints `y` forever. Given arguments, it prints them — joined by spaces — as the repeated line instead.

```
$ yes retry
retry
retry
...
```

`yes` never stops on its own. It ends when something downstream closes the pipe, or when you interrupt it with Ctrl-C.

## What it is for

`yes` exists to **answer prompts**. An older interactive command that asks "are you sure? [y/n]" again and again can be fed a stream of confirmations:

```
$ yes | slow-interactive-command
```

Most modern commands have a proper option for this — `rm -f`, and similar — and that is the better way when it exists. `yes` is the fallback for a command that offers no such option. It is also a quick way to generate an endless stream of identical lines for a test.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The output was closed normally. |
| `1` | A write error occurred. |

---

# seq

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Print a sequence of numbers from a start to an end.

`seq` prints a sequence of numbers, one per line.

```
seq [options] last
seq [options] first last
seq [options] first increment last
```

```
$ seq 3
1
2
3
$ seq 2 2 10
2
4
6
8
10
```

The three forms control where the sequence starts and how it steps:

- `seq last` — count from 1 up to `last`.
- `seq first last` — count from `first` up to `last`.
- `seq first increment last` — count from `first` to `last`, stepping by `increment`.

The increment may be negative, to count down, and the numbers may be fractional. `seq` is most often used to drive a loop a fixed number of times.

## Options

| Option | Effect |
|---|---|
| `-s`, `--separator=STRING` | Separate the numbers with `STRING` instead of a newline. |
| `-t`, `--terminator=STRING` | End the output with `STRING` instead of a newline. |
| `-w`, `--equal-width` | Pad the numbers with leading zeros so they all have the same width. |
| `-f`, `--format=FORMAT` | Format each number with a `printf`-style floating-point `FORMAT`. |

```
$ seq -s , 1 5
1,2,3,4,5
$ seq -w 8 10
08
09
10
```

`-f` takes a single floating-point conversion — see [`printf`](/peios/using-peios/peiosutils/output-and-evaluation/printf.md) for the format syntax — and cannot be combined with `-w`.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The sequence was printed. |
| `1` | An argument was not a valid number, the increment was zero, or no argument was given. |

---

# env

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Run a command with a modified environment, or print the current environment.

`env` runs a command with a changed **environment** — the set of `NAME=VALUE` variables a process inherits. Run with no command, it prints the current environment instead.

```
env [options] [NAME=VALUE]... [command [arg]...]
```

```
$ env LANG=C TZ=UTC my-program
$ env
PATH=/bin
HOME=/home/jack
...
```

## How it works

You give `env` a list of `NAME=VALUE` assignments, then a command. `env` applies the assignments to its own environment and then runs the command, which inherits the result. The assignments affect only that one run — your shell's environment is untouched.

This is the clean way to run something with a particular variable set, without changing it for everything else, and the standard way to run a command with one variable temporarily different.

With **no command**, `env` simply prints the environment it would have used — which, with no options, is the current one.

## Building the environment

| Option | Effect |
|---|---|
| `-i`, `--ignore-environment` | Start from an *empty* environment, not the inherited one — so the command sees only what you assign. A bare `-` argument means the same. |
| `-u`, `--unset=NAME` | Remove `NAME` from the environment. |
| `--file=FILE` | Read `NAME=VALUE` lines from a `.env`-style `FILE` and apply them. |

The order is: start (inherited, or empty with `-i`), apply `--file`, apply `--unset`, apply the `NAME=VALUE` arguments.

## Running the command

| Option | Effect |
|---|---|
| `-C`, `--chdir=DIR` | Change to `DIR` before running the command. |
| `--argv0=NAME` | Run the command but present `NAME` as its zeroth argument. |
| `-S`, `--split-string=S` | Split `S` into separate arguments. This exists for script interpreter lines, where everything after the interpreter arrives as one string. |

## Signal handling

`env` can adjust how the command treats signals before launching it:

| Option | Effect |
|---|---|
| `--ignore-signal[=SIG]` | Set the named signals to be ignored. |
| `--default-signal[=SIG]` | Reset the named signals to their default handling. |
| `--block-signal[=SIG]` | Block delivery of the named signals while the command runs. |
| `--list-signal-handling` | List the signal-handling changes the other options requested. |

## Other options

| Option | Effect |
|---|---|
| `-0`, `--null` | When printing the environment, end each line with a NUL character instead of a newline. Not valid together with a command. |
| `-v`, `--debug` | Print each processing step `env` performs. |

## Exit status

When `env` runs a command, it exits with **that command's** status. The exception is a failure in `env` itself:

| Code | Meaning |
|---|---|
| (command's own) | The command ran; this is its exit status. |
| `125` | `env` itself failed — for example, a bad option. |
| `126` | The command was found but could not be run. |
| `127` | The command was not found. |

---

# printenv

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Print the values of environment variables.

`printenv` prints environment variables.

```
printenv [options] [variable...]
```

Name one or more variables and `printenv` prints the value of each. Name none and it prints the whole environment, one `NAME=VALUE` pair per line.

```
$ printenv HOME
/home/jack
$ printenv
PATH=/bin
HOME=/home/jack
...
```

## Options

| Option | Effect |
|---|---|
| `-0`, `--null` | End each output line with a NUL character instead of a newline — so values that themselves contain newlines can still be told apart. |

## `printenv` and `env`

Both can print the environment. `printenv` is the one built for *reading* it — it can pick out a single variable by name. [`env`](/peios/using-peios/peiosutils/output-and-evaluation/env.md) prints the environment too, but its real job is *running a command* with a modified one. Use `printenv` to look something up; use `env` to change something for a command.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every named variable was found and printed. |
| `1` | A named variable was not set. |
| `2` | A usage error. |

---

# numfmt

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Convert numbers between plain digits and human-readable forms.

`numfmt` converts numbers between plain digits and **human-readable** forms — turning `1500000` into `1.5M`, or `1.5M` back into `1500000`.

```
numfmt [options] [number...]
```

```
$ numfmt --to=si 1500000
1.5M
$ numfmt --from=si 1.5M
1500000
```

`numfmt` takes numbers as arguments, or — with none — reads them from standard input, which lets it rescale a column of numbers flowing through a pipe.

## The direction of conversion

| Option | Effect |
|---|---|
| `--from=UNIT` | Parse input numbers that carry a `UNIT` suffix, scaling them up to plain digits. |
| `--to=UNIT` | Scale output numbers down and add a `UNIT` suffix. |

A `UNIT` is one of:

| Unit | Suffixes mean |
|---|---|
| `none` | No suffixes — a suffix is an error. The default. |
| `si` | Powers of 1000: `1K` = 1000, `1M` = 1000000. |
| `iec` | Powers of 1024: `1K` = 1024, `1M` = 1048576. |
| `iec-i` | Powers of 1024 with a two-letter suffix: `1Ki` = 1024, `1Mi` = 1048576. |
| `auto` | Accept any of the above on input, reading `Ki`/`Mi` as powers of 1024 and bare `K`/`M` as powers of 1000. |

## Working on fields

By default `numfmt` converts the whole input line. To convert numbers sitting in a *column* of wider text, name the field:

| Option | Effect |
|---|---|
| `--field=FIELDS` | Convert only the numbers in these fields. `FIELDS` uses the same range syntax as `cut` — `3`, `2-5`, `2-`. |
| `-d`, `--delimiter=X` | Use `X` to separate fields instead of whitespace. |
| `--header[=N]` | Pass the first `N` lines through unconverted, as a header. |

## Shaping the output

| Option | Effect |
|---|---|
| `--format=FORMAT` | Format the number with a `printf`-style floating-point `FORMAT`, controlling width, padding, and precision. |
| `--padding=N` | Pad the output to `N` columns — positive right-aligns, negative left-aligns. |
| `--grouping` | Group digits according to the locale (for example, `1,000,000`). |
| `--round=METHOD` | Choose the rounding method used when scaling. |
| `--suffix=SUFFIX` | Append `SUFFIX` to each result, and accept it on input. |
| `--from-unit=N` / `--to-unit=N` | Set the unit size the input or output is counted in. |

## Other options

| Option | Effect |
|---|---|
| `--invalid=MODE` | Choose what to do with input that is not a valid number: `abort`, `fail`, `warn`, or `ignore`. |
| `-z`, `--zero-terminated` | Treat the NUL character as the line delimiter. |
| `--debug` | Print warnings about questionable input. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every number was converted. |
| `2` | An input was not a valid number, or an option was misused. |

---

# expr

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Evaluate an arithmetic, string, or comparison expression and print the result.

`expr` evaluates a single expression and prints its value.

```
expr expression
```

```
$ expr 6 + 7
13
$ expr length "Peios"
5
```

Each part of the expression — every number, operator, and keyword — is a separate argument. That is why the spaces matter: `expr 6+7` is one argument and not an expression, while `expr 6 + 7` is three.

## Arithmetic

| Operator | Result |
|---|---|
| `ARG1 + ARG2` | Sum. |
| `ARG1 - ARG2` | Difference. |
| `ARG1 * ARG2` | Product. |
| `ARG1 / ARG2` | Integer quotient. |
| `ARG1 % ARG2` | Remainder. |

## Comparison

A comparison yields `1` for true and `0` for false. It is arithmetic when both sides are numbers, and lexical otherwise.

| Operator | True when… |
|---|---|
| `ARG1 = ARG2` | the two are equal. |
| `ARG1 != ARG2` | they are unequal. |
| `ARG1 < ARG2` | `ARG1` is less. |
| `ARG1 <= ARG2` | `ARG1` is less or equal. |
| `ARG1 > ARG2` | `ARG1` is greater. |
| `ARG1 >= ARG2` | `ARG1` is greater or equal. |

## Logic

| Operator | Result |
|---|---|
| `ARG1 \| ARG2` | `ARG1` if it is neither null nor `0`; otherwise `ARG2`. |
| `ARG1 & ARG2` | `ARG1` if neither argument is null or `0`; otherwise `0`. |

## String operations

| Form | Result |
|---|---|
| `length STRING` | The number of characters in `STRING`. |
| `substr STRING POS LENGTH` | `LENGTH` characters of `STRING`, counting `POS` from 1. |
| `index STRING CHARS` | The position of the first of any of `CHARS` in `STRING`, or `0`. |
| `STRING : REGEXP` | Match `REGEXP` anchored at the start of `STRING`. Returns the matched length, or the text captured by `\(…\)`. |
| `match STRING REGEXP` | The same as `STRING : REGEXP`. |

Many of these operators — `*`, `<`, `|`, `(` — are also meaningful to a shell, so quote or escape them so they reach `expr` intact.

## Exit status

`expr`'s exit status reports on the *value* it computed:

| Code | Meaning |
|---|---|
| `0` | The result was neither null nor `0`. |
| `1` | The result was null or `0`. |
| `2` | The expression was syntactically invalid. |
| `3` | An error occurred during evaluation — such as division by zero. |

---

# factor

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Print the prime factors of numbers.

`factor` prints the prime factors of a number.

```
factor [options] [number...]
```

```
$ factor 360
360: 2 2 2 3 3 5
```

Each line of output is the original number, a colon, and its prime factors in increasing order, repeated as often as they divide in. Give several numbers and `factor` factors each; give none and it reads numbers from standard input.

## Options

| Option | Effect |
|---|---|
| `-h`, `--exponents` | Write a repeated factor in exponent form — `p^e` — instead of repeating it. |

```
$ factor -h 360
360: 2^3 3^2 5
```

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every number was factored. |
| `1` | An input was not a valid positive integer. |

---

# test

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Evaluate a condition — a file check or a comparison — and report the result as an exit status.

`test` evaluates a single condition and reports whether it is **true** or **false**. It prints nothing — the answer is its exit status: `0` for true, `1` for false. That makes it the command a shell's `if`, `while`, `&&`, and `||` are built on.

```
test expression
[ expression ]
```

The two forms are the same command. `[` is `test` under another name; when invoked as `[`, it requires a closing `]` as its last argument. `[ -f notes.txt ]` and `test -f notes.txt` are identical.

```
$ test -f notes.txt && echo "the file exists"
```

## File tests

### Existence and type

| Test | True when the file… |
|---|---|
| `-e FILE` | exists. |
| `-f FILE` | exists and is a regular file. |
| `-d FILE` | exists and is a directory. |
| `-L FILE`, `-h FILE` | exists and is a symbolic link. |
| `-p FILE` | exists and is a named pipe (FIFO). |
| `-S FILE` | exists and is a socket. |
| `-b FILE` | exists and is a block device. |
| `-c FILE` | exists and is a character device. |
| `-s FILE` | exists and is not empty. |

### Access

These three tests ask whether you may actually *use* the file:

| Test | True when… |
|---|---|
| `-r FILE` | you may read the file. |
| `-w FILE` | you may write the file. |
| `-x FILE` | you may execute the file, or search the directory. |

On Peios these are **real access checks**. `test` asks the kernel whether your token is granted the right in question — a live check against the file's [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md), the same decision any actual read, write, or execute would face. The answer is therefore honest: `test -w FILE` is true exactly when a write would be permitted.

`-x` on a regular file means "you are permitted to execute it" — which is a different question from whether the file *is* an executable. A file can be runnable code and still fail `-x` for you, and the two are decided separately; see [Access decisions](/peios/security-fundamentals/access-decisions/overview.md).

### Comparing two files

| Test | True when… |
|---|---|
| `FILE1 -nt FILE2` | `FILE1` is newer than `FILE2`. |
| `FILE1 -ot FILE2` | `FILE1` is older than `FILE2`. |
| `FILE1 -ef FILE2` | both name the same file (same device and inode). |

### Other file tests

| Test | True when the file… |
|---|---|
| `-t FD` | file descriptor `FD` is open on a terminal. |
| `-N FILE` | has been modified since it was last read. |
| `-O FILE`, `-G FILE` | carries an owner-id / group-id field matching the caller's. |
| `-g FILE`, `-u FILE`, `-k FILE` | has its set-group-id, set-user-id, or sticky inode bit set. |

The last two rows probe **decorative inode fields**. The Peios access model does not consult them — they are stored on the inode and reported for completeness, but they carry no authority. For a true picture of what may be done to a file, use the access tests `-r`, `-w`, `-x` above, not `-O`, `-g`, or `-u`.

## String tests

| Test | True when… |
|---|---|
| `-n STRING` | `STRING` is not empty. A bare `STRING` means the same. |
| `-z STRING` | `STRING` is empty. |
| `STRING1 = STRING2` | the strings are equal. |
| `STRING1 != STRING2` | the strings are unequal. |
| `STRING1 < STRING2` | `STRING1` sorts before `STRING2`. |
| `STRING1 > STRING2` | `STRING1` sorts after `STRING2`. |

## Integer comparisons

| Test | True when… |
|---|---|
| `A -eq B` | `A` equals `B`. |
| `A -ne B` | `A` does not equal `B`. |
| `A -lt B` | `A` is less than `B`. |
| `A -le B` | `A` is less than or equal to `B`. |
| `A -gt B` | `A` is greater than `B`. |
| `A -ge B` | `A` is greater than or equal to `B`. |

## Combining conditions

| Form | Result |
|---|---|
| `! EXPRESSION` | The negation. |
| `( EXPRESSION )` | Grouping — escape the parentheses so the shell does not take them. |
| `EXPR1 -a EXPR2` | True when both are true. |
| `EXPR1 -o EXPR2` | True when either is true. |

`-a` and `-o` are genuinely ambiguous to parse and are best avoided. Combine separate `test` calls with the shell's own `&&` and `||` instead:

```
test -f notes.txt && test -r notes.txt
```

## A note on shells

Many command shells provide their own built-in `test` and `[`, and the shell's version is what runs when you type `test` at a prompt or in a script. A built-in may not behave as described here — in particular, the access tests `-r`, `-w`, and `-x` are the real access checks described above only when *this* command runs. To be certain you are running this command rather than the shell built-in, invoke it by its full path.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The expression was true. |
| `1` | The expression was false. |
| `2` | The expression was malformed. |

---

# true and false

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Two commands that do nothing — one always succeeds, the other always fails.

`true` and `false` do nothing at all. Their only product is an **exit status**: `true` always succeeds, `false` always fails.

```
true
false
```

```
$ true ; echo $?
0
$ false ; echo $?
1
```

`true` exits `0`. `false` exits `1`. They take no arguments that matter, produce no output, and have no effect on anything.

## What they are for

A command that is *only* an exit status is useful as a fixed value in places that expect a command:

- **An always-true or always-false loop.** `while true` is the standard way to write a loop that runs until something inside it breaks out.
- **A deliberate placeholder.** Setting a configurable command to `true` makes that step do nothing and "succeed"; `false` makes it a step that always fails.
- **A known result in a test.** When a script or a condition needs a guaranteed pass or fail to check against, `true` and `false` provide it.

They are the simplest commands there are — and the [overview](/peios/using-peios/peiosutils/output-and-evaluation/overview.md)'s note on exit status is the whole idea behind them.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Returned by `true`, always. |
| `1` | Returned by `false`, always. |

---

# tee

_Peios / Using Peios / Peiosutils / Output and evaluation_

> Copy standard input to standard output and to each named file at once.

`tee` reads standard input and writes it, unchanged, to standard output **and** to each file you name. It is the T-junction of a pipeline: the data keeps flowing down the pipe while a copy is also captured on disk.

```
tee [options] [file...]
```

```
$ echo saved | tee note.txt
saved
$ cat note.txt
saved
```

The name is the shape of the letter T — one stream in, the same stream out two ways. That makes `tee` the tool for the moment you want to both *see* (or keep piping) some data and *keep* it at the same time.

## Writing to several places at once

Every byte that arrives on standard input is written to standard output and to each named file. Name more than one file and each one receives a full, identical copy:

```
$ producer | tee a.log b.log c.log | consumer
```

Here `producer`'s output reaches `consumer` down the pipe exactly as if `tee` were not there, and `a.log`, `b.log`, and `c.log` each end up holding the same complete copy of it. With no files named, `tee` copies input straight to standard output and nothing else — a plain pass-through.

`tee` does not buffer between reads: it writes each chunk out as it is read, so a file being written by `tee` fills up as the data flows, rather than all at once when the input ends.

## Overwrite or append

By default, each named file is **truncated** — opened fresh, so any existing contents are discarded before the new data is written. A file that does not exist is created.

With `-a` (`--append`), `tee` **appends** to each named file instead: existing contents are kept and the new data is added to the end. A file that does not exist is still created.

```
$ echo first  | tee log.txt        # log.txt now holds "first"
$ echo second | tee -a log.txt     # log.txt now holds "first" then "second"
```

## Creating a file inherits a security descriptor

When `tee` **creates** a new output file — a named file that did not already exist — that file needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md), and it inherits one from the directory it is created in. This is the same creation-inherits-a-descriptor rule that governs every file-creating tool on Peios; see [Files and directories](/peios/using-peios/peiosutils/files-and-directories/overview.md) for the shared model.

With `-a`, when the named file already exists, `tee` opens that existing file to append and creates nothing, so no new descriptor is involved. Whether opening a file to write succeeds — whether creating a new one or writing an existing one — is decided by the normal access check against the relevant directory or file. `tee` reports a file it cannot open and, by default, carries on with the outputs it *can* write.

## Files named `-`

`tee` gives no special meaning to `-`. A file argument of `-` refers to a file literally named `-` in the current directory; it is opened, created, and written exactly like any other named file. It is **not** a stand-in for standard output — standard output is always written and needs no naming.

## Ignoring interrupts

With `-i` (`--ignore-interrupts`), `tee` ignores the interrupt signal (the one a terminal sends on Ctrl-C). This lets `tee` keep copying to its files even as an interrupt tears down the rest of a pipeline, so the capture is not cut short.

## Behaviour on write errors

By default, if a write to one output fails, `tee` reports it and stops writing to *that* output, but keeps writing to the others; a broken-pipe error on standard output is treated quietly. Two options change this policy.

`-p` sets write-error behaviour so that errors on a pipe (a downstream reader that has gone away) are ignored while other write errors are still reported.

`--output-error[=MODE]` sets the policy explicitly. Given on its own, with no `=MODE`, it selects `warn-nopipe`. The modes are:

| Mode | Behaviour |
|---|---|
| `warn` | Warn on a write error to **any** output, including a broken pipe, and continue with the remaining outputs. |
| `warn-nopipe` | Warn on a write error that is **not** a pipe error, and continue. Broken-pipe errors are ignored. This is the mode selected by a bare `--output-error`. |
| `exit` | **Exit** on a write error to any output, including a broken pipe. |
| `exit-nopipe` | Exit on a write error that is **not** a pipe error. Broken-pipe errors are ignored. |

## Options

| Option | Effect |
|---|---|
| `-a`, `--append` | Append to each named file rather than truncating it. |
| `-i`, `--ignore-interrupts` | Ignore the interrupt signal. |
| `-p` | Ignore errors from writing to a broken pipe, still reporting other write errors. |
| `--output-error[=MODE]` | Set the write-error policy: `warn`, `warn-nopipe`, `exit`, or `exit-nopipe`. A bare `--output-error` means `warn-nopipe`. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The input was copied to standard output and every named file without error. |
| `1` | A file could not be opened, a write failed under a policy that reports or exits on it, or the input could not be read. |

---

# Hashing and encoding

_Peios / Using Peios / Peiosutils / Hashing and encoding_

> The commands that compute checksums and hashes to verify data integrity, and that encode binary data into text and back.

This topic covers two related jobs. **Hashing** reduces a file to a short fixed-size value — a checksum — that can be used to tell whether the file has changed. **Encoding** rewrites binary data as plain text so it can travel safely through channels that only handle text.

## The commands

**Checksums and hashes**

| Command | Purpose |
|---|---|
| [checksum commands](/peios/using-peios/peiosutils/hashing-and-encoding/checksum-commands.md) | `md5sum`, `sha1sum`, `sha256sum`, and the rest — one command per hash algorithm. |
| [`cksum`](/peios/using-peios/peiosutils/hashing-and-encoding/cksum.md) | The general checksum tool — any of the algorithms, selected by an option. |
| [`sum`](/peios/using-peios/peiosutils/hashing-and-encoding/sum.md) | A small, legacy block-checksum. |

**Encoding**

| Command | Purpose |
|---|---|
| [`base32` and `base64`](/peios/using-peios/peiosutils/hashing-and-encoding/base32-and-base64.md) | Encode binary data as text using the base32 or base64 alphabet, and decode it back. |
| [`basenc`](/peios/using-peios/peiosutils/hashing-and-encoding/basenc.md) | The general encoder — base64, base32, base16, and several more, selected by an option. |

## What these commands are not

It is worth being clear up front, because both jobs are easy to mistake for something they are not.

**A hash is not encryption.** Hashing is one-way: a checksum tells you *whether* data has changed, but the original cannot be recovered from it. It protects against corruption and detects tampering — it does not keep anything secret.

**Encoding is not encryption either.** `base64` rewrites data into a text-safe form, but anyone can decode it straight back — it is a change of *representation*, not of secrecy. Encoding something does not protect it.

Neither hashing nor encoding hides data. They are about *integrity* and *transport*, not confidentiality.

## Where to start

To verify a file you downloaded against a published checksum, read the [checksum commands](/peios/using-peios/peiosutils/hashing-and-encoding/checksum-commands.md). To turn binary data into something safe to paste into text, read [`base32` and `base64`](/peios/using-peios/peiosutils/hashing-and-encoding/base32-and-base64.md).

---

# Checksum commands

_Peios / Using Peios / Peiosutils / Hashing and encoding_

> md5sum, sha1sum, sha256sum and the rest — the family of commands that each compute and verify one fixed hash algorithm.

This page covers a family of commands that all work the same way and differ only in **which hash algorithm** they use:

| Command | Algorithm | Digest size |
|---|---|---|
| `md5sum` | MD5 | 128-bit |
| `sha1sum` | SHA-1 | 160-bit |
| `sha224sum` | SHA-224 | 224-bit |
| `sha256sum` | SHA-256 | 256-bit |
| `sha384sum` | SHA-384 | 384-bit |
| `sha512sum` | SHA-512 | 512-bit |
| `b2sum` | BLAKE2b | up to 512-bit |

```
sha256sum [options] [file...]
```

Everything below is written with `sha256sum`, but applies to every command in the table.

## Computing a checksum

Run plainly, the command prints the hash of each file, followed by the file name:

```
$ sha256sum installer.iso
9f86d0818884...b1a5  installer.iso
```

With no file, it reads standard input. The hash is a fingerprint of the file's contents: change a single byte and the hash changes completely.

## Verifying with a checksum

The everyday use is **verification** — confirming a file is exactly what it should be, usually a download.

First, the file's publisher computes a checksum and publishes it, often in a file:

```
$ sha256sum installer.iso > installer.iso.sha256
```

Then anyone with the file and that checksum file can verify:

```
$ sha256sum -c installer.iso.sha256
installer.iso: OK
```

`-c` reads each `hash  filename` line, recomputes the hash, and reports `OK` or `FAILED`. A `FAILED` means the file is not the one the checksum was made from — corrupted in transit, or altered.

| Option | Effect |
|---|---|
| `-c`, `--check` | Read checksums from the given files and verify them. |
| `--ignore-missing` | In check mode, do not fail over files that are listed but absent. |
| `--quiet` | In check mode, print nothing for files that pass — only failures. |
| `--status` | In check mode, print nothing at all; report only through the exit status. |
| `-w`, `--warn` | Warn about improperly formatted lines in the checksum file. |
| `--strict` | In check mode, fail if any checksum line is malformed. |

## Output format

| Option | Effect |
|---|---|
| `--tag` | Produce a tagged line — `SHA256 (file) = hash` — that records which algorithm was used. |
| `--untagged` | Produce the plain `hash  file` line. This is the default. |
| `-z`, `--zero` | End each output line with a NUL character instead of a newline. |
| `-b`, `--binary` | Note the file as read in binary mode. |
| `-t`, `--text` | Note the file as read in text mode. |

`b2sum` additionally accepts `-l`, `--length=BITS` to produce a shorter BLAKE2b digest.

## A note on choosing an algorithm

These commands detect change — but not all of them resist a *deliberate* attempt to forge a match. MD5 and SHA-1 can be defeated by an attacker who wants two different files to share a checksum. They are still fine for catching accidental corruption, but for verifying that a file has not been **tampered with**, use a SHA-2 command (`sha256sum` and up) or `b2sum`.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The checksums were computed, or — in check mode — every file verified. |
| `1` | In check mode, a file failed verification. |
| `2` | An error — a file could not be read, or a checksum file was malformed. |

---

# cksum

_Peios / Using Peios / Peiosutils / Hashing and encoding_

> The general checksum tool — compute or verify a digest with any of the supported algorithms.

`cksum` is the **general** checksum command. Where each of the [checksum commands](/peios/using-peios/peiosutils/hashing-and-encoding/checksum-commands.md) is fixed to one algorithm, `cksum` does any of them — the algorithm is an option.

```
cksum [options] [file...]
```

```
$ cksum installer.iso
3915528286 372736000 installer.iso
$ cksum -a sha256 installer.iso
9f86d0818884...b1a5  installer.iso
```

With no `-a`, `cksum` computes a CRC checksum and prints the CRC value, the file's byte count, and the name. With `-a`, it behaves like the corresponding dedicated command.

## Choosing the algorithm

| Option | Effect |
|---|---|
| `-a`, `--algorithm=NAME` | Use the named algorithm. |

`NAME` may be:

| Name | Algorithm |
|---|---|
| `crc` | The default CRC. |
| `crc32b` | A CRC-32 variant. |
| `md5` | MD5 — as `md5sum`. |
| `sha1` | SHA-1 — as `sha1sum`. |
| `sha224`, `sha256`, `sha384`, `sha512` | The SHA-2 family — as the matching `sha*sum`. |
| `sha3` | SHA-3. |
| `blake2b` | BLAKE2b — as `b2sum`. |
| `sm3` | The SM3 hash. |
| `sysv`, `bsd` | The legacy block checksums — as [`sum`](/peios/using-peios/peiosutils/hashing-and-encoding/sum.md). |

`crc`, `crc32b`, `sha3`, and `sm3` are available *only* through `cksum` — there is no dedicated command for them.

## Verifying

`cksum` checks files the same way the dedicated commands do:

| Option | Effect |
|---|---|
| `-c`, `--check` | Read checksums and verify the files against them. |
| `--ignore-missing` | Do not fail over listed files that are absent. |
| `--quiet` | Print nothing for files that pass. |
| `--status` | Print nothing; report only through the exit status. |
| `-w`, `--warn` | Warn about malformed checksum lines. |
| `--strict` | Fail on a malformed checksum line. |

## Output format

| Option | Effect |
|---|---|
| `--tag` | Produce a tagged `ALGORITHM (file) = hash` line. The default for most algorithms. |
| `--untagged` | Produce a plain `hash  file` line. |
| `--raw` | Output the raw binary digest, with nothing else. |
| `--base64` | Print the digest in base64 rather than hexadecimal. |
| `-l`, `--length=BITS` | For an algorithm that supports it, produce a shorter digest. |
| `-z`, `--zero` | End each output line with a NUL character. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success — or, in check mode, every file verified. |
| `1` | In check mode, a file failed verification. |
| `2` | An error — a file could not be read, or an option was invalid. |

---

# sum

_Peios / Using Peios / Peiosutils / Hashing and encoding_

> Compute a small, legacy block checksum of a file.

`sum` computes a small, old-style checksum of a file and reports it along with the file's size in blocks.

```
sum [options] [file...]
```

```
$ sum archive.tar
12345 84 archive.tar
```

The output is the checksum, the block count, and the file name. With no file, `sum` reads standard input.

## Which algorithm

`sum` predates the modern hashes and offers two historical algorithms:

| Option | Algorithm |
|---|---|
| `-r` | The BSD checksum, counted in 1 KiB blocks. This is the default. |
| `-s`, `--sysv` | The System V checksum, counted in 512-byte blocks. |

## When to use it

`sum` exists for compatibility — for reading checksums produced by old tools, and for scripts that still expect its output. Its checksum is short and weak: it catches some accidental corruption, but it is easily fooled and gives no protection against tampering.

For anything new, do not use `sum`. Use a [checksum command](/peios/using-peios/peiosutils/hashing-and-encoding/checksum-commands.md) such as `sha256sum`, or [`cksum`](/peios/using-peios/peiosutils/hashing-and-encoding/cksum.md) — both of which `cksum` can still produce, via `cksum -a bsd` and `cksum -a sysv`, if you need the legacy values.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The checksum was computed. |
| `1` | A file could not be read. |

---

# base32 and base64

_Peios / Using Peios / Peiosutils / Hashing and encoding_

> Encode binary data as text using the base32 or base64 alphabet, and decode it back.

`base32` and `base64` encode binary data as plain text — and decode it back. They are the same command with one difference: the **alphabet** they use.

```
base32 [options] [file]
base64 [options] [file]
```

```
$ echo Peios | base64
UGVpb3MK
$ echo UGVpb3MK | base64 -d
Peios
```

## What encoding is for

Some channels only carry text safely — they mangle or reject arbitrary bytes. Encoding rewrites binary data using a small, safe set of characters, so it can pass through unharmed; decoding reverses it exactly. `base64` uses 64 characters and is the more compact; `base32` uses 32 and is more robust where case might not survive. With no file, both read standard input.

Encoding is **not** encryption — anyone can decode the result. See the [overview](/peios/using-peios/peiosutils/hashing-and-encoding/overview.md).

## Options

By default these commands *encode*. The options below apply identically to both.

| Option | Effect |
|---|---|
| `-d`, `--decode` | Decode instead of encode. |
| `-i`, `--ignore-garbage` | When decoding, skip any characters that are not part of the alphabet, rather than failing on them. |
| `-w`, `--wrap=COLS` | When encoding, wrap the output to lines of `COLS` characters. The default is 76; `-w 0` disables wrapping and produces one unbroken line. |

When decoding, newlines in the input are always tolerated; `-i` is for *other* stray characters.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | A file could not be read, or — when decoding — the input was not valid for the alphabet. |

---

# basenc

_Peios / Using Peios / Peiosutils / Hashing and encoding_

> The general encoder — encode and decode data in base64, base32, base16, and several other encodings.

`basenc` is the **general** encoding command. Where [`base32` and `base64`](/peios/using-peios/peiosutils/hashing-and-encoding/base32-and-base64.md) each do one encoding, `basenc` does many — the encoding is chosen by an option.

```
basenc encoding [options] [file]
```

```
$ echo Peios | basenc --base16
5065696F730A
```

Every run of `basenc` must name an encoding. With no file, it reads standard input.

## The encodings

| Option | Encoding |
|---|---|
| `--base64` | Standard base64 — the same as the `base64` command. |
| `--base64url` | Base64 with a file- and URL-safe alphabet. |
| `--base32` | Standard base32 — the same as the `base32` command. |
| `--base32hex` | Base32 with the extended-hex alphabet. |
| `--base16` | Hexadecimal. |
| `--base2msbf` | A bit string, most-significant bit first. |
| `--base2lsbf` | A bit string, least-significant bit first. |
| `--z85` | A compact, ASCII85-style encoding. When encoding, the input length must be a multiple of 4; when decoding, a multiple of 5. |
| `--base58` | Base58 — an alphabet chosen so its characters are not visually confusable. |

## Options

By default `basenc` *encodes*. These options apply to whichever encoding you chose:

| Option | Effect |
|---|---|
| `-d`, `--decode` | Decode instead of encode. |
| `-i`, `--ignore-garbage` | When decoding, skip characters that are not part of the alphabet. |
| `-w`, `--wrap=COLS` | When encoding, wrap output to lines of `COLS` characters. `-w 0` disables wrapping. |

## `basenc` and the dedicated commands

For plain base64 or base32, `base64` and `base32` are the shorter way to ask. Use `basenc` when you need an encoding the dedicated commands do not offer — hex, URL-safe base64, the bit-string forms, z85, or base58.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | No encoding was named, a file could not be read, or the input was not valid for the chosen encoding. |

---

# System and processes

_Peios / Using Peios / Peiosutils / System and processes_

> The commands that report on the system, run other commands in a managed way, signal processes, and work with time, terminals, and storage.

This topic gathers the commands that deal with the **running system** rather than with files: reading the time, asking what hardware and system you are on, launching another command under some constraint, signalling a process, working with the terminal.

## The commands

**Time**

| Command | Purpose |
|---|---|
| [`date`](/peios/using-peios/peiosutils/system-and-processes/date.md) | Print — or set — the system date and time. |
| [`sleep`](/peios/using-peios/peiosutils/system-and-processes/sleep.md) | Pause for a given length of time. |
| [`uptime`](/peios/using-peios/peiosutils/system-and-processes/uptime.md) | Show how long the system has been running. |

**Running a command under a constraint**

| Command | Purpose |
|---|---|
| [`timeout`](/peios/using-peios/peiosutils/system-and-processes/timeout.md) | Run a command, and kill it if it runs too long. |
| [`nohup`](/peios/using-peios/peiosutils/system-and-processes/nohup.md) | Run a command so it survives the terminal closing. |
| [`nice`](/peios/using-peios/peiosutils/system-and-processes/nice.md) | Run a command at an adjusted scheduling priority. |
| [`stdbuf`](/peios/using-peios/peiosutils/system-and-processes/stdbuf.md) | Run a command with altered stream buffering. |
| [`chroot`](/peios/using-peios/peiosutils/system-and-processes/chroot.md) | Run a command with a different directory as its root. |

**Signalling processes**

| Command | Purpose |
|---|---|
| [`kill`](/peios/using-peios/peiosutils/system-and-processes/kill.md) | Send a signal to a process. |

**System identity and capacity**

| Command | Purpose |
|---|---|
| [`uname`](/peios/using-peios/peiosutils/system-and-processes/uname.md) | Print system information — the OS, the kernel, the machine. |
| [`arch`](/peios/using-peios/peiosutils/system-and-processes/arch.md) | Print the machine's hardware architecture. |
| [`hostname`](/peios/using-peios/peiosutils/system-and-processes/hostname.md) | Display — or set — the system's host name. |
| [`hostid`](/peios/using-peios/peiosutils/system-and-processes/hostid.md) | Print the host's numeric identifier. |
| [`nproc`](/peios/using-peios/peiosutils/system-and-processes/nproc.md) | Print how many processor cores are available. |

**Who you are**

| Command | Purpose |
|---|---|
| [`whoami`](/peios/using-peios/peiosutils/system-and-processes/whoami.md) | Print the name of the user a process is running as. |
| [`id`](/peios/using-peios/peiosutils/system-and-processes/id.md) | Print the user and group identity of a process, or of a named user. |
| [`groups`](/peios/using-peios/peiosutils/system-and-processes/groups.md) | Print the groups a user belongs to. |
| [`logname`](/peios/using-peios/peiosutils/system-and-processes/logname.md) | Print the login name of the user a process is running as. |

These report the **projection** of a token onto POSIX user and group numbers, which is what Linux programs see. The numbers grant nothing on their own — [`id`](/peios/using-peios/peiosutils/system-and-processes/id.md) explains what they can and cannot say, and [`token`](/peios/security-fundamentals/tokens/token-command.md) shows the identity underneath them.

**Storage and terminal**

| Command | Purpose |
|---|---|
| [`sync`](/peios/using-peios/peiosutils/system-and-processes/sync.md) | Flush cached writes out to persistent storage. |
| [`tty`](/peios/using-peios/peiosutils/system-and-processes/tty.md) | Print the name of the terminal on standard input. |
| [`stty`](/peios/using-peios/peiosutils/system-and-processes/stty.md) | Display or change terminal settings. |

## A note on changing the system

Most commands here only *report*. A few can *change* the running system — `date` can set the clock, `hostname` can set the host name. Changing system-wide state is a privileged operation: it succeeds only for a caller whose token holds the right to do it, and is refused otherwise. The pages for those commands say so where it applies, and [Privileges](/peios/security-fundamentals/privileges/overview.md) is the full picture.

## Where to start

[`uname`](/peios/using-peios/peiosutils/system-and-processes/uname.md) answers "what am I running on?". [`kill`](/peios/using-peios/peiosutils/system-and-processes/kill.md) is the one to read carefully — sending the wrong signal to the wrong process is a quick way to lose work.

---

# date

_Peios / Using Peios / Peiosutils / System and processes_

> Print or set the system date and time, with a flexible output format.

`date` prints the current date and time. With the right option, it can also *set* the system clock, display some other moment, or format a date however you need.

```
date [options] [+format]
```

```
$ date
Sun 17 May 2026 14:32:08 BST
$ date +%Y-%m-%d
2026-05-17
```

## Formatting the output

A `+format` argument controls the output. The format string is printed literally, except for `%` sequences, each of which is replaced by a piece of the date. The common ones:

| Sequence | Is replaced by |
|---|---|
| `%Y` `%m` `%d` | Year, month, day. |
| `%H` `%M` `%S` | Hour, minute, second. |
| `%F` | The full date — same as `%Y-%m-%d`. |
| `%T` | The time — same as `%H:%M:%S`. |
| `%A` `%a` | Weekday name, full and abbreviated. |
| `%B` `%b` | Month name, full and abbreviated. |
| `%j` | Day of the year. |
| `%s` | Seconds since 1970-01-01 UTC. |
| `%z` `%Z` | Numeric and named time zone. |
| `%%` | A literal `%`. |

The full set is long; `date --help` lists every sequence with an example.

## Displaying a different moment

| Option | Effect |
|---|---|
| `-d`, `--date=STRING` | Show the time described by `STRING` — `"yesterday"`, `"next Friday"`, `"@1615432800"` — instead of now. |
| `-r`, `--reference=FILE` | Show `FILE`'s last modification time. |
| `-f`, `--file=DATEFILE` | Like `-d`, but once for each line of `DATEFILE`. |
| `-u`, `--universal` | Work in Coordinated Universal Time (UTC) rather than the local zone. |

## Standard formats

| Option | Output |
|---|---|
| `-I`, `--iso-8601[=FMT]` | ISO 8601 — `FMT` is `date`, `hours`, `minutes`, `seconds`, or `ns`. |
| `-R`, `--rfc-email` | The format used in email headers. |
| `--rfc-3339=FMT` | RFC 3339 — `FMT` is `date`, `seconds`, or `ns`. |

## Setting the clock

| Option | Effect |
|---|---|
| `-s`, `--set=STRING` | Set the system clock to the time described by `STRING`. |

Setting the clock changes state for the whole system, so it is a **privileged** operation — it succeeds only for a caller whose token carries the right to set the time, and is refused otherwise. See [Privileges](/peios/security-fundamentals/privileges/overview.md).

## Exit status

| Code | Meaning |
|---|---|
| `0` | The date was printed or set. |
| `1` | An invalid date or format, or the clock could not be set. |

---

# sleep

_Peios / Using Peios / Peiosutils / System and processes_

> Pause for a given length of time.

`sleep` does nothing for a given length of time, then exits.

```
sleep number[suffix]...
```

```
$ sleep 5          # pause for five seconds
$ sleep 1.5h       # pause for an hour and a half
```

It is used to space things out — a pause between the steps of a script, a delay before a retry.

## Specifying the duration

`number` may be a whole number or a fraction. A `suffix` gives the unit:

| Suffix | Unit |
|---|---|
| `s` | Seconds. This is the default if no suffix is given. |
| `m` | Minutes. |
| `h` | Hours. |
| `d` | Days. |

Given more than one argument, `sleep` waits for the **sum** of them — `sleep 1m 30s` pauses for ninety seconds.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The full time elapsed. |
| `1` | A bad argument, or `sleep` was interrupted before the time was up. |

---

# timeout

_Peios / Using Peios / Peiosutils / System and processes_

> Run a command and kill it if it is still running after a time limit.

`timeout` runs a command with a time limit. If the command finishes in time, nothing special happens. If it is still running when the limit is reached, `timeout` kills it.

```
timeout [options] duration command...
```

```
$ timeout 30s slow-fetch https://example.com
```

It is the guard against a command that might hang — a network fetch, a script that could loop forever.

## The duration

`duration` is a number with an optional unit suffix: `s` for seconds (the default), `m` for minutes, `h` for hours, `d` for days. A duration of `0` disables the limit.

## How the command is stopped

When the limit is reached, `timeout` sends the command a `TERM` signal — a request to shut down cleanly. A well-behaved command stops there. A command that ignores `TERM` keeps running, which is what `-k` is for.

| Option | Effect |
|---|---|
| `-s`, `--signal=SIGNAL` | Send `SIGNAL` instead of `TERM` when the limit is reached. |
| `-k`, `--kill-after=DURATION` | If the command is *still* running `DURATION` after the first signal, send it a `KILL` signal — which cannot be ignored. |
| `--preserve-status` | Exit with the command's own status even when it timed out. |
| `--foreground` | Allow the command to keep reading from the terminal; do not time out the command's own children. |
| `-v`, `--verbose` | Report to standard error whenever a signal is sent. |

## Exit status

`timeout` passes through the command's exit status when the command finishes on its own. Otherwise:

| Code | Meaning |
|---|---|
| (command's own) | The command finished within the limit. |
| `124` | The command timed out. |
| `125` | `timeout` itself failed. |
| `126` | The command was found but could not be run. |
| `127` | The command was not found. |
| `137` | The command was ended by a `KILL` signal. |

---

# nohup

_Peios / Using Peios / Peiosutils / System and processes_

> Run a command so it keeps running after the terminal closes — and how the nohup.out it creates is secured.

`nohup` runs a command so that it **survives the terminal closing**. Normally, when a terminal session ends, the system sends a hangup signal to the commands running under it, and they stop. A command started with `nohup` ignores that signal and keeps running.

```
nohup [options] command [arg]...
```

```
$ nohup long-build &
```

It is how you start something that needs to outlast your session — a long build, a background job — without it being killed the moment you disconnect.

## Where the output goes

A command run with `nohup` is meant to outlive the terminal, so `nohup` cannot leave its streams attached to one. It redirects any stream that is still connected to a terminal:

- **Standard input**, if it is a terminal, is replaced with an empty source — the command reads nothing.
- **Standard output**, if it is a terminal, is appended to a file named `nohup.out` in the current directory. If that file cannot be opened there, `nohup` falls back to `nohup.out` in your home directory.
- **Standard error**, if it is a terminal, is sent to wherever standard output now goes.

Streams that you have already redirected yourself — to a file, or a pipe — are left as they are.

## How nohup.out is secured

When `nohup` has to **create** `nohup.out`, that file is a new file and needs a [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md). `nohup` does not let it inherit one from the directory. It creates `nohup.out` with a deliberate, locked descriptor: **full access for the owner, and no one else**.

The reasoning is that `nohup.out` captures whatever the command writes, which the person running it has not chosen to share. A file that appears as a side effect should not be more open than its creator intended — so `nohup` gives it the safe default of owner-only, rather than whatever the surrounding directory would have handed down.

| Option | Effect |
|---|---|
| `--sddl=SDDL` | Create `nohup.out` with the security descriptor given as an SDDL string, instead of the owner-only default. |

`--sddl` only matters when `nohup` actually creates the file. If `nohup.out` already exists, `nohup` appends to it and leaves its existing security alone.

## Exit status

When `nohup` runs a command, it exits with **that command's** status once it finishes. The exception is a failure in `nohup` itself:

| Code | Meaning |
|---|---|
| (command's own) | The command ran; this is its exit status. |
| `125` | `nohup` could not set things up — it could not detach from the terminal, or could not open `nohup.out` anywhere. The command was not run. |

---

# nice

_Peios / Using Peios / Peiosutils / System and processes_

> Run a command at an adjusted scheduling priority.

`nice` runs a command at an adjusted **scheduling priority** — making it more, or less, willing to give the processor up to other work.

```
nice [options] [command [arg]...]
```

```
$ nice -n 15 big-batch-job
```

With no command, `nice` prints the current niceness.

## Niceness

A process has a *niceness* — a number from **-20** to **19**:

- A **high** niceness (toward 19) means the process is "nicer" to others — it yields the processor readily. Good for background work that should not get in the way.
- A **low** niceness (toward -20) means the process is favoured — it gets the processor more often.

By default `nice` adds **10** to the niceness, making the command run in the background more gracefully.

| Option | Effect |
|---|---|
| `-n`, `--adjustment=N` | Add `N` to the niceness instead of 10. `N` may be negative. |

Raising a command's niceness — running it *lower* priority — is always allowed. Lowering the niceness, to claim a *higher* priority than normal, is a privileged request and may be refused; when it is, `nice` warns and runs the command at the priority it was permitted.

## Exit status

When `nice` runs a command, it exits with **that command's** status. The exception is a failure in `nice` itself:

| Code | Meaning |
|---|---|
| (command's own) | The command ran; this is its exit status. |
| `0` | No command was given; the niceness was printed. |
| `125` | `nice` itself failed — for example, a bad adjustment value. |
| `126` | The command was found but could not be run. |
| `127` | The command was not found. |

---

# kill

_Peios / Using Peios / Peiosutils / System and processes_

> Send a signal to a process.

`kill` sends a **signal** to a process. Despite the name, a signal is not always fatal — it is a message, and some signals ask a process to do something other than stop.

```
kill [options] pid...
```

```
$ kill 4821                 # ask process 4821 to terminate
$ kill -KILL 4821           # force it to stop
```

A `pid` is a process identifier — the number a running process is known by.

## Signals

With no option, `kill` sends `TERM` — a polite request to shut down, which a well-behaved process honours by cleaning up and exiting. The signals you will use most:

| Signal | Meaning |
|---|---|
| `TERM` | Terminate. The default — a request to stop cleanly. |
| `KILL` | Stop immediately. Cannot be caught or ignored, so it cannot be refused — but the process gets no chance to clean up. The last resort. |
| `HUP` | Hang up. Conventionally tells a long-running service to reload its configuration. |
| `INT` | Interrupt — the signal a terminal sends on Ctrl-C. |
| `STOP` / `CONT` | Suspend the process / resume a suspended one. |

| Option | Effect |
|---|---|
| `-s`, `--signal=SIGNAL` | Send `SIGNAL` instead of `TERM`. |
| `-SIGNAL` | A shorthand — `-KILL` or `-9` both send `KILL`. |
| `-l`, `--list` | List the signal names. |
| `-L`, `--table` | List the signals as a table of numbers and names. |

Use `KILL` only when `TERM` has failed. A process killed outright cannot save its work or release what it holds.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Every signal was sent. |
| `1` | A signal could not be sent — a process that does not exist, or one you are not permitted to signal. |

---

# uname

_Peios / Using Peios / Peiosutils / System and processes_

> Print system information — the operating system, the kernel, and the machine.

`uname` prints information about the system you are running on.

```
uname [options]
```

```
$ uname
Peios
$ uname -a
Peios host-01 1.4.0 #1 SMP 2026-05-10 x86_64 Peios
```

With no option, `uname` prints the operating-system name — `Peios`.

## What each option prints

| Option | Prints |
|---|---|
| `-s` | The kernel name. |
| `-o` | The operating-system name — `Peios`. |
| `-n` | The node name — the name this system is known by on the network. |
| `-r` | The kernel release. |
| `-v` | The kernel version. |
| `-m` | The machine's hardware name (its architecture). |
| `-p` | The processor type. |
| `-i` | The hardware platform. |
| `-a` | Everything — equivalent to `-mnrsvo`. |

Give several options and `uname` prints those fields, in a fixed order, on one line.

## The operating system is Peios

`uname -o`, and the operating-system field of `uname -a`, report `Peios`. This is the field a script should check when it wants to confirm what system it is on.

For the machine architecture alone, [`arch`](/peios/using-peios/peiosutils/system-and-processes/arch.md) is the shorter command — it prints exactly what `uname -m` prints.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The requested information was printed. |
| `1` | The system information could not be read. |

---

# arch

_Peios / Using Peios / Peiosutils / System and processes_

> Print the machine's hardware architecture.

`arch` prints the machine's hardware architecture — the kind of processor the system runs on.

```
arch
```

```
$ arch
x86_64
```

That is the whole command. It takes no options and prints a single name.

`arch` prints exactly what [`uname -m`](/peios/using-peios/peiosutils/system-and-processes/uname.md) prints. It exists as a short, obvious name for the one question "what architecture is this?" — convenient in a script that picks an architecture-specific path.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The architecture was printed. |
| `1` | The architecture could not be determined. |

---

# hostname

_Peios / Using Peios / Peiosutils / System and processes_

> Display or set the system's host name.

`hostname` displays the system's **host name** — the name the system is known by.

```
hostname [options] [name]
```

```
$ hostname
host-01.example.com
```

## What is displayed

By default `hostname` prints the fully qualified name. These options narrow or change what is shown:

| Option | Prints |
|---|---|
| `-f`, `--fqdn` | The fully qualified domain name — the full `host.domain` form. This is the default. |
| `-s`, `--short` | The short host name only — the part before the first dot. |
| `-d`, `--domain` | The DNS domain part only. |
| `-i`, `--ip-address` | The network address (or addresses) the host name resolves to. |

## Setting the host name

Given a `name` argument, `hostname` sets the system's host name to it.

```
$ hostname host-02
```

Changing the host name changes state for the whole system, so it is a **privileged** operation — it succeeds only for a caller whose token carries the right to do it, and is refused otherwise. See [Privileges](/peios/security-fundamentals/privileges/overview.md).

## Exit status

| Code | Meaning |
|---|---|
| `0` | The host name was displayed or set. |
| `1` | A failure — the name could not be read, set, or resolved. |

---

# hostid

_Peios / Using Peios / Peiosutils / System and processes_

> Print the host's numeric identifier.

`hostid` prints the host's numeric identifier, in hexadecimal.

```
hostid
```

```
$ hostid
007f0100
```

The host id is a number associated with the system. It takes no options and prints one value.

Where [`hostname`](/peios/using-peios/peiosutils/system-and-processes/hostname.md) gives the system's *name* — meant to be read by people — `hostid` gives a numeric value, used by software that wants a machine identifier in a fixed numeric form.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The host id was printed. |

---

# nproc

_Peios / Using Peios / Peiosutils / System and processes_

> Print how many processor cores are available.

`nproc` prints the number of processor cores available.

```
nproc [options]
```

```
$ nproc
8
```

By default it prints the number of cores available **to the current process** — which can be fewer than the machine has, if the process has been restricted to a subset. It is most often used to decide how many parallel jobs to run.

## Options

| Option | Effect |
|---|---|
| `--all` | Print the number of cores the *whole system* has, ignoring any restriction on the current process. |
| `--ignore=N` | Subtract `N` from the count — for leaving some cores free rather than using every one. |

The environment variables `OMP_NUM_THREADS` and `OMP_THREAD_LIMIT`, if set, also bound the number `nproc` reports.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The count was printed. |
| `1` | An option value was invalid. |

---

# uptime

_Peios / Using Peios / Peiosutils / System and processes_

> Show how long the system has been running.

`uptime` shows how long the system has been running, along with a few related figures.

```
uptime [options]
```

```
$ uptime
 14:32:08  up 6 days,  3:21,  load average: 0.18, 0.24, 0.21
```

The default line packs in three things:

- the **current time**;
- how long the system has been **up**;
- the **load average** — a rough measure of how busy the system has been over the last 1, 5, and 15 minutes.

If you know `uptime` from other systems you may be expecting a count of logged-in users between the uptime and the load average. Peios does not print one. That count comes from a login database (`utmp`) that Peios does not keep — logon sessions are a [KACS](/peios/security-fundamentals/access-decisions/overview.md) concept, and the tooling to report them is not built yet. Rather than print a figure that would always read `0 users`, the field is left out until it can be answered honestly.

## Options

| Option | Effect |
|---|---|
| `-p`, `--pretty` | Show just the uptime, in a readable phrase — `up 6 days, 3 hours, 21 minutes`. |
| `-s`, `--since` | Show the date and time the system started, rather than how long ago that was. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The uptime was printed. |
| `1` | The boot time could not be read. |

---

# sync

_Peios / Using Peios / Peiosutils / System and processes_

> Flush cached writes out to persistent storage.

`sync` flushes cached writes to persistent storage.

```
sync [options] [file...]
```

```
$ sync
```

When a program writes to a file, the data does not always reach the physical device immediately — for speed, the system holds recent writes in memory and writes them out a little later. `sync` forces that pending data out *now*, so that what is on the device matches what programs have written.

It is the command to run before doing something that should not lose recent writes — before powering off by hand, or before removing storage.

## Options

With no argument, `sync` flushes everything. The options narrow it:

| Option | Effect |
|---|---|
| `file...` | Flush only the given files, rather than everything. |
| `-d`, `--data` | For the given files, flush only the file data, not metadata that does not need it. |
| `-f`, `--file-system` | Flush the entire file system that contains each given file. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The flush completed. |
| `1` | A named file could not be reached. |

---

# tty

_Peios / Using Peios / Peiosutils / System and processes_

> Print the name of the terminal connected to standard input.

`tty` prints the name of the terminal connected to standard input.

```
tty [options]
```

```
$ tty
/dev/pts/3
```

If standard input is not a terminal — because it has been redirected from a file or a pipe — `tty` prints `not a tty` instead.

That second behaviour is the useful one: a script can run `tty` to find out whether it is being run interactively or as part of a pipeline, and behave accordingly.

## Options

| Option | Effect |
|---|---|
| `-s`, `--silent` | Print nothing; report the answer only through the exit status. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Standard input is a terminal. |
| `1` | Standard input is not a terminal. |
| `2` | A usage error. |

---

# stty

_Peios / Using Peios / Peiosutils / System and processes_

> Display or change terminal settings.

`stty` displays and changes the settings of a terminal — how it handles input, output, and special keys.

```
stty [options] [setting...]
```

```
$ stty
speed 38400 baud; line = 0;
-brkint -imaxbel iutf8
```

Run with no arguments, `stty` prints the settings that differ from the usual defaults. Given `setting` arguments, it changes them.

## Viewing the settings

| Option | Effect |
|---|---|
| `-a`, `--all` | Print *every* setting, in a readable layout. |
| `-g`, `--save` | Print every setting in a single compact line — a form that can be fed straight back to `stty` to restore them. |

The `-g` form is how you save and restore a terminal's state:

```
$ saved=$(stty -g)      # capture the current state
$ stty -echo            # ...change something...
$ stty "$saved"         # restore exactly what was saved
```

## Changing the settings

A `setting` argument turns something on or off, or assigns a value. A few common ones:

| Setting | Effect |
|---|---|
| `echo` / `-echo` | Show / hide typed characters — `-echo` is how a password prompt hides input. |
| `rows N` / `cols N` | Tell the terminal its size. |
| A number | Set the connection speed (baud rate). |

`stty` has a large catalog of settings; `stty -a` shows them all with their current values.

## Choosing the terminal

| Option | Effect |
|---|---|
| `-F`, `--file=DEVICE` | Operate on `DEVICE` instead of the terminal on standard input. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The settings were printed or changed. |
| `1` | A setting was invalid, or the terminal could not be accessed. |

---

# stdbuf

_Peios / Using Peios / Peiosutils / System and processes_

> Run a command with altered buffering on its standard streams.

`stdbuf` runs a command with **altered buffering** on its standard streams.

```
stdbuf [options] command...
```

```
$ slow-producer | stdbuf -oL grep error
```

## What buffering is, and why change it

A program usually does not write each piece of output the instant it is produced — it collects output in a buffer and writes it in batches, which is faster. The cost is *delay*: when a program's output feeds a pipe, that batching can hold lines back for a long time, which is unhelpful when you are watching a pipeline live.

`stdbuf` lets you override that batching for a command, so its output appears sooner.

## Setting the buffering

| Option | Stream |
|---|---|
| `-i`, `--input=MODE` | Standard input. |
| `-o`, `--output=MODE` | Standard output. |
| `-e`, `--error=MODE` | Standard error. |

`MODE` is one of:

| MODE | Buffering |
|---|---|
| `0` | Unbuffered — every write goes out immediately. |
| `L` | Line-buffered — output is flushed at the end of each line. Not valid for input. |
| a size | Fully buffered with a buffer of that many bytes. Accepts suffixes — `K`, `M`, and so on. |

`-oL` — line-buffered output — is the common case: it makes a command in a pipeline emit each line as it is finished.

## A limitation

`stdbuf` adjusts the *default* buffering. A command that manages its own stream buffering will override what `stdbuf` sets, and some commands do not use buffered streams at all — for those, `stdbuf` has no effect.

## Exit status

When `stdbuf` runs a command, it exits with **that command's** status. The exception is a failure in `stdbuf` itself:

| Code | Meaning |
|---|---|
| (command's own) | The command ran; this is its exit status. |
| (a `stdbuf`-level error code) | The command could not be started. |

---

# chroot

_Peios / Using Peios / Peiosutils / System and processes_

> Run a command with a different directory as its root.

`chroot` runs a command with a **different directory as its root** — its `/`. Inside that command, the chosen directory *is* the top of the file system, and nothing above it can be named or reached.

```
chroot newroot [command [arg]...]
```

```
$ chroot /mnt/system /bin/sh
```

With a `command`, `chroot` changes the root to `newroot` and runs the command there. With no command, it starts an interactive shell.

## What it does

A process normally sees the whole file system, rooted at the real `/`. After `chroot`, the named directory becomes that process's `/`: a path like `/etc/config` inside the command resolves to `newroot/etc/config`, and there is no path that reaches outside `newroot` at all.

It is used to work inside another installed system — repairing or configuring a system image by entering it — and to run a command confined to a known subtree.

For the command to be usable, `newroot` must already contain what the command needs: the command's own executable, and whatever libraries and files it depends on, all present *under* `newroot`.

## Options

| Option | Effect |
|---|---|
| `--skip-chdir` | Do not change the working directory to `/` after changing the root. Permitted only when `newroot` is the current `/`. |

## A privileged operation

Changing a process's root directory is a **privileged** operation. `chroot` succeeds only for a caller whose token carries the right to do it, and is refused otherwise — the ability to redraw a process's view of the file system is not something every principal holds. See [Privileges](/peios/security-fundamentals/privileges/overview.md).

## Exit status

`chroot` exits with the command's own status once it finishes. Otherwise:

| Code | Meaning |
|---|---|
| `125` | `chroot` itself failed — for example, `newroot` is not a directory. |
| `126` | The command was found but could not be run. |
| `127` | The command was not found. |

---

# whoami

_Peios / Using Peios / Peiosutils / System and processes_

> Print the name of the user a process is running as.

`whoami` prints the name of the user the calling process is running as.

```
whoami
```

```
$ whoami
jack
```

It takes no arguments. It is the same answer as `id -un`, which is all it has ever been.

## Where the name comes from

The name is resolved from the process's effective user ID, which is a projection of the [token's](/peios/security-fundamentals/tokens/overview.md) user SID. The lookup goes to the authority — there is no `/etc/passwd` to read — and comes back as the principal's canonical name. [Resolving names](/peios/security-fundamentals/managing-local-principals/resolving-names.md) is the full picture.

The name is the principal's; the *number* it was looked up by is not what grants anything. If you want the identity access is actually decided against, `id -Z` prints the SID.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The name was printed. |
| `1` | The user ID could not be resolved to a name. |

A failure here usually means the authority could not be reached rather than that the account is missing — identity does not resolve before the authority is running, which is the correct answer rather than a gap.

---

# id

_Peios / Using Peios / Peiosutils / System and processes_

> Print the user and group identity of a process, or of a named user.

`id` prints who a process is: its user, its primary group, the groups it belongs to, and its security context.

```
id [options] [USER]...
```

```
$ id
uid=5001000(jack) gid=5000513(Users) groups=5000513(Users),100(Everyone),101(Authenticated Users),1544(Administrators) context=S-1-5-21-1004336348-1177238915-682003330-1000:High
```

With no argument it reports the calling process. Given one or more users, it reports on those instead.

## The numbers are projections

This is the thing to understand before reading `id`'s output on Peios.

The `uid=` and `gid=` numbers are **projections** of the token's [SIDs](/peios/security-fundamentals/identity/sids.md). They are real, stable, and exactly what Linux programs see and what filesystems store — but **nothing is decided by them**. Access is decided against the SID the number was projected from, by the [access check](/peios/security-fundamentals/access-decisions/overview.md). A number here grants nothing.

That distinction is not academic, because the projection cannot carry everything a token holds:

- Groups whose SIDs are deliberately unnumbered — `Interactive`, `Network`, `Batch`, `Service` — **cannot appear at all**. They are what make a rule like "network logons cap at Low" expressible, and they have no gid by design.
- A group's **attributes** have nowhere to go. A deny-only group, which can block access through a deny entry but grant nothing through an allow entry, appears in `groups=` looking exactly like an ordinary membership.
- The **integrity level** and the **logon session** have no number either.

So two tokens can project to identical numbers and mean different things. The clearest case is a [filtered token](/peios/security-fundamentals/tokens/restricted-tokens.md): it carries the same user SID and the same group numbers as the token it came from, and differs only in attributes the projection cannot express.

That is why the default line ends with a **`context=`** field.

## The context field

`context=` is the authoritative half: the user SID access is actually decided against, and the integrity level.

```
context=S-1-5-21-1004336348-1177238915-682003330-1000:High
```

The integrity level is part of it precisely because the SID alone does not separate an elevated token from its filtered counterpart — both carry the same user SID, and the level is what differs.

If you know `id` from another Unix, this is the field that carries the SELinux context there. Peios has no SELinux; its security context is the [token](/peios/security-fundamentals/tokens/overview.md), so that is what the field holds.

`context=` is omitted when a user is named rather than the calling process — a named user has no token here to read — and when `POSIXLY_CORRECT` is set, so a caller that asked for the POSIX shape still gets it.

For the whole token rather than this summary, use [`token`](/peios/security-fundamentals/tokens/token-command.md).

## Options

| Option | Effect |
|---|---|
| `-u`, `--user` | Print only the effective user ID. |
| `-g`, `--group` | Print only the effective group ID. |
| `-G`, `--groups` | Print only the group IDs, space-separated. |
| `-n`, `--name` | With `-u`, `-g` or `-G`, print names instead of numbers. |
| `-r`, `--real` | With `-u`, `-g` or `-G`, print the real ID rather than the effective one. |
| `-z`, `--zero` | Separate entries with NUL rather than whitespace. Not allowed in the default format. |
| `-Z`, `--context` | Print only the security context. |
| `-p` | A readable, multi-line form. |
| `-P` | Print as a password-file entry. |
| `-a` | Ignored; accepted for compatibility. |
| `-A` | Not applicable on Peios — see below. |

`-n` and `-r` need one of `-u`, `-g` or `-G`; they do not change the default format.

### `-r` means something different here

On other Unixes the real/effective split is the setuid boundary. On Peios a setuid bit does not change a token, so that boundary does not exist. What the split tracks instead is [impersonation](/peios/security-fundamentals/impersonation/overview.md): the real IDs come from the process's own token, the effective ones from whatever token is in force.

`id` never impersonates, so in practice it reports the same either way and the `euid=`/`egid=` fields never appear. That is not a fault — there is genuinely nothing differing to report.

### `-A` is not applicable

`-A` reports BSD audit session properties. Peios audits through [its own event system](/peios/security-fundamentals/auditing/overview.md) and keeps no BSM session, so `-A` says so rather than silently succeeding at nothing.

## `id -G` and `id -G <yourself>` differ

Asking about yourself and asking about your *name* are two different questions here, and they give different answers:

- **`id -G`** reads your process's credential — the projection of your **token's** group set, which includes the groups the authority added when the token was minted: `Everyone`, `Authenticated Users`, and any `Administrators`-style aliases.
- **`id -G jack`** looks the name up through the [principal store](/peios/security-fundamentals/managing-local-principals/resolving-names.md), which returns **recorded** memberships only. The stapled groups are not recorded anywhere, because membership in them is a rule rather than a stored fact — so they do not come back.

On other systems these two differ only in the order they print. Here the second list is genuinely shorter, and neither is wrong.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The identity was printed. |
| `1` | A user could not be found, an option combination was rejected, or the context could not be read. |

---

# groups

_Peios / Using Peios / Peiosutils / System and processes_

> Print the groups a user belongs to.

`groups` prints the groups a user belongs to.

```
groups [USERNAME]...
```

```
$ groups
Users Everyone Authenticated Users Administrators

$ groups jack
jack : Users Administrators
```

With no argument it reports the calling process. Given names, it reports on those.

## Those two answers are both correct

The example above is not a mistake, and it is the thing worth knowing about this command on Peios.

**`groups`** reads the calling process's credential — the projection of its [token's](/peios/security-fundamentals/tokens/overview.md) group set. That set includes the groups the authority *stapled on* when the token was minted: `Everyone`, `Authenticated Users`, and the like.

**`groups jack`** looks the name up through the [principal store](/peios/security-fundamentals/managing-local-principals/resolving-names.md), which returns **recorded** memberships — the ones something actually wrote down.

The stapled groups are not recorded anywhere, because membership in them is a **rule**, not a stored fact. Everyone is in `Everyone`; nothing needs to record it, and nothing can enumerate it. So they appear in the first answer and not the second.

On other Unixes these two forms differ only in the order they print.

## What cannot appear

Both forms are lossy in the same way, and unavoidably so — a group can only be listed if it has a group ID, and some deliberately do not:

- `Interactive`, `Network`, `Batch` and `Service` are **unnumbered on purpose**. They describe *how* you signed in rather than who you are, which is what lets a rule like "network logons cap at Low" be written at all. You are in `Interactive` at the console and not over the network, so there is no static answer to record.
- A group's **attributes** are not representable. A deny-only group — one that can block access through a deny entry but grant nothing through an allow entry — is printed exactly like an ordinary membership.

[`token groups`](/peios/security-fundamentals/tokens/token-command.md) is the lossless view, with every SID and its attributes.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The groups were printed. |
| `1` | A named user could not be found, or the group list could not be read. |

A group ID with no name is printed as the bare number and sets the exit status, rather than stopping the listing.

---

# logname

_Peios / Using Peios / Peiosutils / System and processes_

> Print the login name of the user a process is running as.

`logname` prints the login name the calling process runs under.

```
logname
```

```
$ logname
jack
```

It takes no arguments.

## Login name, not current name

`logname` and [`whoami`](/peios/using-peios/peiosutils/system-and-processes/whoami.md) look like the same command and answer slightly different questions.

`whoami` reports the **effective** identity — whoever the process is acting as right now. `logname` reports the **login** identity: the principal the process's own token is for, regardless of any token it has since taken on. When a process is [impersonating](/peios/security-fundamentals/impersonation/overview.md) a client, `whoami` follows the impersonation and `logname` does not.

Most of the time nothing is impersonating and the two agree.

If you know `logname` from another Unix, note that it answers this from the token rather than from a login-record file. There is no `utmp` on Peios — nothing writes one — so the traditional source does not exist. The token is the record of who a process is, and it is a better one: it cannot drift from the identity access is actually checked against, because it *is* that identity.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The login name was printed. |
| `1` | No login name could be determined. |

---

# token

_Peios / Using Peios / Peiosutils / System and processes_

> The token command inspects and manipulates tokens — reading a token's contents, adjusting it, duplicating and restricting it, and driving impersonation.

`token` is the command-line tool for working with **tokens** directly. It reads a token's contents, adjusts it, produces derived tokens, and drives impersonation — the low-level operations this topic describes, exposed at a shell.

```
token subcommand [target] [arguments]
```

```
$ token                       # one-line summary of your own token
$ token show --all             # every field of your own token
$ token privs --pid 4821       # the privileges on process 4821's token
```

`token` is a direct, debug-level tool. Day to day you do not inspect tokens by hand — the system does. `token` is for diagnosing an access problem, for understanding what identity a process is really running under, and for building and testing identity setups. Run with no subcommand, it prints a one-line summary of your own token.

## Choosing which token

Almost every subcommand operates on a token, and these flags choose which one. With none, the target is your own.

| Flag | Target |
|---|---|
| `--self` | Your own token. The default. |
| `--real` | Your **primary** token specifically, rather than the effective one — relevant when your thread is impersonating. |
| `--pid PID` | The primary token of process `PID`. |
| `--tid TID` | The impersonation token of thread `TID` (used with `--pid`). |
| `--peer SOCK_FD` | The peer's captured token on a connected socket — see [Peer tokens](/peios/security-fundamentals/impersonation/peer-tokens.md). |

Reading another process's token is itself access-controlled: it succeeds only with the right authority over that process.

## Inspecting a token

### `show`

`token show` prints a token's contents. It is the default — bare `token` is `token show --short`.

| Flag | Effect |
|---|---|
| `--short` | A one-line summary. |
| `--all` | Every query class — the fullest dump. |

### Field accessors

Each of these prints one part of a token, for when you want just that piece:

| Subcommand | Prints |
|---|---|
| `user` | The user SID — who the token is. |
| `owner` | The default owner SID. |
| `group` | The primary group SID. |
| `groups` | The group list. |
| `privs` | The privileges, with their enabled state. |
| `caps` | The capabilities. |
| `claims` | The user and device claims. |
| `integrity` | The integrity level. |
| `logon` | The logon type and logon SID. |
| `source` | What minted the token. |
| `origin` | The originating session for a derived token. |
| `stats` | Token statistics — IDs, timestamps, the modification counter. |
| `default-dacl` | The token's default DACL. |

### `query`

`token query CLASS` performs a raw read of a single named token-info class and prints the result as JSON — the lowest-level inspection route, for tooling.

## Changing a token

### `adjust`

`token adjust` mutates a token in place:

| Form | Changes |
|---|---|
| `adjust privs NAME=STATE …` | Enable, disable, or remove privileges. `STATE` is `enabled`, `disabled`, or `removed`. |
| `adjust groups IDX=STATE …` | Enable or disable groups by their list index. |
| `adjust default --dacl SDDL` | Replace the token's default DACL. Also `--owner-idx` / `--group-idx`. |
| `adjust session ID` | Replace the token's session id. |

### `restrict`

`token restrict` produces a [restricted token](/peios/security-fundamentals/tokens/restricted-tokens.md) — a more limited variant of a token.

| Flag | Effect |
|---|---|
| `--drop-privs MASK\|NAMES` | Privileges to drop. |
| `--deny IDX,…` | Group indices to mark deny-only. |
| `--restrict SID,…` | The restricting SIDs to apply. |

### `duplicate`

`token duplicate` (alias `dup`) copies a token, optionally changing its `--type` (primary or impersonation), its impersonation `--level`, or its `--access` mask.

### `link` and `linked`

`token link` joins two tokens as an elevation pair — a full token and its filtered counterpart — given their file descriptors and a session id. `token linked` shows a token's elevation-linked counterpart, if it has one. See [Elevation](/peios/security-fundamentals/tokens/elevation.md).

## Impersonation

| Subcommand | Effect |
|---|---|
| `impersonate` | Begin impersonating the target token on the calling thread. With a trailing `-- command …`, run that command under the impersonating token. |
| `revert` | Drop any active impersonation on the calling thread. |

See [Impersonation](/peios/security-fundamentals/impersonation/overview.md) for the model these drive.

## Creating tokens

| Subcommand | Effect |
|---|---|
| `create SPEC` | Create a token from a binary token-spec (`SPEC` is a file, or `-` for standard input). |
| `install SPEC` | Create a token from a spec and install it as the caller's primary token. |

Creating and installing tokens is a privileged operation, reserved for the components that legitimately mint identity.

## Output options

| Flag | Effect |
|---|---|
| `--raw` | Render SIDs in raw `S-1-…` form only. |
| `--label` | Render SIDs as their labels where known, falling back to raw. |
| `--json` | Emit JSON instead of human-readable output. |

## `token` and the inspection surfaces

`token` is the convenient front-end. Underneath, it reads the same kernel surfaces and rules described in [Inspecting tokens](/peios/security-fundamentals/inspecting/tokens.md) — that page covers the query mechanism, the access rules for reading another process's token, and what cannot be inspected.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded. |
| `1` | A usage error. |
| non-zero | The operation failed — no such target, an access denial, or a bad spec. |

---

# logonse

_Peios / Using Peios / Peiosutils / System and processes_

> The logonse command lists logon sessions and their processes, creates and destroys them, and sets a process's mitigation flags.

`logonse` is the command-line tool for **logon sessions** — the kernel's records of authentication events that this topic describes. It lists the active sessions, shows which processes belong to one, creates and destroys sessions, and (as a related low-level job) sets a process's mitigation flags.

```
logonse subcommand [arguments]
```

```
$ logonse list
$ logonse show 4711
```

`logonse` is a low-level administrative and debugging tool. It requires a subcommand: `list`, `show`, `create`, `destroy`, or `psb`.

## Listing sessions

### `logonse list`

Enumerates the active logon sessions, and the process IDs in each.

```
$ logonse list
session 0     pids: [1, 2, 14, 22]
session 4711  pids: [820, 844, 901]
```

### `logonse show`

Shows the process IDs that belong to one session.

```
$ logonse show 4711
```

### A caveat on list and show

There is no syscall that enumerates logon sessions, and `logonse` does not read the kernel's sessions file (whose SD restricts it to Administrators and SYSTEM). `logonse list` and `logonse show` work by walking the running processes and reading each one's token to find which session it belongs to. That has two consequences worth knowing:

- It is **best-effort**. A session that has no running process — held alive only by a token file descriptor somewhere — will not appear, because there is no process to find it through.
- It is a **snapshot under change**. Processes start and exit while the walk runs, so the result is a close approximation of the moment, not a locked one.

For routine "who is signed in" use this is fine. For an authoritative listing, the kernel's own sessions surface — described in [Inspecting sessions](/peios/security-fundamentals/inspecting/sessions.md) — is the source of record.

## Creating and destroying sessions

### `logonse create`

Creates a new logon session for a user, described by a logon type, an authentication-package name, and the user's SID.

```
$ logonse create --logon-type interactive --auth-package Negotiate --user-sid S-1-5-21-...-1001
```

| Flag | Meaning |
|---|---|
| `--logon-type TYPE` | The kind of logon: `interactive`, `network`, `batch`, `service`, `network-cleartext`, or `new-credentials`. |
| `--auth-package STR` | The name of the authentication package that vouched for the logon. |
| `--user-sid SID` | The user the session belongs to, as an `S-1-…` SID or an SDDL alias such as `BA`. |

On success `logonse` prints the new session's id. Creating a session is a **privileged** operation — minting authentication records is reserved for the components that legitimately do so.

> [!NOTE]
> The tool builds the kernel's binary session spec from these fields for you. The underlying [wire format](/peios/using-peios/wire-formats-reference/token-and-session-specs.md) is unchanged; only the command-line surface is typed.

### `logonse destroy`

Destroys a session — but only an **empty** one, with no tokens still referencing it.

```
$ logonse destroy 4711
```

A session with live tokens cannot be destroyed this way; its tokens must go first. See [Session lifecycle](/peios/security-fundamentals/logon-sessions/lifecycle.md).

## Setting process mitigation flags

### `logonse psb`

`logonse psb` sets the **mitigation flags** in a process's Process Security Block.

```
$ logonse psb --pid 4821 --mitigations 0x1c0
```

| Flag | Meaning |
|---|---|
| `--pid PID` | The process to act on. |
| `--mitigations MASK` | The mitigation bitmask to apply, in hexadecimal or decimal. |

This subcommand is about process hardening rather than logon sessions — it lives in `logonse` because both deal with low-level per-process kernel state. For what the mitigation flags mean and how they behave, see [Process mitigations](/peios/security-fundamentals/process-mitigations/overview.md).

## Output options

| Flag | Effect |
|---|---|
| `--json` | Emit JSON instead of human-readable output. Accepted by every subcommand. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded. |
| `1` | A usage error, or the operation failed. |

---

# mount

_Peios / Using Peios / Peiosutils / Disks and mounts_

> Attach a filesystem to the Peios mount tree — new mounts, binds, moves, remounts and propagation changes — and apply a KACS mount policy at mount time.

`mount` attaches a filesystem to the Peios mount tree. With no operands it instead *lists* what is currently mounted. It is a faithful reworking of the util-linux `mount(8)` surface for Peios, with two structural differences: Peios has **no `/etc/fstab` and no `/etc/mtab`**, so every mount is described entirely on the command line and live mount state is read from the kernel; and a `mount` can apply a **KACS mount policy** to the new filesystem at attach time (see [Mount policies](/peios/using-peios/mount-policies/overview.md)).

```
mount [-t TYPE] [-o OPTIONS] SOURCE TARGET
mount [-o remount,OPTIONS] TARGET
mount --bind|--rbind|--move SOURCE TARGET
mount --make-shared|--make-private|... TARGET
mount [-l] [-t TYPE]
```

Internally `mount` uses the fd-based mount API (`fsopen` / `fsconfig` / `fsmount` / `move_mount`, plus `open_tree` and `mount_setattr`), never the classic single-shot `mount(2)`. This matters in one place you can see: when a mount fails, the kernel's `fs_context` message log is drained and printed as the reason, even without `-v`.

## Operands and argument shapes

Because there is no fstab to consult, operand resolution is strict:

- **No operands, no verb** — list mode (see [below](#list-mode)).
- **Two operands** — `SOURCE TARGET`.
- **One operand** — an error. In util-linux a lone operand is resolved through fstab; Peios has none, so a single operand cannot be turned into a `SOURCE TARGET` pair. Supply both, or use `--source` / `--target`.
- **A lone target** is valid only for `-o remount` and for a standalone propagation change (`--make-*`), which act on an existing mount point.

`--source SRC` and `--target DIR` name the operands explicitly and may be combined with a single positional. `--target-prefix DIR` prepends `DIR/` to the target after it is chosen. Options may be interspersed with operands (`mount SRC -o ro TGT`), matching util-linux.

Paths, `-o key=value` values, labels and UUIDs are handled as **opaque byte strings**, never assumed to be UTF-8; an embedded NUL is a usage error.

### Canonicalisation

By default source and target paths are canonicalised (made absolute, symlinks resolved). `-c` / `--no-canonicalize` disables that; `X-mount.nocanonicalize[=source|target]` is the `-o` form and can disable it for just one side. The final path handed to the kernel is protected against a TOCTOU symlink swap on its last component.

## Operation modes (verbs)

`mount` performs one of several operations. The **structural verbs** — bind, rbind, move, beneath and remount — are mutually exclusive with one another. A **propagation** change is *not* a structural verb: it may stand alone on a target, or trail another verb or a new mount in the same command (applied afterwards as one or more `mount_setattr` steps), and several may be combined.

| Verb | How to request it | What it does |
|---|---|---|
| **New mount** | `mount [-t T] SRC TGT` | Attach a fresh instance of the filesystem at `SRC` onto `TGT`. |
| **Bind** | `-B` / `--bind`, or `-o bind` | Make an existing subtree visible at a second location. |
| **Recursive bind** | `-R` / `--rbind`, or `-o rbind` | Bind a subtree together with every mount underneath it. |
| **Move** | `-M` / `--move`, or `-o move` | Relocate an existing mount to a new mount point. |
| **Move beneath** | `--beneath SRC TGT` | Attach `SRC` *beneath* the mount currently at `TGT` (the kernel enforces several constraints; violations surface as exit 32). |
| **Remount** | `-o remount[,...] TGT` | Change the options of an existing mount (see [Remounting](#remounting)). |
| **Propagation** | `--make-*` / `--make-r*`, or the `-o` tokens below | Change how mount/unmount events propagate across a subtree. |
| **List** | `mount` / `mount -l` | Print the current mounts. |

### Propagation flags

Each of these sets the propagation type of the mount at the target. The `--make-r*` (and `r`-prefixed `-o`) forms apply recursively to the whole subtree; the recursive form is all-or-nothing (it either applies to the entire subtree or to none of it).

| Flag | `-o` token | Recursive flag | Recursive `-o` token | Meaning |
|---|---|---|---|---|
| `--make-shared` | `shared` | `--make-rshared` | `rshared` | Events propagate to and from peer mounts. |
| `--make-slave` | `slave` | `--make-rslave` | `rslave` | Events propagate *in* from the master but not back out. |
| `--make-private` | `private` | `--make-rprivate` | `rprivate` | No propagation either way. |
| `--make-unbindable` | `unbindable` | `--make-runbindable` | `runbindable` | Private, and cannot be bind-mounted. |

A freshly created mount, bind or move is **private** by default unless its destination's parent is shared, in which case it joins that peer group — the standard kernel rule.

## Source specification

| Form | Resolution |
|---|---|
| Device path (`/dev/sda1`) | Used directly. |
| `-L LABEL` / `LABEL=`, `-U UUID` / `UUID=`, `PARTLABEL=`, `PARTUUID=` | Resolved to a device via libblkid. No match is a mount failure (exit 32). |
| A directory or regular file | Used directly (a bind source or target; a file may be a bind target). |
| A pseudo source (`tmpfs`, `proc`, `sysfs`, `none`, …) | Passed through; `-t` is required because it cannot be probed. |
| An image file | Attached through a loop device (see [Loop devices](#loop-devices)). |

## Filesystem type

`-t TYPE` names the type explicitly. `-t auto`, or omitting `-t` entirely, asks libblkid to **safely probe** the source; an ambiguous or multiply-signed device is refused (exit 32) rather than guessed at. `X-mount.auto-fstypes=LIST` constrains the probe candidates. Type *lists* and `no<type>` negation are not accepted in the mounting path (they remain meaningful only as a [listing filter](#list-mode)).

## The `-o` option language

`-o` takes a comma-separated list of `key` or `key=value` tokens and is repeatable (all occurrences are joined). A `key="..."` double-quoted value protects embedded commas; the `key=value` split is on the **first** `=`. Every token is sorted into one of the following categories.

### Per-mount attributes

Applied to the mount itself. Each accepts its negation; `ro`/`rw` additionally accept a `=vfs` / `=fs` / `=recursive` scope qualifier (bare is `=vfs`, non-recursive; `=fs` targets the superblock read-only flag; `=recursive` applies to the whole subtree).

| Option | Effect |
|---|---|
| `ro` / `rw` | Read-only / read-write. |
| `suid` / `nosuid` | Honour / ignore set-user-ID bits. |
| `dev` / `nodev` | Allow / disallow device nodes. |
| `exec` / `noexec` | Allow / disallow execution. |
| `atime` / `noatime` | Update / never update access times. |
| `relatime` / `norelatime` | Relative-atime updates on or off. |
| `strictatime` / `nostrictatime` | Strict-atime updates on or off. |
| `diratime` / `nodiratime` | Directory access-time updates on or off. |
| `nosymfollow` | Do not follow symlinks on this mount. There is no positive `symfollow` token — the attribute is cleared only via a remount mask. |

The atime tokens share a single mode field; the last one specified wins.

### Superblock flags

| Option | Effect |
|---|---|
| `sync` / `async` | Synchronous / asynchronous writes. |
| `dirsync` | Synchronous directory updates. |
| `lazytime` / `nolazytime` | Lazy on-disk timestamp updates on or off. |
| `iversion` / `noiversion` | Inode version counting on or off. |
| `silent` / `loud` | Suppress or emit certain kernel messages. |
| `mand` / `nomand` | Obsolete (removed from the kernel). Accepted and ignored with a note under `-v`. |

### Filesystem-specific parameters

Any token not recognised above is forwarded verbatim to the filesystem: `key=value` as a string parameter, a bare `key` as a flag. There is no built-in allow-list and no length limit; a rejection by the filesystem is reported with the offending key and the kernel's message.

For example, StrataFS takes its precedence-ordered directory stack through
`strata=`:

```sh
mount -t stratafs none /bin -o 'strata=/lcl/bin+create:/usr/bin+ro'
```

See [StrataFS](/peios/using-peios/disks-and-filesystems/stratafs/overview.md) for the stack flags, merge
and write-routing model, and the `stratafs` inspection command.

### Userspace-only tokens

These never reach the filesystem. They include the meta-verbs `remount`, `bind`, `rbind`, `move`; the loop controls `loop` / `loop=/dev/loopN`, `offset=`, `sizelimit=` (numeric values accept `K`/`M`/`G`/`T` and `KiB`/`MiB`/… suffixes); the propagation tokens listed [above](#propagation-flags); and `defaults`, which expands to `rw,suid,dev,exec,async` (later tokens override it).

### Functional `X-mount.*` options

| Option | Effect |
|---|---|
| `X-mount.mkdir[=mode]` | Create the target directory if missing (default mode `0755`; the alias of `-m`). |
| `X-mount.subdir=DIR` | Attach subdirectory `DIR` of a freshly mounted filesystem at the target. Effective only for a new-instance mount; silently ignored (noted under `-v`) for bind/move/remount/propagation. |
| `X-mount.noloop` | Suppress the implicit loop device for a regular-file source. |
| `X-mount.auto-fstypes=LIST` | Constrain the `-t auto` probe to these types. |
| `X-mount.nocanonicalize[=source\|target]` | The `-o` form of `-c`; with `=source` or `=target` it disables canonicalisation for just that path. |

`X-mount.idmap` and `X-mount.owner` / `group` / `mode` are **not supported** and are rejected with a usage error: Peios ownership is SID/security-descriptor based, so the fix is to add a SID/ACE to the descriptor rather than remap or chown.

### KACS mount policy

This is the genuinely Peios-specific part of `mount`. A mount policy is a per-superblock setting that governs how FACS treats the filesystem; the full model is described in [Mount policies](/peios/using-peios/mount-policies/overview.md) and its detail pages. `mount` can set that policy at attach time:

| Option | Policy class |
|---|---|
| `-o policy=deny-missing` | `facs_deny_missing` — a file with no SD is unreachable. |
| `-o policy=synth-ephemeral` | `facs_synthesize_ephemeral` — a missing SD is synthesised in memory only. |
| `-o policy=synth-persist` | `facs_synthesize_persistent` — a missing SD is synthesised and written back. |
| `--synth-sddl SDDL` | Provide the mount-level SD template used during synthesis (only valid with a `synth-*` policy). |

`policy=unmanaged` is **not** user-settable; only the kernel sets the unmanaged class, for its own pseudo-filesystems. `policy=` is valid only on a **new mount** of a real filesystem — combining it with bind/move/remount/propagation, or with list mode, is a usage error. See [Policy classes](/peios/using-peios/mount-policies/policy-classes.md) for what each class does and [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md) for how the SD is physically stored.

The policy is applied to the *detached* filesystem before it is attached: if setting it fails, nothing is ever published with an unintended policy (no rollback needed). Because setting a mount policy is a `SeTcbPrivilege`-gated operation (see [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md)), a caller without that privilege gets a clean `EPERM` (exit 1) and no mount. `--synth-sddl` is validated client-side first — it must be well-formed SDDL and must include an owner.

Peios applies no coarse `uid==0` check anywhere: the `mount` applet is not installed set-user-ID, and every privileged action is authorised per-operation by KACS.

### Remounting

`-o remount` changes the options of an existing mount without detaching it. Peios does **not** perform the util-linux "read the old flags and re-supply them" dance — the fd-based API applies deltas rather than resetting, so each remount touches only what you name. Per-mount attributes (`ro`/`rw`, `nosuid`, atime, …) are changed via `mount_setattr`; superblock flags, filesystem parameters and `ro=fs`/`rw=fs` go through the superblock reconfigure path. `=recursive` remounts the whole subtree, all-or-nothing.

A **bind** mount shares its source's superblock, so any superblock-level option on a bind remount (a Category-B flag, a filesystem parameter, or `ro=fs`/`rw=fs`) is refused as a usage error rather than silently mutating the shared superblock for every mount of that filesystem. Only per-mount VFS attributes are valid on a bind remount.

## Loop devices

A regular-file source is backed by a loop device automatically **when the type is unspecified or the filesystem is recognised by libblkid**; `X-mount.noloop` suppresses this. `-o loop` forces auto-allocation of a free device, `loop=/dev/loopN` names one, and `offset=` / `sizelimit=` select a region of the file (they imply loop and are an error against a real block device). A backing file already attached at the same offset and size is *reused* rather than doubly attached, to avoid corruption. Loops created by `mount` are auto-cleared by the kernel on unmount, so they do not leak; `umount -d` force-clears the rest (see [`umount`](/peios/using-peios/mount-policies/umount.md)).

## Other flags

| Flag | Effect |
|---|---|
| `-t`, `--types TYPE` | Filesystem type, or `auto`. |
| `-o`, `--options LIST` | Mount options (above); repeatable. |
| `-r`, `--ro`, `--read-only` | Mount read-only (`-o ro`). |
| `-w`, `--rw`, `--read-write` | Mount read-write and **forbid** the automatic read-only fallback on a write-protected device (`-o rw`). |
| `--source SRC` / `--target DIR` | Name the operands explicitly. |
| `--target-prefix DIR` | Prepend `DIR/` to the target. |
| `-B`, `--bind` / `-R`, `--rbind` / `-M`, `--move` / `--beneath` | The structural verbs. |
| `--make-*`, `--make-r*` | Propagation changes. |
| `--exclusive` | Force a unique superblock instance (no reuse). Meaningful only for multi-instance filesystems (e.g. `tmpfs`) or read-only block mounts; on an already-mounted writable block device it fails with `EBUSY` (exit 32). |
| `-m`, `--mkdir[=MODE]` | Create the target directory if missing (default mode `0755`). |
| `-L`, `--label LABEL` / `-U`, `--uuid UUID` | Select the source by filesystem label / UUID. |
| `-c`, `--no-canonicalize` | Do not canonicalise paths. |
| `-f`, `--fake` | Dry run: parse, resolve and plan everything but skip the mount syscalls and the policy step. |
| `-v`, `--verbose` | Narrate resolved values and each syscall; drain the kernel `fs_context` log. Repeatable but `-vv` is the same as `-v`. |
| `-l`, `--show-labels` | In list mode, append each filesystem's label. |
| `--onlyonce` | Skip the mount if it is already present (a driver-aware check against live mount state, not a naive string match). |
| `-N`, `--namespace NS` | Operate inside mount namespace `NS` (a PID, an ns file path, or a named namespace). Source resolution happens in the caller's namespace; the mount lands in the target namespace. |
| `-i`, `--internal-only` | Do not invoke a `mount.<type>` helper. |
| `-n`, `--no-mtab` | Accepted and ignored (Peios has no mtab). |
| `--synth-sddl SDDL` | KACS synth-policy template SD (above). |
| `-h`, `--help` / `-V`, `--version` | Standard. |

No external mount helpers ship in this version, so a network or FUSE type fails with "no helper for type" (exit 1) — a clean deferral, not a crash.

## List mode

With no operands, `mount` prints one line per mount, read from `/proc/self/mountinfo`:

```
SOURCE on TARGET type FSTYPE (OPTIONS)
```

`mount -t TYPE` with no operands *filters* the list by filesystem type instead of mounting; here type lists (`-t ext4,xfs`) and `no<type>` negation (`-t nosysfs`) are honoured. `-l` appends ` [LABEL]` to each line when a label is known.

Details of the rendering: the option field is the positional VFS-then-superblock merge with a coalesced `ro`/`rw`, not sorted; mountinfo octal escapes are decoded; control characters in the mount point become `?`; and the source is shown as the kernel recorded it, with `/dev/loopN` resolved to its backing file and `/dev/dm-N` to `/dev/mapper/<name>`. The listing shows only mounts the caller's token may observe; a partial or empty view is correct, not an error, and the paths of filtered-out parents are never reconstructed.

For a device-oriented view of what is available to mount, use [`lsblk`](/peios/using-peios/mount-policies/lsblk.md).

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Incorrect invocation or a permission denial — bad flags, mutually-exclusive verbs, a lone operand, an invalid `policy=` or `--synth-sddl`, an authorisation failure (`EPERM`/`EACCES`), an embedded NUL, or "no helper for type". |
| `2` | System error (out of memory, no free loop device, cannot fork). |
| `4` | Internal error (an invariant failure). |
| `8` | Interrupted (SIGINT), after signal-safe cleanup. |
| `32` | Mount failure — a syscall in the flow failed, a label/UUID had no match, a probe was undetermined, or a `--beneath`/move/`--exclusive` kernel refusal. |
| `126` | An external `mount.<type>` helper was found but failed to execute (moot until a helper ships). |

Code `16` (mtab) is never produced. Code `64` (some succeeded, some failed) only arises when several source arguments are given and at least one succeeds while another fails.

---

# umount

_Peios / Using Peios / Peiosutils / Disks and mounts_

> Detach a filesystem from the Peios mount tree by mount point or by source, with lazy, forced, recursive and all-targets variants.

`umount` detaches a filesystem from the Peios mount tree. It is the counterpart of [`mount`](/peios/using-peios/mount-policies/mount.md) and, like it, reads live mount state from the kernel (`/proc/self/mountinfo`) rather than any `/etc/mtab` — Peios has none. Each operand is resolved against that live state and unmounted with `umount2(2)`.

```
umount [-lfRA] [-dr] TARGET|SOURCE...
```

## Operands and how they are resolved

Each operand is either a **mount point** or a **source**:

- If the (canonicalised) operand is itself a mount point in the current mount table, it is unmounted directly. This is always unambiguous.
- Otherwise the operand is treated as a **source** and matched against the sources in the mount table. If it is mounted in exactly one place, that place is unmounted. If it is mounted in **several** places, `umount` refuses with an error naming the candidates — resolve it by naming a specific mount point, or use `-A` to unmount all of them.

If an operand matches nothing, it is "not mounted": normally an error (exit 32), but `-g` makes that a success and `-q` suppresses the message.

Several operands may be given in one invocation, and options may be interspersed with them (`umount /a -f /b`). Paths are handled as opaque bytes; an embedded NUL is a usage error. By default each operand is canonicalised; `-c` disables that and additionally selects `UMOUNT_NOFOLLOW` so the kernel does not follow a symlink in the final component.

## Recursive and all-targets unmounts

Two flags expand a single operand into multiple unmounts. Within one operand the expansion **stops on the first failure**.

- **`-R` / `--recursive`** unmounts the target and everything mounted underneath it, including any over-mount stack at a single point, ordered deepest-first.
- **`-A` / `--all-targets`** unmounts *every* mount point of the given source in the current mount namespace. This is the live-mountinfo "unmount everywhere" complement to source resolution; it is not an fstab feature.
- **`-A -R` together** compose: for each mount point of the source, recurse underneath it.

`-R` and `-r` are mutually exclusive (combining them is a usage error, exit 1).

## Flags

| Flag | Effect |
|---|---|
| `-l`, `--lazy` | Detach the filesystem now and clean up references later (`MNT_DETACH`). |
| `-f`, `--force` | Force the unmount (`MNT_FORCE`), e.g. for an unreachable server. |
| `-R`, `--recursive` | Unmount the target and everything under it (see above). |
| `-A`, `--all-targets` | Unmount every mount point of the given source (see above). |
| `-d`, `--detach-loop` | After unmounting, free the backing loop device. Best-effort and verified: it clears the device only if it still backs the mount just removed, so a recycled loop number is never clobbered. Usually redundant, since `mount`-created loops auto-clear on unmount. |
| `-r`, `--read-only` | If the unmount fails, remount the filesystem read-only instead (via `mount_setattr`). Mutually exclusive with `-R`. |
| `-g`, `--graceful` | Exit `0` when the target is absent or not mounted (rather than failing). Applied unconditionally. |
| `-q`, `--quiet` | Suppress "not mounted" messages. |
| `-c`, `--no-canonicalize` | Do not canonicalise paths; selects `UMOUNT_NOFOLLOW`. Applied unconditionally. |
| `-v`, `--verbose` | Say what is being unmounted. Repeatable. |
| `-N`, `--namespace NS` | Enter mount namespace `NS` (a PID, an ns file path, or a named namespace) before reading its mount table and unmounting. All work happens inside that namespace. |
| `-n`, `--no-mtab` | Accepted and ignored (no mtab on Peios). |
| `-i`, `--internal-only` | Do not invoke a `umount.<type>` helper (none ship). |
| `--fake` | Dry run: resolve operands and report, but skip the unmount syscalls. |
| `-h`, `--help` / `-V`, `--version` | Standard. |

The `umount2` flag mapping is direct: `-l` → `MNT_DETACH`, `-f` → `MNT_FORCE`, `-c` (or a symlinked final component) → `UMOUNT_NOFOLLOW`. `MNT_EXPIRE` is deliberately not exposed, matching util-linux.

### On privilege

As with `mount`, there is no coarse `uid==0` check: the applet is not set-user-ID and every unmount is authorised per-operation by KACS. Where util-linux silently suppresses an option for non-root users (`-c`, and `-g`'s effectiveness), Peios applies it unconditionally.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success (or a `-g` no-op on an absent target). |
| `1` | Incorrect invocation or a permission denial — including `-R` with `-r`, a missing operand, an ambiguous source, an embedded NUL, or an authorisation failure. |
| `2` | System error (e.g. cannot read the mount table). |
| `8` | Interrupted (SIGINT). |
| `32` | Unmount failed, or the target is not mounted (unless `-g`). |
| `64` | Several source/target arguments were given and at least one succeeded while another failed. |
| `126` | An external `umount.<type>` helper was found but failed to execute (moot until a helper ships). |

A single `-R` / `-A` / `-A -R` invocation stops on the first failure and yields `32`; the aggregate `64` only appears across multiple top-level arguments. To see what is currently mounted before unmounting, use [`mount`](/peios/using-peios/mount-policies/mount.md) with no arguments or [`lsblk`](/peios/using-peios/mount-policies/lsblk.md).

---

# lsblk

_Peios / Using Peios / Peiosutils / Disks and mounts_

> List block devices as a tree, flat list, JSON or key=value pairs — with filesystem identity, topology, and SD-derived owner and mode columns.

`lsblk` lists the block devices on the system and their relationships — disks, their partitions, and the device-mapper and loop devices layered on top. It is the device-oriented companion to [`mount`](/peios/using-peios/mount-policies/mount.md): where `mount` with no arguments shows what is *mounted*, `lsblk` shows what is *available to mount* and what filesystem sits on each device.

```
lsblk [options] [DEVICE...]
```

By default it prints a tree, one row per device with children indented beneath their parent. Given one or more `DEVICE` operands (a name like `sda`, a full path like `/dev/sda`, or anything resolving to a device node), it re-roots the output at those devices.

## Where the data comes from

Each column is sourced from one of four places, and any that cannot be read degrades to an empty cell rather than aborting the run:

- **sysfs** (`/sys/block`) — the device tree, sizes, and the topology/hardware columns. Peios leaves `/sys/block` unpatched, so this is the standard no-udev path.
- **libblkid** — filesystem identity: `FSTYPE`, `FSVER`, `UUID`, `LABEL`, and the partition-table columns. libblkid is opened at runtime.
- **The device node's security descriptor** — `OWNER` and `MODE`, read the same way [`ls -l`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) reads them.
- **The `/dev/disk/by-*` symlink farm** — `ID-LINK`. Populated by the device manager once it has run; there is deliberately no `/run/udev/data` parser, so the column reads the links themselves and is empty in the initramfs, where no device manager runs.

## Output modes

The mode selects how the rows are formatted; it does not change which columns are shown.

| Flag | Mode |
|---|---|
| *(default)* | Indented tree. |
| `-l`, `--list` | Flat list — the same columns, no tree glyphs. |
| `-J`, `--json` | JSON. Flag columns render as bare booleans and `MOUNTPOINTS` as an array; children nest under a `children` key. |
| `-P`, `--pairs` | `KEY="value"` pairs, one device per line. |
| `-r`, `--raw` | Raw, space-separated. Values that could contain a space or control character are hex-escaped (`\xNN`) so fields stay parseable. |
| `-T`, `--tree[=COLUMN]` | Force tree output even alongside `-l`, optionally attaching the tree glyphs to `COLUMN` instead of `NAME`. |

## Columns

With no column flag, `lsblk` prints the default set: `NAME`, `MAJ:MIN`, `RM`, `SIZE`, `RO`, `TYPE`, `MOUNTPOINTS`. Three flags swap in a preset, and `-o` names an explicit list (which wins over all of them):

| Flag | Column set |
|---|---|
| `-o`, `--output LIST` | Exactly the comma-separated columns named (case-insensitive; an unknown name is a usage error). |
| `-O`, `--output-all` | Every available column. |
| `-f`, `--fs` | Filesystem view: `NAME`, `FSTYPE`, `FSVER`, `LABEL`, `UUID`, `MOUNTPOINTS`. |
| `-m`, `--perms` | Permissions view: `NAME`, `SIZE`, `OWNER`, `MODE`. |

The available columns are `NAME`, `KNAME`, `PATH`, `MAJ:MIN`, `FSTYPE`, `FSVER`, `LABEL`, `UUID`, `PTUUID`, `PTTYPE`, `PARTTYPE`, `PARTLABEL`, `PARTUUID`, `MOUNTPOINT`, `MOUNTPOINTS`, `SIZE`, `RO`, `RM`, `HOTPLUG`, `TYPE`, `OWNER`, `MODE`, `MODEL`, `VENDOR`, `REV`, `SERIAL`, `TRAN`, `HCTL`, `ALIGNMENT`, `MIN-IO`, `OPT-IO`, `PHY-SEC`, `LOG-SEC`, `STATE`, `ROTA`, `SCHED`, `PKNAME`, and `ID-LINK`.

### The `OWNER` and `MODE` columns

`OWNER` and `MODE` describe the *device node*, not the filesystem on it, and they mirror [`ls -l`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) exactly — there is no `GROUP` column and no POSIX permission bits, because access to a device node is governed by its security descriptor, not by a mode. `OWNER` is the owner SID (`S-1-…`); `MODE` is a three-character `[type][x][+]`: the device-type character (`b` for a block device, `c` for a character device), an `x` slot (never set for a block device), and a `+` when the DACL is inheritance-protected. When the SD cannot be read — for instance on a non-Peios host with no KACS syscalls — `OWNER` shows `?` and `MODE` degrades honestly.

## Filtering, sorting and shaping

| Flag | Effect |
|---|---|
| `-a`, `--all` | Include empty (zero-size) devices, which are hidden by default. |
| `-d`, `--nodeps` | Do not print a device's holders or slaves (drop the children). |
| `-I`, `--include LIST` | Show only devices with these major numbers (comma-separated). |
| `-e`, `--exclude LIST` | Exclude devices by major number. The default is `1` (RAM disks); an explicit `-e` replaces that default, and `-a` clears exclusions entirely. |
| `-s`, `--inverse` | Print dependencies in inverse order (holders above the devices they depend on). |
| `-M`, `--merge` | Collapse a subtree shared by several parents (a multipath device) to a single occurrence. |
| `-E`, `--dedup COLUMN` | Drop rows whose `COLUMN` value duplicates an earlier one. |
| `-x`, `--sort COLUMN` | Sort siblings by `COLUMN` (numeric columns sort by value, not text). |

## Formatting

| Flag | Effect |
|---|---|
| `-b`, `--bytes` | Print `SIZE` as an exact byte count instead of a human-readable value. |
| `-p`, `--paths` | Print full `/dev` paths in the `NAME` column. |
| `-n`, `--noheadings` | Omit the header row. |
| `-i`, `--ascii` | Draw the tree with ASCII characters instead of box-drawing glyphs. |
| `-y`, `--shell` | Render column keys shell-safe (`MAJ:MIN` becomes `MAJ_MIN`). |
| `-w`, `--width NUM` | Truncate each table row to `NUM` columns wide. |
| `--sysroot DIR` | Read sysfs, the mount table and `/dev` from `DIR` instead of `/` (chiefly for testing against a fixture). |
| `-h`, `--help` / `-V`, `--version` | Standard. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Incorrect invocation (an unknown column, a malformed major-number or width value) or a failed run (for example, `/sys/block` is unreadable). |

`lsblk` uses this single non-zero code, matching util-linux. A per-device read failure is not fatal: it degrades the affected columns to empty or `?` and the run continues. A broken output pipe (piping into `head`, for instance) is treated as success.

---

# mkirf

_Peios / Using Peios / Peiosutils / Boot images_

> Compile the /boot/initramfs/ source tree into a deterministic cpio initramfs image, resolving hook order and validating the layout. Has a watch mode.

`mkirf` compiles an initramfs *source tree* into an initramfs *image*. The source tree is an ordinary directory — on a running Peios system that directory is [`/boot/initramfs/`](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) — whose contents map 1:1 onto `/` inside the initramfs. The image is a single deterministic, gzip-compressed newc cpio archive that the bootloader loads into memory alongside the kernel.

```
mkirf [--watch] [--debounce SECS] [--exclude GLOB]... <src-dir> <out-file>
```

This page is the command reference. For what the initramfs stage *is* — prelude, the `/boot/initramfs/` layout, and the handoff to the real root — see [The initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md); for how hooks declare their order, see [Boot hooks](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md). `mkirf` reads `<src-dir>` and writes `<out-file>`; it never writes back into the source tree, so the directory you inspect is always exactly what packages and you have put there.

## What a build does

Given a source tree, one build runs a fixed sequence:

1. **Validate the layout.** `<src-dir>` must be a directory, must carry an executable `init` (the initramfs PID 1 — a symlink is fine as long as it resolves within the tree), and must *not* already contain a hook sequence — `hooks.seq` or `system/prelude/hooks.seq.<n>` — which `mkirf` generates. A missing or non-executable `init`, or a stray sequence file, stops the build.
2. **Walk the tree.** Every object beneath `<src-dir>` becomes a cpio entry — regular files (with their executable bit preserved), directories, symlinks (target stored verbatim), character/block device nodes, and FIFOs. Sockets, and any other unsupported type, are rejected. Entries are sorted in `LC_ALL=C` byte order, which places every directory before its descendants — the order the kernel's unpacker requires.
3. **Resolve the hook order.** Hooks are the regular files directly under `<src-dir>/usr/libexec/prelude/hooks.d/` (packaged) or `<src-dir>/lcl/libexec/prelude/hooks.d/` (operator-placed), with the operator's copy winning when both hold the same file name. The legacy `<src-dir>/hooks/` is still read, and each hook found there is reported with the directory it should move to. `mkirf` parses each hook's `# /// hook` metadata block, topologically sorts the resulting capability DAG, and bakes the execution order into generated manifests injected into the image at `/system/prelude/hooks.seq.<n>`. A missing `hooks/` directory simply means no hooks — the manifests are still written, empty, because prelude treats a *missing* sequence as a broken image rather than as "nothing to run".

   The version is in the **file name**, and `mkirf` writes every version it can express faithfully — currently `hooks.seq.1` (the flat resolved order) and `hooks.seq.2` (that order plus each hook's declarations). `mkirf` ships in `peiosutils` and prelude ships in its own package, so an image can pair a newer writer with an older reader; naming the versions separately lets one image satisfy both, which a single file carrying an in-band version cannot. A version is dropped only when a future format carries something it can no longer project honestly.

   They live under `/system` — the tier for *derived* content, alongside `system/retc` — rather than `/usr`, which is the vendor tier no package ships these into, or `/var/state`, which is for mutable state rather than immutable build output. See [Validation and errors](#validation-and-errors) below.
4. **Write atomically.** The archive is written to a sibling temp file and `rename`d into place, so a failed run never leaves a half-written image behind. On success `mkirf` prints the path, entry count, and byte size to stderr.

### Early (pre-decompression) segments

A reserved `<src-dir>/++/` directory, if present, holds *early* initramfs segments — content the kernel consumes **before** it decompresses the main archive, namely CPU microcode and ACPI table overrides. Each immediate child of `++/` is one segment whose own contents map onto the cpio root (`++/microcode/kernel/x86/microcode/GenuineIntel.bin` becomes `kernel/x86/microcode/GenuineIntel.bin`); the segment directory name is a human label and never appears in the archive. Every `++/` entry must itself be a directory. The segments are merged into one **uncompressed** cpio prepended ahead of the gzip-compressed main archive. With no `++/` directory the output is exactly a single gzip member. `mkirf` stays format-agnostic here: it knows "early segments", never "microcode".

## The deterministic-build guarantee

The same source tree always compiles to the same image, byte for byte. This is what makes "did anything actually change?" a meaningful question and underpins later work such as signed boot artifacts. Determinism comes from normalising everything that would otherwise vary between builds:

- **Ownership, inode numbers, and timestamps** are all emitted as zero.
- **Permission bits are normalised.** The source tree is authoritative for file *type* and, for regular files, *executability* — nothing else. Read/write permission bits are not Peios's access mechanism, so they are flattened to a constant: directories `0755`, symlinks `0777`, FIFOs and device nodes `0644`, regular files `0755` if executable and `0644` otherwise.
- **The gzip member** carries mtime 0 and no embedded filename (the `gzip -n` equivalent), at compression level 9.
- **The early region** is byte-stable, with file payloads padded to a 16-byte boundary by widening the preceding entry's name field with NUL bytes.

## Validation and errors

`mkirf` will not produce an image from a source tree that cannot boot. A misconfiguration is caught when the image is built — on a running system where the message is easy to read — rather than as a mystery failure at the next boot. The checks:

- **Layout.** `<src-dir>` is not a directory; no `init`, or `init` is not a regular file, or is not executable; a pre-existing hook sequence; a `++` entry that is not a directory; two `++` segments defining the same file. (Exit 1.)
- **Hooks.** A malformed `# /// hook` metadata block (unclosed, a non-comment line inside it, a duplicate or unknown key, a badly-formed array), a dependency **cycle**, a `requires` that **nothing supplies**, or a capability that is **both provided and contributed to** (a capability is either a set of alternatives or a set of contributors, never both). An `after` naming a capability nothing supplies is *not* an error — that is what distinguishes it from `requires`. (Exit 1.) A hook with no metadata block at all is *not* an error — it is the escape hatch: it is scheduled last, in name order, and `mkirf` prints a warning so a forgotten block is visible.
- **I/O.** Any filesystem read, write, or compression failure. (Exit 1.)
- **Usage.** A semantic invocation error clap cannot express — currently, `--watch` with `<out-file>` inside `<src-dir>` (see below). (Exit 2.)

## Watch mode

With `--watch`, `mkirf` stays resident: it builds once up front (so the watch never begins from a stale archive), then watches `<src-dir>` recursively and rebuilds after every change, debounced. This is what lets `/boot/initramfs/` behave like an ordinary part of the filesystem — edit a hook and the initramfs is current again — rather than a build artifact an administrator has to remember to regenerate.

Watch mode is a foreground loop that runs until killed; supervising and restarting it is a service manager's job, not `mkirf`'s. A rebuild that fails (say, a hook edited into a dependency cycle) is logged but does **not** stop the watch, so fixing the offending file recovers on the next change.

`<out-file>` must not live inside `<src-dir>`: each rebuild would write the image back into the watched tree and retrigger the watch endlessly. `mkirf` refuses this invocation up front (exit 2). The `--debounce` window (default 5 seconds) is the settle time — `mkirf` waits for the tree to be quiet for that long before rebuilding, so a burst of edits produces one rebuild rather than many.

## Options

| Option | Effect |
|---|---|
| `--watch` | Stay resident and rebuild on every change to `<src-dir>`, after an initial build. Runs until killed. |
| `--debounce SECS` | With `--watch`, the settle time before a rebuild. Default `5`. A non-numeric value is a usage error. |
| `--exclude GLOB` | Exclude paths (relative to `<src-dir>`) matching `GLOB` from the image. Repeatable. `*` and `?` stay within a path segment; `**` crosses separators. A matched directory is pruned with its whole subtree. A malformed glob is a usage error. |

The two positionals are both required:

| Positional | Meaning |
|---|---|
| `<src-dir>` | The source tree; its contents map onto `/` in the initramfs. |
| `<out-file>` | The output `cpio.gz` archive. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | The image was built (or `--help`/`--version` was printed). |
| `1` | An operational failure — an invalid layout, an unresolvable hook set, or an I/O/compression error. |
| `2` | A usage error — a bad option, a missing positional, or `--watch` with `<out-file>` inside `<src-dir>`. |

## See also

- [The initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) — what `mkirf`'s output is for.
- [Boot hooks](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md) — the `# /// hook` metadata block and how order is resolved.
- [mkuki](/peios/security-fundamentals/boot-and-trust-establishment/mkuki.md) — wraps `mkirf`'s image, together with a kernel and command line, into a bootable UEFI unified kernel image.

---

# mkuki

_Peios / Using Peios / Peiosutils / Boot images_

> Build a UEFI unified kernel image — kernel, initramfs, and command line appended as PE sections onto an EFI stub. Has a watch mode.

`mkuki` builds a **UEFI unified kernel image (UKI)**: it takes a PE/COFF EFI stub and appends the kernel, the initramfs, and the kernel command line to it as named PE sections, producing a single EFI binary that UEFI firmware boots directly. Where [`mkirf`](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md) produces the initramfs image, `mkuki` is the step that wraps that image — together with a kernel and a command line — into one bootable artifact. The two are the two halves of Peios' Dynamic Boot system.

```
mkuki --kernel PATH --initramfs PATH (--cmdline TEXT | --cmdline-file PATH) --out PATH
      [--stub PATH] [--watch] [--debounce SECS]
mkuki --stub-info
```

## The UKI section layout

A UKI is an ordinary PE/COFF EFI executable — the *stub* — with three extra sections appended, which the stub reads at boot to find its payloads:

| Section | Content | Source |
|---|---|---|
| `.cmdline` | The kernel command line, NUL-terminated. | `--cmdline` or `--cmdline-file` |
| `.linux` | The kernel image. | `--kernel` |
| `.initrd` | The initramfs cpio image. | `--initramfs` |

`mkuki` appends them in exactly that order. Each new section is marked as initialised, read-only data. The tool works at the PE level directly: it parses the stub's DOS/PE headers and section table, computes correctly-aligned virtual addresses and file offsets for the new sections, appends their data, updates the section count and the `SizeOfImage`/`SizeOfHeaders` fields, and — if the stub's header has no spare room for three more section entries — grows the header region and relocates the existing sections' file pointers to make space. A stub that is not a valid `MZ`/`PE` image, or whose optional header is malformed, is rejected.

### The command line

The command line is taken either literally from `--cmdline` or read from the file named by `--cmdline-file` (exactly one is required). Either way, `mkuki` trims trailing newlines and carriage returns and appends a single NUL terminator. A command line containing an embedded NUL byte is rejected.

### The stub

`--stub` names the EFI stub to append to. With no `--stub`, `mkuki` uses a stub bundled into the binary (the systemd EFI stub). `mkuki --stub-info` prints that bundled stub's provenance and exits, so you can see exactly what the default is without building anything.

## Output and atomicity

`mkuki` creates any missing parent directories of `--out`, writes the assembled image to a sibling temp file, and `rename`s it into place, so an interrupted or failed build never leaves a half-written UKI behind. On success it prints the output path and byte size to stderr. Each of the three payloads must be non-empty; an empty `.cmdline`, `.linux`, or `.initrd` fails the build.

> [!NOTE]
> `mkuki` does **not** sign the image. Producing the unified binary and signing it for Secure Boot are separate steps; `mkuki` has no signing options. Because a UKI bundles the kernel, initramfs, and command line into one PE image, that image is what a later Secure Boot milestone signs and the firmware verifies as a whole — but the signing itself is out of `mkuki`'s scope.

## Watch mode

With `--watch`, `mkuki` stays resident: it builds once up front, then rebuilds the UKI whenever an input changes — so the boot image tracks a new kernel, a freshly-repacked initramfs (`mkirf`'s half of Dynamic Boot), or an edited command-line file with no manual step. Like [`mkirf`'s watch mode](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md), it is a foreground loop that runs until killed; supervising it is a service manager's job, and a rebuild that fails (for example, a kernel caught mid-copy) is logged rather than fatal, so fixing the input recovers on the next change.

The watched inputs are `--kernel`, `--initramfs`, and — only if it is a `--cmdline-file`, since a literal `--cmdline` is static — the command-line file. `mkuki` watches each input's **parent directory** (non-recursively), not the file itself: this survives the atomic temp-and-rename writes that `mkirf` and `mkuki` both perform (which appear as a directory event a stale single-file watch would miss) and catches a versioned kernel being swapped in `/boot`. Because the watch is non-recursive, a write to an `--out` path nested deeper under a watched directory does not retrigger it — but an `--out` sitting *directly* in a watched input directory would, so `mkuki` refuses that invocation. The `--debounce` window (default 5 seconds) is the settle time before a rebuild.

## Options

| Option | Effect |
|---|---|
| `--kernel PATH` | The kernel image; becomes the `.linux` section. Required (except with `--stub-info`). |
| `--initramfs PATH` | The initramfs cpio; becomes the `.initrd` section. Required (except with `--stub-info`). |
| `--cmdline TEXT` | The kernel command line, given literally; becomes the `.cmdline` section. Mutually exclusive with `--cmdline-file`. |
| `--cmdline-file PATH` | Read the kernel command line from `PATH` instead. Mutually exclusive with `--cmdline`. |
| `--out PATH` | The output UKI path. Required (except with `--stub-info`). |
| `--stub PATH` | The PE/COFF EFI stub to append sections to. Defaults to the bundled systemd stub. |
| `--stub-info` | Print the bundled stub's provenance and exit. |
| `--watch` | Stay resident and rebuild the UKI whenever `--kernel`, `--initramfs`, or `--cmdline-file` changes. Runs until killed. |
| `--debounce SECS` | With `--watch`, the settle time before a rebuild. Default `5`. |

Exactly one of `--cmdline` or `--cmdline-file` must be given: supplying both, or neither, is a usage error.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The UKI was built (or `--stub-info`, `--help`, or `--version` was printed). |
| `1` | A build or watch failure — a missing or unreadable input, an invalid stub, an empty payload, or an I/O error. |
| `2` | A usage error — a bad or missing option, or an invalid `--cmdline`/`--cmdline-file` combination. |

## See also

- [mkirf](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md) — builds the `.initrd` payload `mkuki` wraps.
- [The initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) — what the bundled initramfs does once firmware boots the UKI.

---

# reg

_Peios / Using Peios / Peiosutils / Registry tools_

> The reg command reads and writes the live registry — values, keys, layers, links, security descriptors, watches, and atomic batches.

`reg` is the command-line interface to the **live** registry — the running [LCS](/peios/using-peios/registry-administration/lcs-and-sources.md) store, reached through the registry system calls. Where [`regman`](/peios/using-peios/registry-administration/regman.md) reads shipped documentation and tells you what a key *means*, `reg` talks to the kernel and reads or writes what a key *is*. It is the everyday tool for scripting configuration: querying effective values, writing to layers, masking and hiding, managing security descriptors, watching for change, and taking backups.

```
reg <command> [options] <key> [value] [data]
```

`reg` is the read/write half of the registry toolset and [`regman`](/peios/using-peios/registry-administration/regman.md) is the lookup half. The division is exact:

| To… | Use… |
|---|---|
| Read or change what a value is *set to*, on this machine, right now | `reg` |
| Look up what a value *means* — its type, default, valid range, when it applies | [`regman`](/peios/using-peios/registry-administration/regman.md) |

Consult `regman` first to learn the legal range for a knob, then use `reg set` to write a value inside it. `reg` never checks a value's meaning and `regman` never touches the live store; they are complementary.

## How reg treats access and privilege

Every `reg` operation goes through the kernel's access check against the single key it opens — the same [access-control](/peios/using-peios/registry-security/access-control.md) path as any other protected object, with **no traversal check** on the parent keys. `reg` performs no identity or membership checks of its own: it does not refuse a privileged operation up front. It attempts the operation and reports faithfully whatever the kernel returns. An operation that needs a privilege you lack (creating a link, a positive-precedence layer, a backup or restore, reading a SACL) simply fails with *access denied* (exit `3`) rather than being blocked by the tool. A single `reg` command may legitimately span privilege tiers.

## Addressing model

Almost every subcommand shares one addressing scheme: a **key path** positional, and an optional **value name** positional. Understanding the split is most of understanding `reg`.

### Key paths

The key is a positional path argument. Both `/` and `\` are accepted as separators — neither character is legal inside an LCS key component, so there is no ambiguity, and you may use whichever your shell quotes most comfortably:

```
reg get Machine/System/KMES
reg get 'Machine\System\KMES'      # identical
```

A leading separator is optional and ignored (`/Machine/X` is the same as `Machine/X`). Paths are compared case-insensitively. The first component is the **hive**:

| Path | Meaning |
|---|---|
| `Machine\…` | The machine hive — system-wide configuration. |
| `Users\<SID>\…` | A specific principal's hive. |
| `CurrentUser\…` | A kernel alias, rewritten to the caller's own `Users\<SID>\` at the syscall boundary. |

On **output**, `reg` displays paths with a backslash by default. `--sep=/` (or `REG_SEP=/`) switches display to forward slash for that invocation; this affects display only, never how a path is parsed.

### The value-name positional

The value name is a **separate** positional argument — it is never folded into the key path. This is deliberate: an LCS value name may itself contain `/` or `\` (which a key component may not), so keeping them apart removes all ambiguity.

```
reg get Machine/System/KMES BufferCapacity
#       └──── key path ─────┘ └── value ──┘
```

The presence or absence of the value argument selects what the command targets:

- **No value argument ⇒ the command targets the key itself** — its metadata, its values as a set, its security descriptor, its children.
- **A value argument ⇒ the command targets that one named value.**
- The **default value** (the empty-name value) is addressed by the literal token `@` in the value-name position, mirroring `.reg` convention: `reg get Machine\App @`.

This split applies uniformly to `get`, `set`, `del`, `mask`, and `unmask`.

### Value literals and types

On `set` (and in a batch), the value's data is a single token whose registry type is either **forced** by a `type:` prefix or **inferred** from its shape. The registry value types and their literal syntax:

| Prefix | Registry type | Data syntax |
|---|---|---|
| `sz:` | `REG_SZ` | UTF-8 string (rest of token, verbatim). |
| `expand:` | `REG_EXPAND_SZ` | UTF-8 string, `%VAR%` left unexpanded on disk. |
| `dword:` | `REG_DWORD` | `42` or `0x2A`; must fit `u32`. |
| `dword-be:` | `REG_DWORD_BIG_ENDIAN` | `42` or `0x2A`; must fit `u32`. |
| `qword:` | `REG_QWORD` | `42` or `0x2A`; must fit `u64`. |
| `multi:` | `REG_MULTI_SZ` | Comma-separated; `\,` escapes a literal comma. |
| `hex:` / `bin:` | `REG_BINARY` | Hex bytes; `:`, `-`, and space separators are ignored. |
| `link:` | `REG_LINK` | An absolute key path (a symlink target). |
| `none:` | `REG_NONE` | No data (token must be empty after the prefix). |

When the substring before the first `:` is **not** a recognised keyword (for example `http://host`), the token is not treated as typed and inference applies:

| Token shape | Inferred type |
|---|---|
| all decimal digits, fits `u32` | `REG_DWORD` |
| all decimal digits, fits `u64` but not `u32` | `REG_QWORD` |
| `0x…` hex, ≤ 8 significant hex digits | `REG_DWORD` |
| `0x…` hex, ≤ 16 significant hex digits | `REG_QWORD` |
| anything else (including empty) | `REG_SZ` |

Inference is intentionally broad, so any all-digit token becomes a number — a PIN, a zip code, or `007` becomes a `REG_DWORD` and loses its textual form. **To force a string, prefix it with `sz:`** (`sz:007` stores the string `007`; `sz:dword:42` stores the literal string `dword:42`). Because the coercion is a known trap, `reg set` **always echoes the resolved type on success**, so a surprising conversion is never silent even under `--quiet`.

## Global options

Every subcommand accepts the following. Each behavioural toggle also has an environment-variable equivalent, so it can be set per-invocation (the flag) or once per shell or script (the variable). The flag wins over the variable, which wins over the built-in default.

| Option | Env var | Effect |
|---|---|---|
| `--json` | `REG_JSON=1` | Emit structured JSON instead of human text. `watch` emits JSON-lines. |
| `-v`, `--verbose` | `REG_VERBOSE=1` | More detailed output. |
| `-q`, `--quiet` | — | Suppress non-essential output. (A surprising type coercion is still reported.) |
| `--sep=CHAR` | `REG_SEP` | Path *display* separator: `\` (default) or `/`. |
| `--help` | — | Print help for the command and exit `0`. |
| `--version` | — | Print the version and exit `0`. |

Mutating subcommands additionally accept:

| Option | Env var | Effect |
|---|---|---|
| `--layer NAME` | `REG_LAYER` | Target layer for the write (default: the base layer). |
| `-y`, `--yes` | `REG_ASSUME_YES=1` | Skip the confirmation prompt on a destructive command. |

Destructive commands (`del -r`, `restore`, `layer del`) prompt for confirmation when standard input is a terminal, and auto-proceed when it is not — so interactive use is guarded while scripts are never blocked. Set `-y`/`--yes` or `REG_ASSUME_YES=1` to skip the prompt explicitly.

---

## Reading

### `reg get <key> [value]`

Read the effective (resolved) value. With a `value`, prints that value's data; with no value, lists the key's effective values (the default `@` value first, then named values sorted case-insensitively) — a shorthand for `ls --values-only`.

| Option | Effect |
|---|---|
| `-L`, `--layers` | Annotate the value with the layer it resolved from and its sequence number. |
| `--raw` | Write the value's raw bytes to standard output verbatim — for piping `REG_BINARY` data. |
| `--no-follow` | Operate on a symlink key itself rather than following it to its target. |

The single-value default form prints data bare (no quotes; one element per line for `REG_MULTI_SZ`) so it pipes cleanly. `-L` shows the winning layer's provenance:

```
$ reg get Machine\System\KMES BufferCapacity
4194304

$ reg get Machine\App Theme -L
Theme = REG_SZ "dark"   (layer: policy, seq 7)
```

Only the *winning* value is shown; the full shadowed stack beneath it is not yet exposed by any client ABI (the `-L` flag will render the whole stack unchanged once that primitive lands). See [Layers](/peios/using-peios/registry-layers/layers.md) for what "effective" resolves against.

### `reg ls <key>`

One-level listing of a key's immediate subkeys and values.

| Option | Effect |
|---|---|
| `-l`, `--long` | Long form: for subkeys, child and value counts; for values, type and byte size. |
| `--keys-only` | List subkeys only. |
| `--values-only` | List values only. |

```
$ reg ls Machine\System\KMES -l
BufferCapacity = REG_QWORD 4194304   (8 bytes)
MaxEventSize   = REG_DWORD 65536   (4 bytes)
```

### `reg tree <key>`

Recursively list the subkey tree rooted at `key`.

| Option | Effect |
|---|---|
| `--depth N` | Limit recursion to `N` levels. |
| `--values` | Include each node's values, not just its keys. |

```
$ reg tree Machine\System --depth 2
```

### `reg info <key>`

Print a key's metadata: leaf name, subkey and value counts, last-write time, hive generation, security-descriptor size, and the volatile and symlink flags.

| Option | Effect |
|---|---|
| `--no-follow` | Inspect a symlink key itself rather than its target. |

```
$ reg info Machine\System\KMES
path             Machine\System\KMES
name             KMES
subkeys          0
values           4
last_write_ns    1717243800000000000
hive_generation  42
sd_size          164
volatile         false
symlink          false
```

---

## Writing

### `reg set <key> <value> <data>`

Create or update a value. The `value` name (or `@`) and the `data` token are both required. Writes are tagged with `--layer` (default: base). See [value literals](#value-literals-and-types) for the `data` syntax.

| Option | Effect |
|---|---|
| `--layer NAME` | Write into layer `NAME` instead of the base layer. |
| `-p`, `--parents` | Create any missing ancestor keys. |
| `--expected-seq N` | Compare-and-swap guard: apply only if the value's current sequence is `N`; a mismatch exits `6` without writing. |

```
$ reg set Machine\App Build 4096
set Machine\App Build = REG_DWORD 4096   (layer: base)

$ reg set Machine\App Servers multi:alpha,beta --layer policy
```

`--expected-seq` supports lock-free read-modify-write: read the value with `-L` to learn its sequence, then write back with `--expected-seq` set to that number; if another writer changed it in between, your write fails cleanly instead of clobbering theirs.

### `reg new <key>`

Create a key with no values. Reports whether the key was created or already existed.

| Option | Effect |
|---|---|
| `--layer NAME` | Create the key in layer `NAME`. |
| `-p`, `--parents` | Create any missing ancestor keys. |
| `--volatile` | Create a volatile (RAM-only) key that does not survive a reboot. |

```
$ reg new Machine\App\Cache --volatile
created Machine\App\Cache   (layer: base)
```

### `reg del <key> [value]`

Delete a value, or a key. (Alias: `delete`.)

With a `value`, deletes **this layer's** entry for that value only — lower-layer entries resurface, because a per-layer delete is not a tombstone (use `mask` for that). With no value, deletes the key, which must be empty unless `-r` is given.

| Option | Effect |
|---|---|
| `--layer NAME` | Delete from layer `NAME`. |
| `-r`, `--recursive` | Delete the key and all its descendants (walks and removes children). |
| `-y`, `--yes` | Skip the confirmation prompt (recursive deletes prompt on a terminal). |

```
$ reg del Machine\App Legacy
deleted value Legacy from Machine\App

$ reg del Machine\App\Temp -r
Recursively delete key Machine\App\Temp and all its contents? [y/N] y
deleted Machine\App\Temp (3 keys)
```

See [Deleting keys and values](/peios/using-peios/registry-layers/deleting-keys-and-values.md) for how a per-layer delete differs from a tombstone.

---

## Masking and hiding

These commands expose LCS's tombstone and hide primitives, which *mask* lower-precedence state rather than removing a layer's own entry — the distinction that makes [layers](/peios/using-peios/registry-layers/layers.md) revertible. The layer is chosen with `--layer` (default: base).

### `reg mask <key> [value]` / `reg unmask <key> [value]`

`mask` sets a **tombstone**: a per-value marker in the target layer that hides that value from all layers below it. `unmask` clears it.

| Option | Effect |
|---|---|
| `--layer NAME` | The layer that carries the tombstone. |
| `--all` | Blanket tombstone: mask *all* lower-layer values on the key (no `value` argument). |

A `value` is required unless `--all` is given.

```
$ reg mask Machine\App ApiKey --layer policy
set tombstone on Machine\App ApiKey (layer: policy)

$ reg mask Machine\App --all --layer policy
set blanket tombstone on Machine\App (layer: policy)
```

### `reg hide <key>` / `reg unhide <key>`

`hide` installs a HIDDEN path entry in the target layer, masking the key's existence in that layer and below. `unhide` removes that layer's path entry, letting the key reappear.

| Option | Effect |
|---|---|
| `--layer NAME` | The layer that hides (or unhides) the key. |

```
$ reg hide Machine\App\Debug --layer policy
hid Machine\App\Debug (layer: policy)
```

---

## Layers

### `reg layer ls`

List layers with name, precedence, enabled state, and (informational) owner SID, ordered by descending precedence.

| Option | Effect |
|---|---|
| `-l`, `--long` | Long form. |

```
$ reg layer ls
policy                   prec=100    enabled   owner=S-1-5-32-544
base                     prec=0      enabled
```

### `reg layer new <name>`

Create a layer by writing its metadata key under `Machine\System\Registry\Layers\`.

| Option | Effect |
|---|---|
| `--precedence N` | Precedence (higher wins). A precedence above `0` requires `SeTcbPrivilege`; the tool attempts it and reports any denial. |
| `--owner SID` | Record an informational owner SID. |
| `--disabled` | Create the layer disabled. |

```
$ reg layer new policy --precedence 100 --owner S-1-5-32-544
created layer policy (precedence 100, enabled)
```

### `reg layer set <name>`

Modify an existing layer's metadata.

| Option | Effect |
|---|---|
| `--precedence N` | Change the precedence. |
| `--enable` / `--disable` | Enable or disable the layer. |
| `--owner SID` | Change the informational owner SID. |

### `reg layer del <name>`

Delete a layer — removes its metadata key; LCS tears down the layer's entries. Prompts for confirmation on a terminal.

```
$ reg layer del policy
Delete layer policy and all its entries? [y/N] y
deleted layer policy
```

Managing per-process **private** layer views is out of scope for `reg`; see [private hives and layers](/peios/using-peios/registry-advanced/private-hives-and-layers.md).

---

## Security descriptors

### `reg sd <key>`

Show or set a key's [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) as SDDL, using the same codec as the [`sd`](/peios/security-fundamentals/security-descriptors/overview.md) tool, so its output is consistent. With no scope flags, the default is owner + group + DACL (a SACL requires the privileged `ACCESS_SYSTEM_SECURITY` right).

| Option | Effect |
|---|---|
| `--set SDDL` | Apply the given SDDL. Without it, `sd` prints the current descriptor. |
| `--owner` | Scope to the owner. |
| `--group` | Scope to the group. |
| `--dacl` | Scope to the DACL. |
| `--sacl` | Scope to the SACL (needs `ACCESS_SYSTEM_SECURITY`). |

```
$ reg sd Machine\App
O:BAG:BAD:(A;;KA;;;BA)(A;;KR;;;WD)

$ reg sd Machine\App --set 'O:BAG:BAD:(A;;KA;;;BA)' --owner --dacl
set security descriptor on Machine\App
```

A security-descriptor change takes effect on future opens only (existing handles keep the access mask they were granted). **Security changes are not layered and are not undone by layer removal** — they mutate the key object directly. See [Access control on keys](/peios/using-peios/registry-security/access-control.md).

---

## Symlinks

### `reg link <key> <target>`

Create a symlink key pointing at the absolute `target` key path. This creates a key with the immutable symlink flag and a default `REG_LINK` value holding the target. Requires `KEY_CREATE_LINK` and `SeTcbPrivilege`/Administrator; the tool attempts it and reports any denial.

| Option | Effect |
|---|---|
| `--layer NAME` | Create the link key in layer `NAME`. |

```
$ reg link Machine\App\CurrentConfig Machine\App\Config\v3
linked Machine\App\CurrentConfig -> Machine\App\Config\v3 (layer: base)
```

`get` and `info` follow symlinks to their target by default; pass `--no-follow` to inspect the link key itself. See [Advanced: registry links](/peios/using-peios/registry-advanced/registry-links.md).

---

## Watching

### `reg watch <key>`

Arm a change watch and stream one event per change until interrupted (or until `--count` events). Each event faithfully means "something under the filter changed — re-read"; `reg` does not decode the change records' contents. With `--json`, emits one JSON object per line.

| Option | Effect |
|---|---|
| `--subtree` | Watch descendant keys too, not just this key. |
| `--filter LIST` | Comma-separated subset of `value`, `subkey`, `sd` (default: all). |
| `--count N` | Exit after `N` events. |

```
$ reg watch Machine\System\KMES --subtree --filter value
changed: Machine\System\KMES
changed: Machine\System\KMES
```

See [Watching for changes](/peios/using-peios/registry-concepts/watches.md) for why a service watches instead of polling.

---

## Batch, export, backup, and restore

Two paths move a subtree around: `export`/`apply` are the *portable, reviewable* path (a diffable text or JSON document); `backup`/`restore` are the *exact, privileged* path (an opaque kernel snapshot).

### `reg apply <file>`

Apply a batch of operations to one hive in a single [transaction](/peios/using-peios/registry-advanced/transactions.md) — all-or-nothing. All operations must target one hive (a cross-hive batch fails with exit `4`). `-` reads standard input.

| Option | Effect |
|---|---|
| `--dir DIR` | Apply every batch file in `DIR` (sorted), each as its own transaction. A missing or empty directory applies nothing and succeeds. |
| `--once-delete` | Delete each batch file after it applies successfully (a drain). |
| `-y`, `--yes` | Skip confirmation. |

The batch format is JSON (canonical and exact) or a line-oriented text format; `apply` auto-detects (a leading `{` or `[` means JSON). **Text-format input is not yet implemented** — supply JSON, or generate one with `reg export --json`. Text *export* works for review.

```
$ reg export --json Machine\App - | reg apply -
applied 4 keys
```

### `reg export <key> [file]`

Dump a subtree to the batch format. Omit `file` (or pass `-`) to write to standard output. Human-readable text by default; `--json` emits the canonical exact form.

| Option | Effect |
|---|---|
| `--layer NAME` | Export one layer's view (default: the effective state). |

```
$ reg export Machine\App app-config.reg
```

### `reg backup <key> <file>`

Write an opaque, exact, binary kernel snapshot of a key and its subtree to `file`. Requires `SeBackupPrivilege`.

```
$ reg backup Machine\System\KMES kmes.snap
backed up Machine\System\KMES -> kmes.snap
```

### `reg restore <key> <file>`

Replace a key and its entire subtree from a snapshot, in one transaction. Requires `SeRestorePrivilege`. Prompts for confirmation on a terminal.

```
$ reg restore Machine\System\KMES kmes.snap
Replace key Machine\System\KMES and its entire subtree from kmes.snap? [y/N] y
restored Machine\System\KMES from kmes.snap
```

See [Backup and restore](/peios/using-peios/registry-administration/backup-and-restore.md) for when to reach for each path.

---

## Output formats

Default output is terse, human-readable, and grep-friendly. `--json` switches every command to structured output — a single self-contained JSON document, except `watch`, which emits JSON-lines (one object per event). In any value listing, the default (`@`) value is emitted first, then named values sorted case-insensitively.

Type formatting on read:

| Registry type | Human form | JSON form |
|---|---|---|
| `REG_SZ` / `REG_EXPAND_SZ` | the string | `{"type":"sz","data":"…"}` |
| `REG_DWORD` / `REG_QWORD` | decimal | `{"type":"dword","data":42}` |
| `REG_MULTI_SZ` | one element per line | `{"type":"multi","data":["a","b"]}` |
| `REG_BINARY` | hex | `{"type":"binary","data":"deadbeef"}` |
| `REG_LINK` | the target path | `{"type":"link","data":"…"}` |

## Exit status

| Code | Meaning | Typical cause |
|---|---|---|
| `0` | Success. | — |
| `1` | Usage error. | Bad arguments; a required option missing. |
| `2` | Key or value not found. | `ENOENT`. |
| `3` | Access denied. | `EACCES`, `EPERM` — the kernel refused the operation. |
| `4` | Invalid specification. | Bad path, type, or literal; a cross-hive batch; a name too long (`EINVAL`, `EXDEV`, `ENAMETOOLONG`). |
| `5` | Syscall or source failure. | `EIO`, `ETIMEDOUT`, `ENOSPC`, `ENOMEM`. |
| `6` | Compare-and-swap conflict. | `EAGAIN` — an `--expected-seq` guard did not match; the value changed under you. |

A denial (`3`) reports the operation, the target, and, where relevant, the access that was required — in the style of the `sd` and `token` tools.

## See also

- [`regman`](/peios/using-peios/registry-administration/regman.md) — the registry *manual*: what a key or value *means*, as opposed to what it is set to. Consult it before you `reg set`.
- [Keys, values, and types](/peios/using-peios/registry-concepts/keys-values-and-types.md) — the data model `reg` addresses.
- [Layers](/peios/using-peios/registry-layers/layers.md) — what "effective" resolves against, and what `--layer`, `mask`, and `hide` operate on.
- [Access control on keys](/peios/using-peios/registry-security/access-control.md) — the check every `reg` operation goes through.

---

# regman

_Peios / Using Peios / Peiosutils / Registry tools_

> regman documents what a registry key or value means — type, default, valid range, and when a change applies — from shipped docs, never the live registry.

[Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md) established that the registry holds values but not their meaning: it keeps a type tag and some bytes, and never knows that `BufferCapacity` must be a power of two or what it is for. That knowledge has to live *somewhere* a person can look it up. It lives in **`regman`**, the registry's manual.

`regman` is the `man` of the registry. Give it a path and it tells you what a key or value actually is. It is the everyday tool for configuring a Peios system — before you touch a knob, `regman` is how you find out what it does and what changing it will cost.

## regman, in one sentence

**`regman <path> [value]` documents what a registry key or value *means* — its type, default, valid values, when a change takes effect, and a prose description — drawn from shipped documentation, never from the live registry.**

That last clause is the one to hold onto; there is a section on it below.

## Looking up a value

Give `regman` a key path and a value name and it prints a **knob-card** — an identity line, an aligned block of facts, and a description:

```
$ regman Machine\System\KMES BufferCapacity

Machine\System\KMES BufferCapacity                    documented by kmes

  Type     REG_QWORD
  Default  4194304  (4 MB)
  Valid    65536–268435456 bytes (64 KB–256 MB), power of two
  Applies  live — ring-buffer swap

Per-CPU ring buffer capacity, in bytes. Must be a power of two; values
that are not are treated as invalid and ignored.
```

Every registry value is a *knob*, and every knob has the same few facts worth knowing, so the card is uniform rather than free-form:

| Field | Tells you |
|---|---|
| **Type** | The value's registry type (`REG_QWORD`, `REG_SZ`, …). |
| **Default** | The value used when nothing is set. |
| **Valid** | The range, set, or constraint a sensible value must satisfy. |
| **Applies** | When a change takes effect — `live`, on `restart`, or on `reboot`. |

The **Applies** field is the one operators reach for most: it is the difference between "edit it and you're done" and "edit it and schedule a reboot". Only the fields that make sense for a given item are shown, and a knob that is being retired carries a prominent deprecation notice at the top of its card.

## Looking up a key

Give `regman` just a key — no value — and it does double duty as an *index*: the key's own description, then a list of every value documented under it, one summary line each.

```
$ regman Machine\System\KMES

Machine\System\KMES                                   documented by kmes

The KMES event subsystem reads its tuning parameters from the values
under this key...

Values
  BufferCapacity         Per-CPU ring buffer capacity in bytes.
  MaxEventSize           Max total size of a userspace-emitted event.
  MaxNestingDepth        Max nesting depth for userspace event payloads.
  MaxEmitRatePerProcess  Max events per second a single process may emit.
```

So you can start at a subtree, see what is configurable there, and drill into any single value.

## Searching, when you do not know the path

If you don't know where a setting lives, `regman -k` searches names and summaries — the registry's `apropos`:

```
$ regman -k buffer
Machine\System\KMES BufferCapacity   Per-CPU ring buffer capacity in bytes.
```

## regman documents intent, not current state

This is the boundary that matters most. **`regman` tells you what a setting *should be* — what it means and which values are legal — not what it is currently set to on this machine.** It reads shipped documentation, never the live registry; it does not talk to [LCS](/peios/using-peios/registry-administration/lcs-and-sources.md) at all. Ask `regman` about `BufferCapacity` and you learn it must be a power of two and defaults to 4 MB; you do *not* learn that this particular box has it set to 8 MB right now. Reading the current value is a separate, live query against the registry — that is [`reg`](/peios/using-peios/registry-administration/reg.md)'s job. `regman` tells you a knob *should* be a power of two; `reg get` tells you what it is set to, and `reg set` changes it. Reach for `regman` to decide what to write, and `reg` to write it.

Put `regman` next to the other two things from [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md) and a clean division of labour appears:

| To learn… | Look at… |
|---|---|
| What a setting *means*, and what is valid | `regman` — the manual |
| What the value is *set to* | [`reg`](/peios/using-peios/registry-administration/reg.md) — a live query against the store |
| What the subsystem is *actually running on* | the [event log](/peios/security-fundamentals/auditing/overview.md) |

`regman` is the one you consult first, and the only one that works before you have changed anything — because documentation ships with the software, not with the machine's state. It is the natural complement to [reject-or-keep](/peios/using-peios/registry-concepts/configuration-and-meaning.md): `regman` tells you the valid range up front, so you set a good value the first time instead of writing one the owning subsystem will quietly refuse.

## Where the documentation comes from

`regman` has no built-in database of every setting. Each package **ships its own documentation** as files dropped into a well-known directory (`/usr/share/regman/`), one fragment per package documenting that package's whole configuration surface. Install a package and its registry documentation appears; remove the package and it goes away. The package that *owns* a set of keys is the package that documents them — the only arrangement that stays correct as the system changes.

A consequence worth knowing: `regman` can only describe settings that some installed package has documented. An undocumented key is invisible to it — `regman` is exactly as complete as the packages on the machine make it. If two packages happen to document the same item, `regman` shows both and flags the overlap rather than silently picking a winner.

(For the people who *write* that documentation, `regman fmt` and `regman lint` prepare and check fragments, and `regman index` keeps lookups fast on large corpora — the lookup is always correct without an index, which is purely an accelerator. These are packaging concerns rather than everyday operator ones.)

## Where to go next

For the idea `regman` exists to serve — why the registry holds values without holding their meaning, and what reject-or-keep means — read [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md).

For the other half of the picture — reading what a value is actually *set to*, which is a live query against the store rather than a documentation lookup — read [LCS and sources](/peios/using-peios/registry-administration/lcs-and-sources.md).

---

# 1.1 What an Event Is

_Peios / Using Peios / Events / Introduction_

> What counts as an event on a Peios system, the two transports that carry them, and what this book deliberately leaves out.

An **event** is a record that something happened: an access was checked,
a service started, a package was installed, a descriptor was found
corrupt. Events are produced by the component that observed the thing,
and consumed by whatever is watching — usually `eventd`, sometimes a
tool reading the stream directly.

This book enumerates every event Peios emits, with the fields each one
carries. It is a lookup, not an explanation. Where an event's *meaning*
needs the mechanism behind it, the chapter links to the manual that
describes that mechanism.

## 1.1.1 Two transports, not one

Most events travel through **KMES**, the kernel message event stream: a
per-CPU ring buffer that the kernel writes into and userspace reads
from. Every KMES event is a binary header followed by a msgpack
payload. Chapters 3 to 7 document KMES events.

Two things in this book are not KMES events, and are included because an
operator looking for "what does Peios tell me" would otherwise miss
them:

- **eventd's synthetic events** (§8) are written straight into a shard
  database and never touch KMES. They carry no header stamps.
- **LCS watch records** (§5.4) are binary records read from a key file
  descriptor. They are a notification mechanism, not an audit trail, and
  their format has nothing in common with a KMES event.

## 1.1.2 What is not here

This book does not cover **logs** or **metrics**. Both reach eventd by a
different path, are stored in different tables, and are not events. The
eventd manual covers them.

Nor does it cover the query language for reading events back. That is
PSPU §3, with the operator-facing view in the eventd manual.

---

# 1.2 The Envelope

_Peios / Using Peios / Events / Introduction_

> The packed binary header in front of every KMES payload — the fields the kernel stamps, the layout, and what ordering you get.

Every KMES event is a packed binary header followed immediately by its
msgpack payload, delivered as one contiguous byte sequence with no
padding anywhere.

The emitter supplies only two things: the **event type string** and the
**payload**. Everything else in the header is stamped by KMES itself,
which is what makes the identity fields trustworthy — an emitter cannot
forge them.

## 1.2.1 Fields KMES stamps

| Field | Meaning |
|---|---|
| `timestamp` | Wall clock at the moment KMES accepted the event, nanoseconds since the Unix epoch. |
| `sequence` | The emitting CPU's per-boot counter. The first event on each CPU gets 1. |
| `cpu_id` | The CPU whose ring buffer holds the event. |
| `origin_class` | 0 for syscall emission, unconditionally. For kernel emission, the value the calling subsystem passed. |
| identity GUIDs | The effective, true and process token GUIDs of the task that caused the event. |

The identity stamps matter for reading this book: **several events carry
no caller in their payload at all**, because the envelope already
names one. StrataFS copy-up records are the clearest case — nothing in
the payload names a token, and the caller is recovered from the header.

`event_size`, `header_size` and `type_len` are structural, computed by
KMES during construction.

## 1.2.2 Layout

All fields before the event type string sit at fixed offsets. The type
string begins at offset 77, with its `u16` length at offset 75, so the
header is exactly `77 + type_len` bytes. The payload runs from
`header_size` to `event_size`, and the next event begins at
`event_size` from the start of the current one.

All multi-byte header integers are little-endian. The identity GUIDs are
opaque 16-byte values.

The full field-by-field layout is normative in PSPK §2, the KMES event
stream specification. This summary is enough to walk a stream; it is not
enough to implement one.

## 1.2.3 Ordering

`timestamp` is captured before `sequence` is assigned, so two events
with the same timestamp on the same CPU are ordered by sequence.

Across CPUs there is no global order. Two events on different CPUs with
close timestamps may have been observed in either order.

---

# 1.3 Encoding Conventions

_Peios / Using Peios / Events / Introduction_

> Every payload is a msgpack map with string keys — how the recurring value types are represented, and the three event-type naming styles.

Every KMES payload in this book is a **msgpack map with UTF-8 string
keys**. The key set is stable per event type.

## 1.3.1 Value representations

The same conceptual types appear across many events and are always
encoded the same way.

| Conceptual type | msgpack representation |
|---|---|
| SID | **bin** holding the binary SID, 8–68 bytes. |
| GUID | **bin**, exactly 16 bytes. |
| ACE | **bin** holding the binary ACE, copied from the descriptor. |
| Access mask | **uint**, 32-bit. |
| Boolean | **bool**. |
| Privilege name | **string**, UTF-8, e.g. `SeBackupPrivilege`. |
| Process ID | **uint**. |
| Path or name | **string**, UTF-8. |
| Timestamp | **uint**, unless an event's schema says otherwise. |
| Object context | **bin** or **nil**. An opaque caller-supplied blob; its contents are service-specific. |

Binary SIDs, GUIDs and ACEs are carried as bytes rather than as text
because they are compared as bytes. A textual SID would have to be
parsed back before it could be matched.

## 1.3.2 Event type strings use three different styles

There is no single convention. What an event type looks like depends on
which component emits it:

| Style | Emitters | Examples |
|---|---|---|
| `kebab-case` | KACS | `access-audit`, `logon-session-destroyed` |
| `dotted.snake` | peinit, peipkg, eventd | `job.created`, `peipkg.repo-add`, `synthetic.config_change` |
| `SCREAMING_SNAKE` | StrataFS, LCS | `STRATAFS_COPY_UP`, `LCS_BACKUP_START` |

The dotted family is not internally consistent either: `peipkg.repo-add`
is dotted-then-kebab while `synthetic.config_change` is
dotted-then-snake.

This is recorded because a consumer matching event types has to know it,
not because it is defended. Match the exact strings in this book rather
than deriving one from a pattern.

---

# 1.4 Reading an Event

_Peios / Using Peios / Events / Introduction_

> The four rules every consumer of this stream lives by — ignore unknown keys, do not read meaning into absence, expect loss, and do not trust the contents.

Four rules govern every consumer of this stream.

## 1.4.1 Ignore unknown keys

Future versions may add fields to an event without changing the existing
ones. A consumer that processes the keys it knows and ignores the rest
keeps working across upgrades. A consumer that rejects unrecognised keys
breaks on the first addition.

## 1.4.2 Do not rely on a key being absent

A field that is optional today may become always-present later. Absence
is not a signal.

## 1.4.3 Delivery is best-effort

KMES is a ring buffer. The kernel writes; keeping up is the subscriber's
problem.

- A subscriber that falls behind **loses events**. eventd notices and
  records a `synthetic.gap` (§8.1), which is how a gap becomes visible
  rather than silent.
- **There is no replay.** An event missed is gone. Nothing can ask for
  it back.
- **Buffers are per-subscriber.** One slow reader does not affect
  another.
- **Order is per-subscriber**, not global.

For durable audit, read events from eventd's stores rather than from
KMES directly. eventd drains its subscription continuously and persists
what it reads; from that point the store is the record, not the ring.

## 1.4.4 Events are not authenticated

Events are trusted because they came from the kernel through KMES, not
because they are signed. Nothing in an event carries a signature.

Cryptographic non-repudiation is a userspace concern applied after
events leave the kernel. If a deployment needs it, it is added on the
far side of eventd, not here.

## 1.4.5 Versioning

Event types are not versioned by a field. The schemas in this book are
stable: fields may be added, but an existing field will not be renamed,
retyped or removed under the same type string.

A change that would break compatibility changes the **type string**
instead — `access-audit` would become `access-audit-v2` — so an existing
consumer keeps receiving the shape it understands and simply never sees
the new one. No type in this book has been versioned that way.

---

# 2.1 The Subject Record

_Peios / Using Peios / Events / Common Records_

> The subject map identifying the effective token behind an operation — its fields, the two parallel arrays, and what it deliberately omits.

The `subject` map identifies the **effective token** under which an
operation ran. It appears in every KACS event except
`logon-session-destroyed`.

For an event fired from an impersonating thread, the subject is the
impersonation token, not the primary. For a non-impersonating thread the
primary token *is* the effective one.

For `continuous-audit` (§3.2) the subject is the effective token **at
the moment of the operation**, not at the moment the handle was opened.
A process whose token changed since the open gets the current subject on
each subsequent operation.

## 2.1.1 Fields

| Key | Type | Meaning |
|---|---|---|
| `user_sid` | bin | The token's user SID. |
| `group_sids` | array of bin | The token's group SIDs. |
| `group_attributes` | array of uint | Per-group attribute bitmasks, parallel to `group_sids`. |
| `integrity_level` | uint | The token's integrity RID — 0, 4096, 8192, 12288 or 16384. |
| `pip_type` | uint | The calling process's PIP type. 0 None, 512 Protected, 1024 Isolated. |
| `pip_trust` | uint | The calling process's PIP trust level. |
| `auth_id` | uint | The LUID of the logon session the token belongs to. |
| `token_id` | uint | The token's own LUID. |
| `impersonation_level` | uint | 0–3. A primary token reports 0. |
| `projected_uid` | uint | The Linux UID projection, for correlating with Linux-side audit data. |

Every field is always present.

`auth_id` is the join key to `logon-session-destroyed` (§3.5) and to
`/sys/kernel/security/kacs/sessions`. `token_id` correlates events from
one specific token.

## 2.1.2 The two parallel arrays

`group_sids[i]` and `group_attributes[i]` describe the same group entry,
and the arrays are always the same length.

| Flag | Value | Meaning |
|---|---|---|
| `SE_GROUP_MANDATORY` | 0x01 | Cannot be disabled. |
| `SE_GROUP_ENABLED_BY_DEFAULT` | 0x02 | Enabled at creation. |
| `SE_GROUP_ENABLED` | 0x04 | Currently enabled. |
| `SE_GROUP_OWNER` | 0x08 | May act as owner for new objects. |
| `SE_GROUP_USE_FOR_DENY_ONLY` | 0x10 | Matches deny ACEs only. |
| `SE_GROUP_INTEGRITY` | 0x20 | Identifies an integrity SID. Present for ABI parity; MIC reads the token's `integrity_level` field, not this flag. |
| `SE_GROUP_INTEGRITY_ENABLED` | 0x40 | Used with `SE_GROUP_INTEGRITY`. |
| `SE_GROUP_RESOURCE` | 0x20000000 | A domain-local group from a resource domain. Metadata only. |
| `SE_GROUP_LOGON_ID` | 0xC0000000 | The logon SID. Cannot be disabled. |

These are MS-DTYP's names, which PCDS uses. The headers declare the same
flags as `KACS_SID_GROUP_*`; the Peios Kernel TRM §3.A maps the two.

Reconstructing group membership from an event means applying the same
rule the access check applies: `SE_GROUP_ENABLED` set and
`SE_GROUP_USE_FOR_DENY_ONLY` clear, for allow-side matching.

## 2.1.3 What the subject deliberately omits

Privileges, claims, the restricted-SID list, confinement state, and the
default DACL are all absent. Each is unbounded, and an event that
embedded them could grow without limit.

Code needing full token state queries the token directly with
`KACS_IOC_QUERY`, while it still exists. The subject record is for
correlation, not for reconstruction.

---

# 2.2 The Process Record

_Peios / Using Peios / Events / Common Records_

> The process map identifying where an event came from, what to correlate on, and why there is no thread ID.

The `process` map identifies the process the event came from. It appears
in every KACS event except `logon-session-destroyed`, which has no
causing process.

| Key | Type | Meaning |
|---|---|---|
| `pid` | uint | The process ID. |
| `name` | string | The kernel's name for the process, typically the executable's basename. |
| `executable_path` | string | The path resolved at exec, with symlinks already followed. |

Every field is always present. For `continuous-audit` this is the
operation-time process, not the one that opened the handle.

## 2.2.1 Correlating on it

`pid` is reliable only in the short term. Process IDs are reused, so a
`pid` in a week-old record may name something unrelated. For durable
records, correlate on `name` and `executable_path`.

`name` is the kernel's internal name. It is not `argv[0]`, and a process
that rewrote its argv is unaffected here.

## 2.2.2 No thread ID

The record identifies a process, not a thread. Events that fire on one
specific thread still report only the process. Nothing in the current
event set carries a `tid`.

---

# 2.3 The Caller Summary

_Peios / Using Peios / Events / Common Records_

> The registry's own identity submap, why LCS carries it instead of the subject record, and how the two compare field by field.

LCS uses its own identity submap, `caller`, rather than the subject
record. Six of its seven audit events carry it.

It exists separately because LCS's events are emitted from a different
subsystem with a different bound on what it will serialise. The two
records overlap but are not interchangeable, and a consumer handling
both needs to read each on its own terms.

| Key | Meaning |
|---|---|
| `effective_token_guid` | The token the operation ran under. |
| `true_token_guid` | The underlying token, where impersonation is in play. |
| `process_guid` | The calling process. |
| `user_sid` | The effective token's user SID. |
| `authentication_id` | The logon session LUID. |
| `token_id` | The token's own LUID. |
| `token_type` | Primary or impersonation. |
| `impersonation_level` | 0 for a primary token. |
| `integrity_level` | The token's integrity RID. |

Nine fields, and no more. Group lists, privilege arrays, claims and
default DACLs are unbounded and are never included — the same reasoning
that shapes the subject record (§2.1), applied independently.

## 2.3.1 Against the subject record

| | `subject` | `caller` |
|---|---|---|
| Emitted by | KACS | LCS |
| Identity by | SIDs | GUIDs, plus `user_sid` |
| Groups | `group_sids` with attributes | absent |
| PIP state | `pip_type`, `pip_trust` | absent |
| Linux projection | `projected_uid` | absent |
| Session join key | `auth_id` | `authentication_id` |

Note the session join key is spelled differently in each. Correlating a
KACS event with an LCS event on the same logon session means matching
`subject.auth_id` against `caller.authentication_id`.

---

# 3.1 access-audit

_Peios / Using Peios / Events / Kernel Access Events_

> The most common event in the system — what triggers it, why one access can produce several, and a worked example.

The most common event in the system. Fires at AccessCheck completion,
from the SACL audit walk, and from a token's `audit_policy` forcing an
audit that no ACE asked for.

Event type string: `access-audit`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1). |
| `object_context` | bin or nil | Caller-supplied opaque identifier for the object. `nil` if AccessCheck was not given one. |
| `requested_access` | uint | The mask the caller requested, after generic mapping. |
| `granted_access` | uint | The mask actually granted. |
| `success` | bool | True when every requested bit is in `granted_access`. |
| `trigger` | map | Why this event fired. Below. |
| `process` | map | Process record (§2.2). |

Every field is always present.

## 3.1.1 The trigger record

| Key | Type | Meaning |
|---|---|---|
| `kind` | string | `sacl` or `policy`. |
| `ace` | bin or nil | For `kind = sacl`, the matched ACE's bytes. For `kind = policy`, `nil`. |

`kind = sacl` means an audit ACE in the object's SACL — or in a central
access policy's SACL — matched this access, and `ace` carries the exact
ACE so a consumer can identify which rule fired.

`kind = policy` means nothing in any SACL asked for this. The audit
fired because the calling token's `audit_policy` carries
`OBJECT_ACCESS_SUCCESS` or `OBJECT_ACCESS_FAILURE`, which forces an
audit on every access that token makes.

The distinction matters when reading volume. A flood of `kind = policy`
events is a property of the token, and is fixed by changing the token's
policy. A flood of `kind = sacl` events is a property of the object, and
is fixed by changing its SACL.

## 3.1.2 One access can produce several events

The event is per matching audit ACE, not per access. An access that
matches three audit ACEs produces three events, each with a different
`ace` in its trigger and otherwise identical.

## 3.1.3 Example

A successful read where a SACL audit ACE matched:

```
{
  "event_type": "access-audit",
  "event_time": <timestamp>,
  "subject": { ... },
  "object_context": <bin>,
  "requested_access": 0x00120089,
  "granted_access":   0x00120089,
  "success": true,
  "trigger": { "kind": "sacl", "ace": <bin> },
  "process": { ... }
}
```

`0x00120089` is `GENERIC_READ` after mapping to file-specific bits.
Generic bits never survive into an event; the mask is always specific.

---

# 3.2 continuous-audit

_Peios / Using Peios / Events / Kernel Access Events_

> Per-operation auditing on an already-open handle — the three masks involved, and why the subject is re-read each time.

Fires per operation on an already-open handle, when the operation's
required access overlaps a continuous audit mask cached on that handle.

Where `access-audit` records the decision to open something,
`continuous-audit` records what was then done with it. The mask is
configured by `SYSTEM_ALARM*` ACEs at the access check that opened the
handle, and the event is fired afterwards by whatever kernel subsystem
enforces the operation — FACS for file handles.

Event type string: `continuous-audit`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1), reflecting the **operation-time** effective token. |
| `object_context` | bin or nil | Object identifier. May differ from the open-time context if the enforcement point keeps its own. |
| `operation` | string | The operation name. FACS uses a `file.` prefix — `file.read`, `file.write`, `file.fallocate`. Other enforcement points use their own. |
| `requested_access` | uint | The mask this specific operation needs. |
| `matched_access` | uint | The subset of `requested_access` that overlapped the handle's continuous audit mask. |
| `granted_access` | uint | The mask cached on the handle at open time. |
| `success` | bool | Whether the operation itself succeeded. |
| `process` | map | Process record (§2.2), operation-time. |

Every field is always present.

## 3.2.1 The three masks

They are easy to confuse, and reading them together is the point of the
event.

- **`requested_access`** is what the *operation* needs. A read needs
  `FILE_READ_DATA`; a write needs `FILE_WRITE_DATA`; an `mmap` with
  `PROT_EXEC` needs `FILE_EXECUTE`.
- **`matched_access`** is `requested_access` intersected with the
  handle's continuous audit mask. This is why the event exists — the
  bits that triggered it.
- **`granted_access`** is what the handle was opened with. An operation
  can only succeed if `requested_access` is a subset of it.

Read as a sentence: *the operation needed these bits, of which these
triggered audit, against a handle opened with these, and it
succeeded or did not.*

## 3.2.2 Why the subject is re-read

A handle outlives the token that opened it. A process that changes its
effective token — starting or stopping impersonation — keeps its
handles, and every subsequent operation is audited under the token
current at that moment.

An investigation that assumes the open-time identity for later
operations will attribute them to the wrong principal.

## 3.2.3 Example

A read on a file carrying an alarm ACE on `FILE_READ_DATA`:

```
{
  "event_type": "continuous-audit",
  "event_time": <timestamp>,
  "subject": { ... },
  "object_context": <bin>,
  "operation": "file.read",
  "requested_access": 0x00000001,
  "matched_access":   0x00000001,
  "granted_access":   0x00120089,
  "success": true,
  "process": { ... }
}
```

---

# 3.3 privilege-use

_Peios / Using Peios / Events / Kernel Access Events_

> Recording a privilege that contributed bits to the granted mask — the five that can appear, and what success actually means here.

Fires at AccessCheck for a privilege that contributed bits to the
granted mask, when the token's `audit_policy` asks for it.

Event type string: `privilege-use`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1). |
| `object_context` | bin or nil | Object identifier from the access check. |
| `privilege` | string | The canonical privilege name. |
| `requested_access` | uint | The bits the caller requested that this privilege might address. |
| `granted_access` | uint | The bits the privilege contributed, before later narrowing. |
| `surviving_access` | uint | The subset of `granted_access` that reached the final granted mask. |
| `success` | bool | True when `surviving_access` is non-empty. |
| `process` | map | Process record (§2.2). |

Every field is always present.

## 3.3.1 Only five privileges can appear

The `privilege` field carries a canonical name, and **only five are
representable**:

- `SeSecurityPrivilege`
- `SeTakeOwnershipPrivilege`
- `SeBackupPrivilege`
- `SeRestorePrivilege`
- `SeRelabelPrivilege`

Any other bit fails the encoder closed rather than emitting an unnamed
privilege. This is consistent with those being the only five that can
influence an access check at all, and therefore the only five that can
produce this event.

A consumer will never see a sixth name here. A privilege used for
something other than an access decision produces no `privilege-use`
event.

## 3.3.2 Success means it worked, not that it fired

This is the field most often misread.

`success = true` — the privilege contributed bits and they survived to
the final grant. The privilege did useful work. Governed by the
`PRIVILEGE_USE_SUCCESS` audit policy.

`success = false` — the privilege contributed bits and a later layer
stripped them. The caller was in a confinement that does not permit it,
or a CAAP rule narrowed it out, or the caller was non-dominant under
PIP. Governed by `PRIVILEGE_USE_FAILURE`.

A `success = false` event is not a failed attempt to *use* a privilege.
It is a privilege that fired and was then overridden — which is usually
the more interesting record of the two, because it shows a boundary
doing its job.

A token can carry either policy bit, both, or neither, and each controls
its own flavour independently.

## 3.3.3 Example

A backup tool whose `SeBackupPrivilege` was stripped by confinement:

```
{
  "event_type": "privilege-use",
  "event_time": <timestamp>,
  "subject": { ... },
  "object_context": <bin>,
  "privilege": "SeBackupPrivilege",
  "requested_access": 0x00000001,
  "granted_access":   0x00000001,
  "surviving_access": 0x00000000,
  "success": false,
  "process": { ... }
}
```

---

# 3.4 caap-policy-diagnostic

_Peios / Using Peios / Events / Kernel Access Events_

> Two unrelated central-access-policy conditions sharing one event type, told apart by the kind field — and the limits of the mismatch report.

Fires during the central access policy step of AccessCheck, for two
unrelated conditions distinguished by the `kind` field: a CAAP SACL that
failed to evaluate, and a staged policy that would have decided
differently from the effective one.

Event type string: `caap-policy-diagnostic`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1). |
| `object_context` | bin or nil | Caller-supplied object identifier. |
| `kind` | string | `sacl-error` or `staging-mismatch`. |
| `phase` | string | Which phase of CAAP evaluation produced the diagnostic. |
| `policy_sid` | bin or nil | The policy involved. `nil` for `staging-mismatch`. |
| `rule_index` | uint or nil | The rule involved. `nil` for `staging-mismatch`. |
| `reason` | string or nil | Diagnostic text, for `sacl-error`. |
| `requested_access` | uint | The mask the caller requested. |
| `effective_granted_access` | uint | Total granted under the effective policy. |
| `staged_granted_access` | uint | Total the staged policy would have granted. |
| `object_results_differ` | bool | Whether staged and effective differed for this object. |
| `process` | map | Process record (§2.2). |

Every key is always present; several are `nil` depending on `kind`.

## 3.4.1 kind = sacl-error

A central access rule's SACL could not be evaluated. `policy_sid`,
`rule_index` and `reason` identify what failed and where.

This is a defect in the policy, not in the access. The access is still
decided; the event says a rule that should have participated could not.

## 3.4.2 kind = staging-mismatch

A staged policy — one being trialled before it takes effect — would have
produced a different result from the policy actually in force. This is
the mechanism's whole purpose: run the new policy in parallel and report
where it would have changed something, before it can break anything.

`policy_sid` and `rule_index` are `nil` here. The mismatch is a property
of the whole evaluation, not attributable to one rule.

## 3.4.3 The limits of the mismatch report

Worth knowing before building anything on it.

It carries the two total granted masks and a boolean. It does **not**
identify which rule differed, and it does not report which audit events
would have changed.

For a mismatch that is purely in SACL behaviour, the two masks can be
**equal** while `object_results_differ` is true. A consumer comparing
only the masks will conclude nothing changed.

---

# 3.5 logon-session-destroyed

_Peios / Using Peios / Events / Kernel Access Events_

> The event fired when a logon session loses its last token reference — why it has no subject or process, and why nothing matches it at creation.

Fires when a logon session loses its last token reference and the kernel
destroys it.

Event type string: `logon-session-destroyed`.

| Key | Type | Meaning |
|---|---|---|
| `session_id` | uint | The destroyed session's LUID. |
| `user_sid` | bin | The session's user SID. |
| `logon_type` | uint | Interactive, Network, and so on. |
| `auth_package` | string | The authenticating package — `Kerberos`, `NTLM`, `local`. |
| `created_at` | uint | When the session was created. |

Every field is always present.

## 3.5.1 No subject, no process

This is the only KACS event with neither. The session *was* the subject,
and it has just ended. No process caused it — the last reference simply
went away, which may have been any process exiting, or none in
particular.

## 3.5.2 Correlating

`session_id` is the same LUID that every token in that session reported
as `subject.auth_id` (§2.1), and that LCS events report as
`caller.authentication_id` (§2.3). It is the join key for everything
that session ever did.

With `created_at`, the event bounds a session's whole lifetime, which is
what makes it useful for reconstructing a login after the fact.

The event fires **exactly once** per session. Consumers — authd
especially — use it to release session-scoped state: Kerberos tickets,
cached directory data, per-session credentials.

## 3.5.3 There is no matching creation event

Nothing is emitted when a session is created, so the pair is asymmetric.
See §3.6 for what else is absent and why.

---

# 3.6 corrupt-sd

_Peios / Using Peios / Events / Kernel Access Events_

> What FACS emits on finding a structurally invalid descriptor on a file, why it is rate-limited, and the related events that do not exist.

Fires when FACS encounters a structurally invalid security descriptor on
a file.

Event type string: `corrupt-sd`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1) for the access that triggered detection. |
| `object_context` | bin or nil | Identifier of the object whose descriptor was corrupt. |
| `reason` | string | What was wrong — `sd_too_large`, `acl_malformed`, `sid_invalid`. |
| `process` | map | Process record (§2.2). |

Every field is always present.

The subject is whoever happened to touch the file, not whoever caused
the corruption. Nothing records that.

## 3.6.1 Rate-limited by design

One event **per inode per cache population**. A corrupt descriptor read
a thousand times during one mount's life produces one event, not a
thousand.

This is deliberate: a filesystem with many corrupt descriptors would
otherwise drown the audit stream at exactly the moment the stream is
most needed. The consequence is that event count says nothing about
access count — one event does not mean one access.

## 3.6.2 Informational only

The kernel denies the access regardless. A corrupt descriptor fails
closed, and this event is not part of that decision — it is what tells
an administrator the corruption exists at all.

Without it, a file with a broken descriptor is simply inaccessible, with
nothing anywhere explaining why.

## 3.6.3 Events that do not exist

Three absences in the KACS set are worth stating, because each is a
reasonable thing to look for:

- **No `logon-session-created`.** Sessions are announced only when they
  end (§3.5). Track creation through authd's own records or by polling
  `/sys/kernel/security/kacs/sessions`.
- **No `token-created`.** A token's existence is observable through
  process inspection, not through an event.
- **No periodic or heartbeat events.** Audit here is entirely
  event-driven. A silent stream means nothing happened, not that
  anything is broken.

These are intentional. The kernel's audit surface covers access
decisions and session endings; other lifecycle tracking belongs to the
layers above it.

---

# 4.1 STRATAFS_COPY_UP

_Peios / Using Peios / Events / Filesystem Events_

> The event fired on every StrataFS copy-up, successful or not — six keys, no caller identity, and where ENOTDIR actually surfaces.

Fires on every StrataFS copy-up, successful or not.

Event type string: `STRATAFS_COPY_UP`.

| Key | Meaning |
|---|---|
| `path` | The relative path within the mount, `/`-prefixed. |
| `provider_index` | The stratum the object was copied from. |
| `provider_stratum` | That stratum's path. |
| `create_index` | The stratum it was copied into. |
| `create_stratum` | That stratum's path. |
| `result_errno` | Zero on success, the failure otherwise. |

Six keys, and no seventh.

## 4.1.1 No caller in the payload

Nothing here names a token, and that is not an omission.

Copy-up preserves the source object's descriptor, so the resulting file
records nothing about who caused it to exist. The caller is recovered
from the **envelope** instead: KMES stamps the effective, true and
process token GUIDs onto the header at ring-write time, and because
copy-up runs in the caller's own context, those are the caller's
(§1.2).

A consumer reading only payloads will conclude these events are
anonymous. They are not — the identity is one level out.

## 4.1.2 ENOTDIR arrives here, not in the refusal event

A parent materialisation that fails with `ENOTDIR` is reported through
this event, carried in `result_errno`, rather than through
`STRATAFS_MUTATION_REFUSED` (§4.2).

That is worth knowing when searching for a refusal and finding nothing:
the record exists, under a different type, with all the required fields
present.

---

# 4.2 STRATAFS_MUTATION_REFUSED

_Peios / Using Peios / Events / Filesystem Events_

> Refusals that come from how a mount is arranged rather than from an access check — what counts, two irregularities, and what is not audited at all.

Fires when a mutation is refused because of how the mount is arranged,
rather than because of an access check.

Event type string: `STRATAFS_MUTATION_REFUSED`.

| Key | Meaning |
|---|---|
| `path` | The relative path within the mount. |
| `operation` | The operation name. |
| `provider_index` | The provider stratum's index. |
| `provider_stratum` | That stratum's path. |
| `errno` | The refusal. |
| deferred flag | Whether the refusal was deferred. |

## 4.2.1 What counts as an arrangement refusal

One explicit list, matching the specification's enumeration exactly:
`EROFS`, `EXDEV`, `ENOTDIR`, `EISDIR`, `ENOTEMPTY`, `EEXIST`, `EINVAL`.

Call sites cover every mutating path — writes, mappings, truncation,
`fallocate`, splice, `copy_file_range`, `remap_file_range`, `setattr`,
`setxattr`, `removexattr`, creation, tmpfile, unlink, rmdir, link,
supersede and rename.

**`EACCES` is deliberately absent.** A refusal produced by an access
check is audited by the mechanism that performed it, and StrataFS does
not duplicate those records. Looking here for a permission denial finds
nothing; look for `access-audit` (§3.1) instead.

## 4.2.2 What these records are for

They report a mismatch between what a caller attempted and how the mount
is arranged — software writing where it cannot, or an arrangement that
does not admit an operation someone expected.

That is diagnostic information about configuration, and it is otherwise
visible only as an error returned to a caller that may well discard it.

Rollbacks are audited under this same type with the deferred flag set: a
create or link whose outer bookkeeping failed and whose lower object
could not be removed again, and a failed publication rollback after a
copy-up.

## 4.2.3 Two irregularities

**A refusal raised before a provider is known** — creation, tmpfile, the
heads of link and rename — passes a provider index of `-1`, so
`provider_stratum` is emitted as an empty string. The specification asks
for the provider stratum in every refusal record, so this is a gap
rather than a design.

**A refused deferred deletion is audited on any non-zero result**, not
only on the arrangement errors, so one refused by an access check does
produce a StrataFS record. This is a deliberate exception to the
`EACCES` exclusion above: the requirement to audit a deferred deletion
is unconditional, because by then nobody is left to receive the error.

## 4.2.4 What is not audited

Resolution, revalidation and enumeration emit nothing. They occur on
every path operation, reveal nothing the resulting access check does
not, and recording them would produce volume out of all proportion to
their significance. There is no audit call anywhere in the lookup path.

Access checks against provider objects are audited by KACS under its own
rules.

---

# 5.1 Registry Events

_Peios / Using Peios / Events / Registry Events_

> The seven audit events LCS emits, which carry the caller summary, and which are audited unconditionally.

LCS emits **seven** audit events through KMES. Six of them carry the
caller summary (§2.3) rather than the subject record.

| Event | Emitted when |
|---|---|
| `LCS_KEY_OPEN_AUDIT` | A key open matched a SACL audit ACE. |
| `LCS_BACKUP_START` | Before `REG_IOC_BACKUP` reads any subtree data. |
| `LCS_BACKUP_COMPLETE` | After a backup completes, or fails after starting. |
| `LCS_RESTORE_START` | Before `REG_IOC_RESTORE` modifies any source state. |
| `LCS_RESTORE_COMPLETE` | After a restore completes, or fails after starting. |
| `LCS_SOURCE_VALIDATION_FAILURE` | LCS rejected malformed source data. |
| `LCS_SELF_CONFIG_INVALID` | LCS rejected an invalid self-configuration value. |

Every payload is a msgpack map with string keys. GUIDs are 16-byte
binary values; SIDs are binary KACS encodings.

## 5.1.1 Backup and restore are audited unconditionally

Whatever the SACL on the target key says. They are privilege-gated bulk
operations that bypass per-key access checks entirely, so the audit
trail is the only record that they happened at all.

The start/complete pairing is deliberate too. `LCS_BACKUP_START` is
emitted *before* any data is read and `LCS_RESTORE_START` before any
state is modified, so an operation that dies partway still leaves
evidence that it began.

## 5.1.2 Separately: watch records

LCS also produces **watch records**, read from a key file descriptor.
These are not KMES events, not msgpack, and not audit — see §5.4.

---

# 5.2 LCS_KEY_OPEN_AUDIT

_Peios / Using Peios / Events / Registry Events_

> The event fired when a key open matches a SACL audit ACE, the SACL that produces nothing, and what happens when emission fails.

Fires when a key open matched a SACL audit ACE.

| Key | Meaning |
|---|---|
| `caller` | Caller summary (§2.3). |
| key GUID | The key that was opened. |
| `requested_access` | The mask after registry generic mapping, with `MAXIMUM_ALLOWED` re-added if the caller asked for it. |
| `granted_access` | The mask granted. Forced to zero on a denial. |
| decision | `allowed` or `denied`. |
| `sacl_match_flags` | Bit 0 for a success-audit match, bit 1 for a failure-audit match. No other bits. |

`granted_access` being zero on a denial is **enforced**, not merely
intended: a denied event carrying a non-zero granted mask is rejected as
a malformed payload.

SACL evaluation follows the KACS AccessCheck algorithm — the SACL is
evaluated alongside the DACL, not separately. Reading or modifying a
SACL requires `ACCESS_SYSTEM_SECURITY`, itself gated by
`SeSecurityPrivilege`.

## 5.2.1 One matching SACL that produces nothing

A request of `MAXIMUM_ALLOWED` **alone** maps to a desired mask of zero.
AccessCheck's SACL walk tests each audit ACE's mask against the mapped
desired access, and no ACE matches zero.

So an open with a matching audit ACE emits no event, and LCS emits
nothing. This is a real hole in coverage for anyone auditing key opens:
the one request shape that asks for everything is the one that records
nothing.

## 5.2.2 When emission fails

The policy is specific to this event, and differs from the bulk ones.

If LCS cannot **construct** a valid payload — corrupt internal state,
allocation failure, anything on the LCS side — the open fails with
`EIO` and no key fd is published. The audit is a precondition of the
access.

If the payload is valid but KMES cannot **retain** it — unavailable,
ring drops, capacity pressure, no consumer — the access decision and the
fd publication are unaffected. Loss accounting is KMES's problem, and
shows up as a `synthetic.gap` (§8.1) rather than as a failed open.

---

# 5.3 Validation and Configuration Failures

_Peios / Using Peios / Events / Registry Events_

> The two events LCS emits for malformed source data and invalid self-configuration — and why a first boot produces nineteen of them.

## 5.3.1 LCS_SOURCE_VALIDATION_FAILURE

Fires when LCS rejects malformed source data.

Carries the source slot identifier, then — where each is known — the
hive name, the RSI request id, the operation code and the key GUID. The
last field, `validation_class`, names what was wrong.

There are **twelve** classes:

| Group | Classes |
|---|---|
| Name fields | `malformed_layer_name`, `malformed_key_name`, `malformed_value_name` |
| Structural | `malformed_response_payload`, `malformed_key_metadata`, `malformed_value_payload`, `malformed_delete_layer_orphan_list` |
| Descriptors | `malformed_security_descriptor`, `malformed_layer_metadata_security_descriptor` |
| Sequencing | `future_sequence_number`, `duplicate_winning_sequence_tie` |
| Protocol | `unknown_rsi_status_code` |

The three name classes are field-specific — layer-name fields, key
component or child-name fields, and value-name fields respectively.

The structural classes cover a response whose operation-specific payload
has the wrong shape or trailing bytes; a lookup or enumeration whose
metadata block is incomplete, duplicated, unreferenced or nil; a value
payload with an invalid type, a tombstone/data mismatch or oversized
data; and an invalid orphan GUID array from `RSI_DELETE_LAYER`.

## 5.3.2 LCS_SELF_CONFIG_INVALID

Fires when LCS rejects an invalid self-configuration value.

Carries the parent path and value name of the offending parameter, the
expected type and numeric range, what was actually received — one of
`missing`, `wrong_type` or `dword_out_of_range`, with the actual type or
value where applicable — and the value LCS retained instead.

### 5.3.2.1 Expect nineteen of these on a first boot

Because `missing` counts as invalid, a first boot before seed restore
emits one event **per parameter on each refresh**: nineteen events
against an empty `Registry\` key.

That is correct and expected, and it is a noticeable share of the boot
audit stream. A consumer alerting on validation failures needs to know
it, or every fresh machine looks like it is failing.

---

# 5.4 Watch Records

_Peios / Using Peios / Events / Registry Events_

> Watch records are not KMES events — binary records read from a key file descriptor, their layout, and what a subtree watch adds.

Watch records are **not KMES events**. They are binary records read from
a key file descriptor with `read()`, and they exist to tell a watcher
that something under a key changed.

They are documented here because an operator asking what the registry
reports would otherwise miss them, but nothing else in this book applies
to them: no msgpack, no envelope, no identity stamps, no audit meaning.

## 5.4.1 Reading

A single `read()` returns as many complete records as fit in the
caller's buffer. A record is never split across two calls.

If the buffer cannot hold even the first queued record, `read()` fails
with `EINVAL` — the buffer is too small to make progress, and the caller
retries with a larger one. On an armed fd with an empty queue, `read()`
blocks, or returns `EAGAIN` under `O_NONBLOCK`.

Only records copied out in full are dequeued.

## 5.4.2 Layout

Every record begins with the same four fields.

| Offset | Size | Field |
|---|---|---|
| 0 | 4 | `total_len` |
| 4 | 2 | `event_type` |
| 6 | 2 | `name_len` |
| 8 | `name_len` | `name`, UTF-8 |

All integers are little-endian. `total_len` is the whole record
including this header, it is how a consumer advances to the next record,
and it is the **only** safe way to do so.

## 5.4.3 Subtree watches carry a path

A subtree watch's records carry additional fields after the name,
locating the key the change happened on relative to the watched key.

| Size | Field |
|---|---|
| 2 | `path_depth` |
| `2 + n` each | `path_components`: a `u16` length, then that many UTF-8 bytes |

`path_depth` is the number of components from the watched key down to
the changed key. Zero means the change was on the watched key itself.

The components are length-prefixed rather than joined by a separator
because registry names can contain any Unicode character and value names
can contain backslashes. A concatenated path string would be ambiguous.

`OVERFLOW` records are emitted in the bare eight-byte form, with no path
even on a subtree watch — there is no single key to name when the queue
itself overflowed.

The header offsets are ABI, named in `uapi/pkm/lcs.h` and listed in the
Peios Kernel TRM §5.A.

---

# 6.1 Job Events

_Peios / Using Peios / Events / Service Events_

> The three events peinit emits across a job's lifecycle, their payloads, and the ordering guarantee between them.

peinit emits a structured event at every job and operation lifecycle
transition. All of them go into the KMES ring buffer, encoded as msgpack
per the envelope in §1.2.

**There is no event socket.** Structured events are not sent to eventd
over any connection. eventd consumes them from the ring buffer, which is
why they survive eventd being down, restarted, or not yet existing. The
only thing peinit sends eventd over a socket is service output, which is
a different path with different guarantees.

## 6.1.1 The three job events

| Event | Fires when | Carries |
|---|---|---|
| `job.created` | The job object exists. | Job identifier, service name, type, image path, identity, operation identifier. |
| `job.started` | `exec` succeeded. | Job identifier, PID, cgroup path. |
| `job.ended` | The process exited or was killed. | Job identifier, final state, exit code or signal, duration, failure cause. |

The event type is the dotted string; the fields form the msgpack
payload. The payloads are supersets of the summaries above — `job.ended`
in particular carries the whole record.

## 6.1.2 Ordering

When one runtime step produces several lifecycle events, peinit emits
them in causal order **before** committing the retained state for that
step. A consumer sees the events in an order consistent with what
happened, not in whatever order the writes completed.

---

# 6.2 Operation Events

_Peios / Using Peios / Events / Service Events_

> The seven operation lifecycle events, the five fields they share, what duration_ns measures from, and the one field that goes by three names.

Every operation event carries the same five fields: `operation_id`,
`type`, `service`, `source`, `caller` — `null` for a
lifecycle-generated operation — and `state` after the transition.

Seven types, each adding its own fields:

| Event | Adds |
|---|---|
| `operation.requested` | — |
| `operation.started` | — |
| `operation.completed` | `duration_ns`, `result` |
| `operation.failed` | `duration_ns`, `failure_reason` |
| `operation.cancelled` | `reason` |
| `operation.merged` | `merged_into` |
| `operation.aborted` | `duration_ns`, `reason` |

## 6.2.1 duration_ns measures from creation

Not from the start of execution. The reasoning is the same as for the
operation timeout: what a caller waited is what matters, and queue time
is part of it.

An operation that sat in a queue for a minute and then ran for a second
reports 61 seconds, not 1.

## 6.2.2 One field, three names

The same value appears under three spellings across two surfaces:

- `failure_reason` on `operation.failed`
- `reason` on `operation.cancelled` and `operation.aborted`
- `error` in the control interface's operation view (PSPU §4)

A consumer correlating an event stream against the control interface has
to map all three onto each other.

---

# 7.1 The Event Set

_Peios / Using Peios / Events / Package Events_

> The eleven event types peipkg emits, their payload fields, and the privilege emission depends on.

peipkg emits eleven event types into the kernel event subsystem. The
caller's identity is stamped into the envelope by the kernel (§1.2) and
is not part of the payload.

| Type | Emitted for | Emitted today |
|---|---|---|
| `peipkg.install` | A successful install | yes |
| `peipkg.upgrade` | A successful upgrade, downgrade, or undo | yes |
| `peipkg.uninstall` | A successful uninstall | yes |
| `peipkg.refresh` | A repository refresh, successful or partially failed | yes |
| `peipkg.transaction-failed` | A rejected or rolled-back transaction | yes |
| `peipkg.recovery` | A recovery resolved through `peipkg recover` | yes |
| `peipkg.authorisation` | An operator authorisation record | yes |
| `peipkg.repo-add` | A repository add | yes |
| `peipkg.repo-remove` | A repository remove | yes |
| `peipkg.claim` | A claim grant or revoke | yes |
| `peipkg.config-change` | A trust-policy or transport-flag change | **no** |

`peipkg.config-change` is specified and **not emitted**. A trust-policy
or transport-flag change today produces no event at all.

## 7.1.1 Payload fields

| Field | Content |
|---|---|
| `txn_id` | The transaction identifier. |
| `outcome` | `success`, `rejection`, or `rollback`. |
| `repo` | The repository, for repository operations. |
| `detail` | The rejection reason, the operation count, or the authorised action. |
| `timestamp` | RFC 3339, UTC. |
| `packages` | Name, version and architecture per package. |

Note `timestamp` here is an **RFC 3339 string**, not the uint used
elsewhere in this book (§1.3). peipkg carries its own.

## 7.1.2 Emission depends on a privilege

An audit privilege on the caller's token. Without it, emission fails,
peipkg warns, and **the operation proceeds unaudited**. The absence of
an event does not mean the operation did not happen.

On a kernel with no emit call, emission is a silent successful no-op.

---

# 7.2 What Is Not Recorded

_Peios / Using Peios / Events / Package Events_

> The specific gaps in peipkg's audit trail — each a place where something happened and nothing was written — and how to read the trail knowing them.

The gaps in peipkg's audit trail are specific, and each one is a place
where something happened and nothing was written.

- An **install or upgrade event carries no source repository**, although
  one is known at the time.
- A **committed cross-root operation's success event carries no
  transaction identifier**, so it cannot be correlated with the rest of
  its transaction.
- **Automatic recovery** at the head of an ordinary operation emits
  nothing. Only recovery through `peipkg recover` produces
  `peipkg.recovery`.
- **`peipkg recover`'s failure paths** emit nothing. A recovery that
  fails leaves no record that it was attempted.
- **Declining at a prompt** emits nothing.
- **Enabling insecure transport** emits no authorisation record, and so
  does **installing unsigned content under an `optional` policy** — the
  two decisions most worth recording are the two that are not.
- **`peipkg-compose` emits nothing at all.** Image composition is
  entirely unaudited.

## 7.2.1 Reading the trail with these in mind

Two consequences follow for anyone building on this stream.

**Absence is not evidence.** An operation with no event may have been
performed by a caller without the audit privilege, may have taken one of
the paths above, or may not have happened. The three are
indistinguishable from the stream.

**Transaction correlation is incomplete.** `txn_id` ties an operation's
events together, but the cross-root success case omits it, so a
reconstruction keyed on `txn_id` will silently drop those.

---

# 8.1 Synthetic Events

_Peios / Using Peios / Events / Event Daemon Events_

> The records eventd generates about itself, written straight to a shard and never through KMES — which shard, what the timestamp means, and what has no event at all.

Synthetic events are records eventd generates **about itself**. They are
written straight into a shard database and never touch KMES.

They carry no KMES header: no identity stamps, no sequence number, no
origin class. What they have is a wall-clock timestamp taken when eventd
generated the record, and a type string prefixed `synthetic.`, which is
what distinguishes them in the `events` table — there is no separate
record-type column.

| Condition | Type |
|---|---|
| Lost events detected on a CPU | `synthetic.gap` |
| eventd started and attached to KMES | `synthetic.startup` |
| Graceful shutdown beginning | `synthetic.shutdown` |
| A write to any store failed | `synthetic.storage_error` |
| A configuration value changed at runtime | `synthetic.config_change` |

Five, and no more. Payload schemas are in the eventd manual §3.2.

## 8.1.1 There is no event for bad input

Deliberately. Log and metric datagrams arrive unauthenticated from
arbitrary local processes, and emitting a durable record per bad
datagram would hand every process an amplification primitive
(PSPU §3.4).

These five are conditions eventd observed **about itself**, not
reactions to what it was sent.

## 8.1.2 Which shard they land in

**CPU-specific** — `synthetic.gap` — goes to the shard assigned to the
CPU that generated it, handed to that writer thread alongside that CPU's
ordinary events. A gap record travels with the events it describes.

**Daemon-wide** — startup, shutdown, config changes, storage errors — go
to shard 0 when shard 0 is writable, otherwise to the lowest-numbered
writable active shard. If no shard is writable, the event is skipped and
the failure is logged to standard error.

A storage error is why the fallback exists. It describes a failure on
one shard but is itself a daemon-wide notification, so it is not written
to the failing shard unless that shard has since been replaced and is
writable again. Writing the record of a shard's failure into that shard
would lose it exactly when it matters.

## 8.1.3 The timestamp is when eventd noticed

Not when the condition occurred.

A `synthetic.gap` is stamped at **detection**, which may be long after
the events it describes were overwritten — and after a restart it may be
the first thing written in a new boot about events lost in the previous
one.

An investigation that treats a gap record's timestamp as the time of the
loss will look in the wrong window.

## 8.1.4 Storage and ordering

Synthetic events live in the same shard databases as KMES events and
take part in the same batching, retention and queries. Access control
treats their types like any other, so
`Machine\System\eventd\Security\Events\synthetic` governs them.

They are ordered by their eventd-assigned timestamp and take no part in
per-CPU sequence numbering.

---

# Appendix A All Event Types

_Peios / Using Peios / Events_

> Every event type in this book in one table — thirty-four types across six emitters, plus the registry's watch records and the known holes.

Every event type in this book, in one table. Thirty-four types across
six emitters, plus the registry's watch records, which are a separate
mechanism.

## A.1 KMES events

| Type | Emitter | Where |
|---|---|---|
| `access-audit` | KACS | §3.1 |
| `continuous-audit` | KACS | §3.2 |
| `privilege-use` | KACS | §3.3 |
| `caap-policy-diagnostic` | KACS | §3.4 |
| `logon-session-destroyed` | KACS | §3.5 |
| `corrupt-sd` | KACS | §3.6 |
| `STRATAFS_COPY_UP` | StrataFS | §4.1 |
| `STRATAFS_MUTATION_REFUSED` | StrataFS | §4.2 |
| `LCS_KEY_OPEN_AUDIT` | LCS | §5.2 |
| `LCS_BACKUP_START` | LCS | §5.1 |
| `LCS_BACKUP_COMPLETE` | LCS | §5.1 |
| `LCS_RESTORE_START` | LCS | §5.1 |
| `LCS_RESTORE_COMPLETE` | LCS | §5.1 |
| `LCS_SOURCE_VALIDATION_FAILURE` | LCS | §5.3 |
| `LCS_SELF_CONFIG_INVALID` | LCS | §5.3 |
| `job.created` | peinit | §6.1 |
| `job.started` | peinit | §6.1 |
| `job.ended` | peinit | §6.1 |
| `operation.requested` | peinit | §6.2 |
| `operation.started` | peinit | §6.2 |
| `operation.completed` | peinit | §6.2 |
| `operation.failed` | peinit | §6.2 |
| `operation.cancelled` | peinit | §6.2 |
| `operation.merged` | peinit | §6.2 |
| `operation.aborted` | peinit | §6.2 |
| `peipkg.install` | peipkg | §7.1 |
| `peipkg.upgrade` | peipkg | §7.1 |
| `peipkg.uninstall` | peipkg | §7.1 |
| `peipkg.refresh` | peipkg | §7.1 |
| `peipkg.transaction-failed` | peipkg | §7.1 |
| `peipkg.recovery` | peipkg | §7.1 |
| `peipkg.authorisation` | peipkg | §7.1 |
| `peipkg.repo-add` | peipkg | §7.1 |
| `peipkg.repo-remove` | peipkg | §7.1 |
| `peipkg.claim` | peipkg | §7.1 |
| `peipkg.config-change` | peipkg | §7.1 — **specified, not emitted** |

## A.2 Not KMES

| Type | Emitter | Transport | Where |
|---|---|---|---|
| `synthetic.gap` | eventd | Written direct to a shard | §8.1 |
| `synthetic.startup` | eventd | Written direct to a shard | §8.1 |
| `synthetic.shutdown` | eventd | Written direct to a shard | §8.1 |
| `synthetic.storage_error` | eventd | Written direct to a shard | §8.1 |
| `synthetic.config_change` | eventd | Written direct to a shard | §8.1 |
| Watch records | LCS | `read()` on a key fd | §5.4 |

## A.3 Which carry an identity, and how

| Events | Identity from |
|---|---|
| KACS, all but `logon-session-destroyed` | `subject` record in the payload (§2.1) |
| `logon-session-destroyed` | `user_sid` and `session_id` directly; the session was the subject |
| LCS, six of seven | `caller` summary in the payload (§2.3) |
| StrataFS, peinit, peipkg | The **envelope** only. No identity in the payload (§1.2) |
| eventd synthetic | None. eventd is describing itself |

## A.4 Known holes

Collected from the chapters, because a reader planning coverage needs
them in one place:

- `peipkg.config-change` is never emitted (§7.1).
- peipkg omits the source repository on install and upgrade, the
  transaction id on committed cross-root success, and emits nothing for
  automatic recovery, recovery failures, declined prompts, insecure
  transport, unsigned installs under an `optional` policy, or
  `peipkg-compose` (§7.2).
- A registry key open requesting `MAXIMUM_ALLOWED` alone emits no
  `LCS_KEY_OPEN_AUDIT`, because the mapped desired mask is zero and no
  ACE matches zero (§5.2).
- StrataFS refusals raised before a provider is known emit an empty
  `provider_stratum` (§4.2).
- `caap-policy-diagnostic` with `kind = staging-mismatch` does not
  identify which rule differed, and its two masks can be equal while
  `object_results_differ` is true (§3.4).
- There is no `logon-session-created`, no `token-created`, and no
  heartbeat (§3.6).
- peipkg emits nothing when the caller's token lacks the audit privilege
  (§7.1).

---

# Constants and catalogs

_Peios / Using Peios / Constants and Catalogs_

> Where every numeric constant in Peios is catalogued — access rights and GenericMappings here, ACE types and well-known SIDs in PCDS, privileges in the kernel manual.

Numeric constants — right bits, type values, enum members, limits — are catalogued in exactly one place each. This topic is either that place or a pointer to it.

## What lives here

| Page | Holds |
|---|---|
| [Access mask bits](/peios/using-peios/constants-and-catalogs/access-mask-bits.md) | Per-object-type rights for files, processes, tokens, registry keys and services; the GenericMapping tables; the `*_ALL_ACCESS` and `STANDARD_RIGHTS_*` aggregates. |
| [Other constants](/peios/using-peios/constants-and-catalogs/other-constants.md) | Impersonation levels, integrity levels, logon types, elevation types, PIP tiers, audit policy flags, create dispositions, `SECURITY_INFORMATION` flags, and the kernel's size limits. |

## What lives elsewhere

Three catalogues are owned by documents that also define their semantics, and are not duplicated here.

| Catalogue | Canonical home |
|---|---|
| ACE types, ACE flags, and their numeric values | PCDS §5.4 — see [ACE types and flags](/peios/using-peios/constants-and-catalogs/ace-types-and-flags.md) |
| Well-known SIDs | PCDS §4.4 — see [Well-known SIDs](/peios/using-peios/constants-and-catalogs/well-known-sids.md) |
| Every privilege, with its LUID bit | Peios Kernel TRM §3.4.2 — see [Privilege catalog](/peios/using-peios/constants-and-catalogs/privilege-catalog.md) |

The three pages above are signposts. Following one gets you to the table.

> [!NOTE]
> A constant with two spellings is listed under the name you would meet it by. Where a header and a specification disagree — and they do, for impersonation levels, create dispositions and the group attribute flags — the page says so and names both. The full mapping is the Peios Kernel TRM §3.A.

## Related catalogues

- Every **event type** the system emits: the [Peios Events Index](/peios/using-peios/events/all-event-types.md).
- Every **audit event's payload schema**: the same book, chapters 3 to 8.

---

# Well-known SIDs

_Peios / Using Peios / Constants and Catalogs_

> Where the well-known SID catalogue lives — PCDS §4.4 — and the PIP trust label ladder.

The well-known SID catalogue is **PCDS §4.4**, in the Peios Core Data Structures specification. It is normative there, and is not duplicated here.

It covers the universal SIDs (`S-1-1-0` Everyone, `S-1-3-0` CREATOR OWNER, and the rest), the NT authority SIDs under `S-1-5`, the BUILTIN groups under `S-1-5-32`, domain SID structure, integrity label SIDs under `S-1-16`, capability SIDs under `S-1-15`, service SIDs under `S-1-5-80`, and the PIP trust labels under `S-1-19`.

For the conceptual treatment — which principals matter and why — read [Well-known principals](/peios/security-fundamentals/identity/well-known-principals.md).

## A note on the BUILTIN range

PCDS lists the BUILTIN groups KACS assigns meaning to. It deliberately does **not** enumerate `S-1-5-32-547` through `S-1-5-32-583`, which Active Directory defines: KACS gives them no special semantics, and they participate in ACE matching like any other group SID.

An older revision of this reference tabulated them. That table was removed on purpose, not lost — enumerating names the system does not act on implies a behaviour that does not exist.

## PIP trust labels

The `S-1-19-T-L` ladder encodes two dimensions: `T` is the PIP type axis, `L` the trust axis. Dominance requires both to be greater than or equal.

The numeric tiers are in [Other constants](/peios/using-peios/constants-and-catalogs/other-constants.md), and the full SID list is in PCDS §4.4. The mechanism is [Process integrity protection](/peios/security-fundamentals/process-integrity-protection/overview.md).

---

# Privilege catalog

_Peios / Using Peios / Constants and Catalogs_

> Where the per-privilege catalogue lives — the Peios Kernel TRM — and the two privileges that are enforced but unnamed.

The per-privilege catalogue — every name, its LUID bit position, and what it does — is in the **Peios Kernel TRM §3.4.2**, "Catalogue". It is not duplicated here.

The conceptual treatment is [Privileges](/peios/security-fundamentals/privileges/overview.md), and the four-category model is [Categories](/peios/security-fundamentals/privileges/categories.md).

## Five privileges influence an access check

Only five can contribute bits to a granted mask, and therefore only five can appear in a `privilege-use` event:

- `SeSecurityPrivilege`
- `SeTakeOwnershipPrivilege`
- `SeBackupPrivilege`
- `SeRestorePrivilege`
- `SeRelabelPrivilege`

Any other bit fails the audit encoder closed rather than emitting an unnamed privilege. See the [Events Index §3.3](/peios/using-peios/events/kernel-access-events/privilege-use.md).

## Two are enforced but not nameable

`SeTakeOwnershipPrivilege` and `SeRelabelPrivilege` are enforced by KACS but absent from the published privilege table, so they cannot be named in a service's `RequiredPrivileges`. A service needing either declares nothing and takes its source token's defaults, or fails to start if it tries to name one.

That asymmetry is peinit's, not the kernel's — see the peinit TRM §4.5.

---

# ACE types and flags

_Peios / Using Peios / Constants and Catalogs_

> Where the ACE type and flag catalogues live — PCDS §5.4 — including the AceType constant table and the MIC policy bits.

The ACE catalogue is **PCDS §5.4**, in the Peios Core Data Structures specification. It is normative there, and is not duplicated here.

It covers every `AceType` value from 0x00 to 0x15 — the body layout of each family, the `AceFlags` bits, and the ACL revision rules that constrain which types may appear.

Twenty of those values have behaviour. Two do not: 0x04 (`ACCESS_ALLOWED_COMPOUND_ACE`, never implemented anywhere) and 0x15 (`SYSTEM_ACCESS_FILTER_ACE`, an MS-DTYP type Peios has not implemented). Both are named by the kernel ABI so that a decoder can label the byte, but neither affects an access decision: they are skipped when the descriptor is evaluated and preserved unchanged when it is written back. `sd` has no name for either and prints them as `OTHER(0x04)` and `OTHER(0x15)`.

## Two names per type

PCDS names both the ACE **structure** and the `AceType` **constant** that selects it, because a reader may arrive with either — one from a declaration, the other from a hex dump. `ACCESS_ALLOWED_ACE` is the structure; `ACCESS_ALLOWED_ACE_TYPE` is the constant whose value is 0x00.

The headers use a third spelling, moving the qualifier to the front: `KACS_ACE_TYPE_ACCESS_ALLOWED`. The Peios Kernel TRM §3.A maps the two vocabularies.

## Inheritance flags

The four propagation flags and the `INHERITED_ACE` provenance flag are catalogued with the inheritance algorithm in PCDS §5.6, rather than with the type values.

## MIC policy bits

The mask of a `SYSTEM_MANDATORY_LABEL_ACE` carries policy bits saying what a non-dominant caller may not do. Those bits are in PCDS §5.4 alongside the ACE type; the mechanism is [Mandatory integrity control](/peios/security-fundamentals/access-decisions/mandatory-integrity-control.md).

---

# Access mask bits

_Peios / Using Peios / Constants and Catalogs_

> The per-object-type access rights — file, process, token, registry key, service — plus the standard, special and generic rights, the GenericMapping tables, and the aggregate *_ALL_ACCESS constants.

The 32-bit access mask has the same shape for every object type. Bits 0–15 are object-specific, 16–20 are standard rights, 24–25 are special, 28–31 are generic. The standard, special and generic regions are uniform; the object-specific bits carry different meanings for different object types.

This page is the catalogue of right *values*. What the mask **means** — how it is evaluated, how generic bits expand — is PCDS §5.3, which is normative. The bit layout of the mask within a descriptor is PCDS §5.1.

| Bit range | Region | Examples |
|---|---|---|
| 0–15 | Object-specific | `FILE_READ_DATA`, `PROCESS_TERMINATE`, `TOKEN_QUERY` |
| 16–20 | Standard rights | `DELETE`, `READ_CONTROL`, `WRITE_DAC`, `WRITE_OWNER`, `SYNCHRONIZE` |
| 21–23 | Reserved | Rejected at parse. PCDS §5.3. |
| 24–25 | Special | `ACCESS_SYSTEM_SECURITY`, `MAXIMUM_ALLOWED` |
| 26–27 | Reserved | Rejected at parse. |
| 28–31 | Generic | `GENERIC_ALL`, `GENERIC_EXECUTE`, `GENERIC_WRITE`, `GENERIC_READ` |

## File access rights

| Right | Value | For files | For directories (alias) |
|---|---|---|---|
| `FILE_READ_DATA` | 0x0001 | Read file content | `FILE_LIST_DIRECTORY` — list entries |
| `FILE_WRITE_DATA` | 0x0002 | Write file content | `FILE_ADD_FILE` — create files |
| `FILE_APPEND_DATA` | 0x0004 | Append-only write | `FILE_ADD_SUBDIRECTORY` — create subdirectory |
| `FILE_READ_EA` | 0x0008 | Read extended attributes | Same |
| `FILE_WRITE_EA` | 0x0010 | Write extended attributes | Same |
| `FILE_EXECUTE` | 0x0020 | Execute file | `FILE_TRAVERSE` — traverse through |
| `FILE_DELETE_CHILD` | 0x0040 | (not applicable) | Delete children regardless of their permissions |
| `FILE_READ_ATTRIBUTES` | 0x0080 | Read attributes | Same |
| `FILE_WRITE_ATTRIBUTES` | 0x0100 | Write attributes | Same |

The directory names are **aliases**: identical bit values, different naming convention depending on whether the object is a file or a directory.

### File GenericMapping

| Generic right | Maps to |
|---|---|
| `GENERIC_READ` | `FILE_READ_DATA` \| `FILE_READ_ATTRIBUTES` \| `FILE_READ_EA` \| `READ_CONTROL` \| `SYNCHRONIZE` |
| `GENERIC_WRITE` | `FILE_WRITE_DATA` \| `FILE_APPEND_DATA` \| `FILE_WRITE_ATTRIBUTES` \| `FILE_WRITE_EA` \| `READ_CONTROL` \| `SYNCHRONIZE` |
| `GENERIC_EXECUTE` | `FILE_EXECUTE` \| `FILE_READ_ATTRIBUTES` \| `READ_CONTROL` \| `SYNCHRONIZE` |
| `GENERIC_ALL` | Every file-specific bit, plus `DELETE`, `READ_CONTROL`, `WRITE_DAC`, `WRITE_OWNER`, `SYNCHRONIZE` |

`FILE_ALL_ACCESS` = `STANDARD_RIGHTS_REQUIRED` \| `SYNCHRONIZE` \| `0x1FF` = **0x001F01FF**.

## Process access rights

| Right | Value | Meaning |
|---|---|---|
| `PROCESS_TERMINATE` | 0x0001 | Send terminating signals. |
| `PROCESS_SIGNAL` | 0x0002 | Send non-terminating informational signals — SIGCHLD, SIGURG, SIGWINCH. |
| `PROCESS_VM_READ` | 0x0010 | Read process memory — `ptrace(PTRACE_PEEK*)`, `process_vm_readv`, `/proc/<pid>/mem` reads. |
| `PROCESS_VM_WRITE` | 0x0020 | Write process memory — `ptrace(PTRACE_POKE*, ATTACH)`, `process_vm_writev`. |
| `PROCESS_DUP_HANDLE` | 0x0040 | Duplicate file descriptors out via `pidfd_getfd`. |
| `PROCESS_SET_INFORMATION` | 0x0200 | Change process attributes — priority, affinity, rlimits, `/proc/<pid>/*` writes. |
| `PROCESS_QUERY_INFORMATION` | 0x0400 | Detailed process info — token, full `/proc/<pid>/*` reads. |
| `PROCESS_SUSPEND_RESUME` | 0x0800 | Send stop and continue signals. |
| `PROCESS_QUERY_LIMITED` | 0x1000 | Basic info — PID, image name, state. Required by `pidfd_open`. |

Bits 0x0004, 0x0008, 0x0080 and 0x0100 are unused.

### Process GenericMapping

| Generic right | Maps to |
|---|---|
| `GENERIC_READ` | `PROCESS_QUERY_INFORMATION` \| `PROCESS_VM_READ` \| `READ_CONTROL` |
| `GENERIC_WRITE` | `PROCESS_SET_INFORMATION` \| `PROCESS_VM_WRITE` \| `WRITE_DAC` |
| `GENERIC_EXECUTE` | `PROCESS_TERMINATE` \| `PROCESS_SUSPEND_RESUME` \| `PROCESS_QUERY_LIMITED` |
| `GENERIC_ALL` | All process-specific bits, plus `STANDARD_RIGHTS_REQUIRED` \| `SYNCHRONIZE` |

`PROCESS_ALL_ACCESS` = `STANDARD_RIGHTS_REQUIRED` \| `SYNCHRONIZE` \| `0x1FFF` = **0x001F1FFF**.

## Token access rights

| Right | Value | Meaning |
|---|---|---|
| `TOKEN_ASSIGN_PRIMARY` | 0x0001 | Install as a process's primary token. |
| `TOKEN_DUPLICATE` | 0x0002 | Create a copy. |
| `TOKEN_IMPERSONATE` | 0x0004 | Install as a thread's impersonation token. |
| `TOKEN_QUERY` | 0x0008 | Read token information. |
| `TOKEN_QUERY_SOURCE` | 0x0010 | Subsumed by `TOKEN_QUERY`. Reserved for format compatibility. |
| `TOKEN_ADJUST_PRIVILEGES` | 0x0020 | Enable, disable or remove privileges. |
| `TOKEN_ADJUST_GROUPS` | 0x0040 | Enable or disable groups. |
| `TOKEN_ADJUST_DEFAULT` | 0x0080 | Change default DACL, owner index, primary group index. |
| `TOKEN_ADJUST_SESSIONID` | 0x0100 | Change `interactive_session_id`. Additionally requires `SeTcbPrivilege`. |

`TOKEN_QUERY_SOURCE` is a documented bit position rather than an enforced right: a token fd granting `TOKEN_QUERY` suffices for everything, and `TOKEN_QUERY_SOURCE` is not separately checked.

### Token GenericMapping

| Generic right | Maps to |
|---|---|
| `GENERIC_READ` | `TOKEN_QUERY` \| `READ_CONTROL` |
| `GENERIC_WRITE` | `TOKEN_ADJUST_PRIVILEGES` \| `TOKEN_ADJUST_GROUPS` \| `TOKEN_ADJUST_DEFAULT` \| `WRITE_DAC` |
| `GENERIC_EXECUTE` | `TOKEN_IMPERSONATE` |
| `GENERIC_ALL` | `TOKEN_ALL_ACCESS` |

`TOKEN_ALL_ACCESS` = `STANDARD_RIGHTS_REQUIRED` \| `0x01FF` = **0x000F01FF**. Note the absence of `SYNCHRONIZE`, unlike the file and process aggregates.

## Registry-key access rights

| Right | Value | Meaning |
|---|---|---|
| `KEY_QUERY_VALUE` | 0x0001 | Read a value. |
| `KEY_SET_VALUE` | 0x0002 | Write a value. |
| `KEY_CREATE_SUB_KEY` | 0x0004 | Create a subkey. |
| `KEY_ENUMERATE_SUB_KEYS` | 0x0008 | Enumerate subkeys. |
| `KEY_NOTIFY` | 0x0010 | Watch for changes. |
| `KEY_CREATE_LINK` | 0x0020 | Create a symbolic link to another key. |

Bits from 0x0040 upward are reserved.

### Registry GenericMapping

| Generic right | Maps to |
|---|---|
| `GENERIC_READ` | `KEY_QUERY_VALUE` \| `KEY_ENUMERATE_SUB_KEYS` \| `KEY_NOTIFY` \| `READ_CONTROL` |
| `GENERIC_WRITE` | `KEY_SET_VALUE` \| `KEY_CREATE_SUB_KEY` \| `READ_CONTROL` |
| `GENERIC_EXECUTE` | `READ_CONTROL` |
| `GENERIC_ALL` | All key-specific bits, plus `STANDARD_RIGHTS_REQUIRED` \| `SYNCHRONIZE` |

## Service access rights

| Right | Value | Meaning |
|---|---|---|
| `SERVICE_QUERY_CONFIG` | 0x0001 | Read configuration. |
| `SERVICE_CHANGE_CONFIG` | 0x0002 | Modify configuration. |
| `SERVICE_QUERY_STATUS` | 0x0004 | Read runtime status. |
| `SERVICE_ENUMERATE_DEPENDENTS` | 0x0008 | List dependent services. |
| `SERVICE_START` | 0x0010 | Start the service. |
| `SERVICE_STOP` | 0x0020 | Stop the service. |
| `SERVICE_PAUSE_CONTINUE` | 0x0040 | Pause and resume. |
| `SERVICE_INTERROGATE` | 0x0080 | Request a status update. |
| `SERVICE_USER_DEFINED_CONTROL` | 0x0100 | Send service-specific control codes. |

## Standard, special and generic rights

| Right | Value |
|---|---|
| `DELETE` | 0x00010000 |
| `READ_CONTROL` | 0x00020000 |
| `WRITE_DAC` | 0x00040000 |
| `WRITE_OWNER` | 0x00080000 |
| `SYNCHRONIZE` | 0x00100000 |
| `ACCESS_SYSTEM_SECURITY` | 0x01000000 |
| `MAXIMUM_ALLOWED` | 0x02000000 |
| `GENERIC_ALL` | 0x10000000 |
| `GENERIC_EXECUTE` | 0x20000000 |
| `GENERIC_WRITE` | 0x40000000 |
| `GENERIC_READ` | 0x80000000 |

### The STANDARD_RIGHTS aggregates

| Constant | Value | Composition |
|---|---|---|
| `STANDARD_RIGHTS_REQUIRED` | 0x000F0000 | `DELETE` \| `READ_CONTROL` \| `WRITE_DAC` \| `WRITE_OWNER` |
| `STANDARD_RIGHTS_READ` | 0x00020000 | Alias of `READ_CONTROL` |
| `STANDARD_RIGHTS_WRITE` | 0x00020000 | Alias of `READ_CONTROL` |
| `STANDARD_RIGHTS_EXECUTE` | 0x00020000 | Alias of `READ_CONTROL` |
| `STANDARD_RIGHTS_ALL` | 0x001F0000 | All five standard rights |

Three of these are the **same value**. `STANDARD_RIGHTS_READ`, `_WRITE` and `_EXECUTE` are all aliases of `READ_CONTROL`, which surprises people reading a mask and expecting them to differ. Only `STANDARD_RIGHTS_REQUIRED` and `STANDARD_RIGHTS_ALL` name distinct values.

`STANDARD_RIGHTS_REQUIRED` is the conventional minimum included in every `*_ALL_ACCESS` aggregate.

---

# Other constants

_Peios / Using Peios / Constants and Catalogs_

> Enumerated values that are not access rights — impersonation levels, integrity levels, logon types, PIP tiers, token audit policy, create dispositions, SECURITY_INFORMATION flags, mitigation flags, and the kernel's size limits.

Enumerated values that are not access rights. Access rights are catalogued separately in [Access mask bits](/peios/using-peios/constants-and-catalogs/access-mask-bits.md).

> [!NOTE]
> Names here are the ones `uapi/pkm/` declares, because that is where a reader meets them. Several were published under different spellings in older design documents; where that is so, this page says which. The Peios Kernel TRM §3.A carries the full mapping.

## Impersonation levels

A token's impersonation level. The conceptual model is [Impersonation levels](/peios/security-fundamentals/impersonation/impersonation-levels.md).

| Constant | Value | Meaning |
|---|---|---|
| `KACS_IMLEVEL_ANONYMOUS` | 0 | No identity. |
| `KACS_IMLEVEL_IDENTIFICATION` | 1 | Inspect only. Cannot be used for an access check. |
| `KACS_IMLEVEL_IMPERSONATION` | 2 | Act as the client locally. The default. |
| `KACS_IMLEVEL_DELEGATION` | 3 | Act as the client locally, and forward credentials to remote machines. |

Older documents spell these `KACS_LEVEL_*`. That spelling does not exist in any header.

A primary token reports level 0.

## Integrity levels

The RIDs used in `S-1-16-*` SIDs and in a token's `integrity_level` field. The model is [Mandatory integrity control](/peios/security-fundamentals/access-decisions/mandatory-integrity-control.md).

| Constant | RID | Name |
|---|---|---|
| `INTEGRITY_LEVEL_UNTRUSTED` | 0 | Untrusted |
| `INTEGRITY_LEVEL_LOW` | 4096 | Low |
| `INTEGRITY_LEVEL_MEDIUM` | 8192 | Medium |
| `INTEGRITY_LEVEL_HIGH` | 12288 | High |
| `INTEGRITY_LEVEL_SYSTEM` | 16384 | System |

Spaced by 4096, so future levels can be inserted between existing ones.

**These are named levels, not a closed enum.** The integrity level is the `S-1-16` SID's single sub-authority as an unsigned integer, compared numerically. Any such value is valid, and non-standard ones appear in Windows-interop descriptors — `medium-plus` at 8448, `protected` at 20480. Code that switches on the five names will mishandle those.

## Mandatory policy flags

A token's `mandatory_policy` field. Both are immutable after token creation.

| Flag | Value | Meaning |
|---|---|---|
| `NO_WRITE_UP` | 0x01 | MIC blocks write-category access from lower integrity. |
| `NEW_PROCESS_MIN` | 0x02 | At exec, lower the token's integrity to match the binary if the binary is lower. |

## Logon types

A session's `logon_type` field. The model is [Logon types](/peios/security-fundamentals/logon-sessions/logon-types.md).

| Constant | Value | Meaning |
|---|---|---|
| `LOGON_TYPE_INTERACTIVE` | 2 | Console, SSH, terminal services. |
| `LOGON_TYPE_NETWORK` | 3 | Network resource access — SMB, RPC, federated. |
| `LOGON_TYPE_BATCH` | 4 | Scheduled job. |
| `LOGON_TYPE_SERVICE` | 5 | Service running under a specific principal. |
| `LOGON_TYPE_NETWORK_CLEARTEXT` | 8 | Network logon with a cleartext credential. |
| `LOGON_TYPE_NEW_CREDENTIALS` | 9 | Keep the local identity; use alternative credentials outbound. |

Values 1, 6, 7 and 10 upward are reserved.

## Elevation types

A token's `elevation_type` field.

| Constant | Value | Meaning |
|---|---|---|
| `KACS_ELEVATION_DEFAULT` | 1 | Not part of a linked pair. |
| `KACS_ELEVATION_FULL` | 2 | The elevated half of a linked pair. |
| `KACS_ELEVATION_LIMITED` | 3 | The non-elevated half. |

## PIP tiers

A process's PSB `pip_type` field.

| Value | Meaning |
|---|---|
| 0 | None. Unprotected, the default for unsigned binaries. |
| 512 | Protected. Standard PIP protection. |
| 1024 | Isolated. Reserved; no signing key targets it. |

**There are no public constants for these.** Nothing in `uapi/pkm/` names the tiers, and `PIP_TYPE_NONE` / `_PROTECTED` / `_ISOLATED` — which older documents use — exist nowhere in the tree. Protected survives only as the kernel-private `PKM_KACS_PIP_TYPE_PROTECTED`.

A program reasoning about tiers compares the numbers. The Peios Kernel TRM §3.7 says the same.

## Token audit policy flags

A token's `audit_policy` field. These are what force the audit events in the [Events Index](/peios/using-peios/events/kernel-access-events/access-audit.md).

| Flag | Value | Meaning |
|---|---|---|
| `OBJECT_ACCESS_SUCCESS` | 0x01 | Force an audit event on every successful access. |
| `OBJECT_ACCESS_FAILURE` | 0x02 | Force an audit event on every failed access. |
| `PRIVILEGE_USE_SUCCESS` | 0x04 | Emit a `privilege-use` event when a privilege's bits survive. |
| `PRIVILEGE_USE_FAILURE` | 0x08 | Emit a `privilege-use` event when its bits are stripped. |

## Create dispositions

For `kacs_open`.

| Constant | Value | Behaviour |
|---|---|---|
| `KACS_DISPOSITION_SUPERSEDE` | 0 | If it exists, delete and recreate; otherwise create. |
| `KACS_DISPOSITION_OPEN` | 1 | If it exists, open; otherwise fail with `ENOENT`. |
| `KACS_DISPOSITION_CREATE` | 2 | If it exists, fail with `EEXIST`; otherwise create. |
| `KACS_DISPOSITION_OPEN_IF` | 3 | If it exists, open; otherwise create. |
| `KACS_DISPOSITION_OVERWRITE` | 4 | If it exists, truncate and open; otherwise fail with `ENOENT`. |
| `KACS_DISPOSITION_OVERWRITE_IF` | 5 | If it exists, truncate and open; otherwise create. |

Older documents spell these `KACS_FILE_SUPERSEDE`, `KACS_FILE_OPEN` and so on. That spelling does not exist in any header.

## SECURITY_INFORMATION flags

For `kacs_get_sd` and `kacs_set_sd`. Declared as `KACS_SECINFO_*`; the names below are the MS-DTYP spellings PCDS uses.

| Flag | Value | Right to read | Right to write |
|---|---|---|---|
| `OWNER_SECURITY_INFORMATION` | 0x01 | `READ_CONTROL` | `WRITE_OWNER` |
| `GROUP_SECURITY_INFORMATION` | 0x02 | `READ_CONTROL` | `WRITE_OWNER` |
| `DACL_SECURITY_INFORMATION` | 0x04 | `READ_CONTROL` | `WRITE_DAC` |
| `SACL_SECURITY_INFORMATION` | 0x08 | `ACCESS_SYSTEM_SECURITY` | `ACCESS_SYSTEM_SECURITY` |
| `LABEL_SECURITY_INFORMATION` | 0x10 | `READ_CONTROL` | `WRITE_OWNER`, plus the integrity rules |

`SACL_SECURITY_INFORMATION` and `LABEL_SECURITY_INFORMATION` are mutually exclusive in one call.

## Process mitigation flags

The `KACS_MIT_*` flags on the PSB. The catalogue of what each one gates is [Process mitigations](/peios/security-fundamentals/process-mitigations/catalog.md).

## Size and count limits

| Limit | Value | Context |
|---|---|---|
| Max SD size | 65,535 bytes | Any SD blob. Normative in PCDS §5.1. |
| Max ACL size | 64 KB | Any ACL within an SD |
| Max single ACE size | 64 KB | Bounded by the ACL size |
| Min SID size | 8 bytes | Revision, count and authority, no sub-authorities |
| Max SID size | 68 bytes | 15 sub-authorities |
| Max token wire spec | 64 KB | `kacs_create_token` input |
| Max session wire spec | 4096 bytes | `kacs_create_session` input |
| Max CAAP wire spec | 256 KB | `kacs_set_caap` input |
| Max CAAP rules per policy | 256 | |
| Max applies-to expression | 64 KB | Per CAAP rule |
| Max conditional stack depth | 1024 | Evaluator limit |
| Max TLP cache entries | 64 | Trusted Library Path prefixes |
| Max TLP path length | 4096 bytes | Per prefix |
| Max mount template SD | 64 KB | `kacs_set_mount_policy` template |

---

# Wire formats reference

_Peios / Using Peios / Wire Formats Reference_

> Where every byte-level layout in Peios is specified — security descriptors, SIDs, conditional-ACE bytecode, claim entries, token and session specs, CAAP, and the KMES event envelope.

Byte-level layouts are specified normatively in the PCSA books and in the kernel manual's ABI appendices. This page is the map.

## Security descriptors and their parts

| Structure | Where |
|---|---|
| Security descriptor header, self-relative | PCDS §5.1 |
| ACL header and ACE array | PCDS §5.2 |
| ACE header and per-type body layouts | PCDS §5.4 |
| Access mask layout | PCDS §5.3 |
| Claim entry — `CLAIM_SECURITY_ATTRIBUTE_RELATIVE_V1` | PCDS §5.9 |
| Conditional-ACE bytecode, with the full opcode table | PCDS §5.11 |
| Resource attribute ACEs | PCDS §5.10 |

See also [Security descriptors](/peios/using-peios/wire-formats-reference/security-descriptors.md).

## Identifiers

| Structure | Where |
|---|---|
| SID binary format | PCDS §4.1 |
| SID string format | PCDS §4.2 |
| GUID | PCDS §2 |
| LUID | PCDS §3 |

## Kernel interfaces

| Structure | Where |
|---|---|
| Token and session wire specs | Peios Kernel TRM §3.A — see [Token and session specs](/peios/using-peios/wire-formats-reference/token-and-session-specs.md) |
| CAAP wire format | See [CAAP format](/peios/using-peios/wire-formats-reference/caap-format.md) |
| Registry ABI, including watch record layout | Peios Kernel TRM §5.A |
| Token query class payloads | Peios Kernel TRM §3.A |

## Event stream

| Structure | Where |
|---|---|
| KMES event header, field by field | PSPK §2 |
| Event envelope, in summary | [Events Index](/peios/using-peios/events/all-event-types.md) §1.2 |
| Per-event payload schemas | [Events Index](/peios/using-peios/events/all-event-types.md), chapters 3 to 8 |

## Packages and repositories

The package container, manifest, signature and repository index formats are PSPU §5.

---

# Security descriptors (wire format)

_Peios / Using Peios / Wire Formats Reference_

> Where the byte-level security descriptor layouts are specified — PCDS chapters 4 and 5.

The byte-level layout of a security descriptor and everything inside it is specified in **PCDS**, the Peios Core Data Structures specification. It is normative there and is not duplicated here.

| Structure | Section |
|---|---|
| Descriptor header, self-relative form — `Revision`, `Sbz1`, `Control`, and the four offsets | PCDS §5.1 |
| Control flags | PCDS §5.1 |
| ACL header — `AclRevision`, `AclSize`, `AceCount` | PCDS §5.2 |
| ACE header — `AceType`, `AceFlags`, `AceSize` | PCDS §5.4 |
| Per-type ACE body layouts | PCDS §5.4 |
| Access mask bit layout | PCDS §5.3 |
| SID binary format | PCDS §4.1 |
| Claim entry format | PCDS §5.9 |
| Conditional-ACE bytecode | PCDS §5.11 |

Peios uses the MS-DTYP §2.4.6 self-relative format without translation, so a descriptor written by a Windows domain controller and replicated through Samba is evaluated as-is. Where KACS's *evaluator* departs from MS-DTYP — a separate question from the format — the Peios Kernel TRM §3.B has the list.

The **right values** carried in an access mask are catalogued in [Access mask bits](/peios/using-peios/constants-and-catalogs/access-mask-bits.md).

---

# CAAP format

_Peios / Using Peios / Wire Formats Reference_

> Where the central access policy wire format is specified.

The central access policy wire format — the structure `kacs_set_caap` accepts — is specified in the **Peios Kernel TRM §3.A**, the KACS ABI reference.

The conceptual model, and what the structures mean, is [Policies and rules](/peios/security-fundamentals/central-access-policies/policies-and-rules.md).

## Shape, in summary

Fields are length-prefixed: each part begins with a 32-bit byte count followed by that many bytes, and an absent field has length zero. That convention runs through the policy blob, its rules, and each rule's applies-to expression and SACL.

The limits that bound it — 256 KB per wire spec, 256 rules per policy, 64 KB per applies-to expression — are in [Other constants](/peios/using-peios/constants-and-catalogs/other-constants.md).

The applies-to expression and the rule SACLs use the same conditional-ACE bytecode as a conditional ACE, specified in PCDS §5.11.

---

# Token and session specs

_Peios / Using Peios / Wire Formats Reference_

> Where the binary token and session specifications are laid out — the Peios Kernel TRM's KACS ABI appendix.

The binary specs that `kacs_create_token` and `kacs_create_session` accept, and the payload format of every token query class, are laid out in the **Peios Kernel TRM §3.A**, the KACS ABI reference. All 46 header offsets and all 24 query classes are there.

The conceptual field list — what a token holds and what each field does — is [Token types](/peios/security-fundamentals/tokens/token-types.md).

## Shape, in summary

Both specs are length-prefixed throughout: a field is a 32-bit byte count followed by that many bytes, and an absent field has a count of zero. Arrays are a 32-bit element count followed by that many records.

Size limits are 64 KB for a token spec and 4096 bytes for a session spec; both are in [Other constants](/peios/using-peios/constants-and-catalogs/other-constants.md).

The enumerated values a spec carries — impersonation level, elevation type, logon type, mandatory policy, audit policy — are catalogued in [Other constants](/peios/using-peios/constants-and-catalogs/other-constants.md) too, under the names the headers declare.

For building a session spec from the command line rather than by hand, `logonse` types the surface for you; the underlying format is unchanged.

---

# Kernel ABI reference

_Peios / Using Peios / Kernel ABI Reference_

> Where the KACS and LCS kernel ABIs are documented — syscall numbers, ioctls, structure layouts, and constants, generated from uapi.

The kernel ABI is documented in the **Peios Kernel TRM**, in two appendices generated from `uapi/pkm/`:

| Appendix | Covers |
|---|---|
| §3.A, KACS ABI Reference | Syscall numbers, ioctls, structure layouts, token query classes, and the KACS constants |
| §5.A, LCS ABI Reference | The registry ABI — syscalls, ioctls, structure layouts, and the watch record header offsets |

Two more appendices sit alongside them: §3.B lists where the KACS evaluator departs from MS-DTYP, and §3.C lists the audit events KACS emits.

## Names

Those appendices use the names `uapi/pkm/` declares. A reader arriving with the name PCDS uses — which is MS-DTYP's — will find the mapping in §3.A's own divergence table. Impersonation levels, create dispositions, group attribute flags and `SECURITY_INFORMATION` flags all differ between the two vocabularies. See Conventions §6.5 for why neither is being renamed.

## Related

- Byte-level layouts by structure: [Wire formats reference](/peios/using-peios/wire-formats-reference/overview.md).
- Numeric constants by kind: [Constants and catalogs](/peios/using-peios/constants-and-catalogs/overview.md).
- The tracepoints the kernel exposes: [Kernel tracepoints](/peios/developing-for-peios/debugging-the-kernel/kernel-tracepoints.md).
