Handbook
Commands
zenpac is also installed as zp. Both names do exactly the same.
| Command | What it does |
|---|---|
zp update |
Downloads the DBs of the enabled repos that changed, and rebuilds their indexes; installs the official repos’ AppStream catalogs for software centers |
zp search <terms>… |
Packages whose name or description contains every term (ignoring case). Exact names first, then names starting with a term, then other matches |
zp show <name> (alias info) |
The details of the package zenpac would install |
zp list [--upgradable|--world|--auto|--foreign|--held] [--repo <repo>] |
The installed packages, or one kind of them (see listing) |
zp policy <name> |
Every enabled repo’s version of a package, and which one is selected |
zp depends <name> |
Each dependency, and what satisfies it: a package of that name or one that provides it |
zp rdepends <name> |
The packages that depend on a package, by name or on anything it provides |
zp repo list |
Every configured repo: enabled or not, priority, signatures, how many packages its index holds, and when it was updated |
zp repo enable <repo>, zp repo disable <repo> |
Switch a repo on or off (see configuration) |
zp repo add <repo> [--source <source>] [--fingerprint <fp> | --allow-unsigned] |
Add a repo a source publishes, after checking its namespace’s key with you (see adding repos) |
zp repo remove <repo> [--revert] [-y] |
Remove a user repo; what it installed stays, or goes back as without it (see removing repos) |
zp repo search [<words>…] [--source <source>] |
The published repos whose name or description contains every word (see finding repos) |
zp install <spec>… [--virtual <name>] [-y] [-s] (alias add) |
Install packages and what they need, and add them to the world (see installing) |
zp upgrade [-y] [-s] (alias full-upgrade) |
Upgrade every package to its newest version (see upgrading and removing) |
zp remove <name>… [-y] [-s] (alias del) |
Take packages or virtual groups out of the world, and with them what nothing needs any more |
zp reinstall <name>… [-y] [-s] |
Install packages again in their installed version, to put back lost or damaged files (see reinstalling) |
zp mark manual|auto|hold|unhold <name>… |
Put installed packages into the world or take them out, or hold them at their version. No package changes (see the world) |
zp stats |
What the system holds: installed packages (count, size, files), the history, the store, the cache, and each repo’s index with when it was updated |
zp edit-sources [<file>] |
Edit a config file in your editor, saved only if the configuration it makes is valid (see configuration) |
zp config show |
The configuration in effect, as TOML, with where each value comes from (see configuration) |
zp world [show|export|apply <file>] |
Show the world, print it as a world file, or make a world file the world (see the world) |
zp fix [<name>…] [-y] [-s] [--allow-partial] |
Install what the world needs and misses, and reinstall damaged packages (see repairing) |
zp audit [<name>…] (alias verify) |
Check that the installed packages’ files are as they were installed (see auditing) |
zp diff-config [show|keep <file>…|take <file>…] |
Review the config files upgrades and removals kept (see reviewing kept config files) |
zp history [list|show <n>] |
The finished changes, oldest first, or what one did (see history) |
zp recover [finish|rollback] |
Show an interrupted transaction, or finish it or roll it back (see after an interruption) |
zp satisfy <depend>… [--virtual <name>] [-y] [-s] |
Make dependency expressions true, e.g. a recipe’s build dependencies (see build dependencies) |
zp fetch <name>… [-O <dir>] (alias download) |
Download the package files into a directory (default: the current one), checked like every download |
zp cache clean |
Remove every downloaded package from /var/cache/zenpac/pkg |
zp cache prune [--keep <n>] |
Remove the package versions the enabled repos no longer offer, keeping the n newest others of each package |
zp keys list |
Every pinned key: its state, user IDs and the repos that pin it |
zp keys trust <namespace> [<fingerprint>] |
Show the keys a namespace publishes, or trust one of them (see signatures and trust) |
zp keys refresh |
Download the pinned keys again, for new signatures and revocations |
zp keys revoke <namespace> |
Stop trusting a namespace’s keys |
zp completion <shell> |
Print the completion script for bash, zsh, fish, elvish or powershell, for the name it’s called by (zenpac or zp). The package installs them |
Official repos always rank above user repos. A user repo can’t replace an official package, even with a newer version:
$ zp policy nano
✔ zen/core nano 9.3-1 selected
masterfix/tools (user repo) nano 99-1
Running as yourself
Queries never need root. A change does, on the running system, and zenpac gets it itself, after you’ve seen the plan:
- You run
zp install ripgrepas yourself: zenpac plans it and shows the plan, reading the system’s store without changing it. - You confirm, and zenpac runs itself again as root through
run0,sudoordoas, which ask for your password once. - As root, zenpac plans again from the current state. If the plan is the one you confirmed, it carries it out without asking again; if something changed meanwhile (another update, another zenpac), it shows the new plan and asks.
Your answers (which provider, whether to upgrade along) go along. Commands
without a plan (update, mark, recover, diff-config, cache clean, …) run as root from the start. -s never needs root.
- Which program:
elevateinzenpac.toml(see configuration):autotakes the first ofrun0,sudoanddoasthat’s installed; name one to use only that, orneverto always run zenpac as root yourself. - With
run0, zenpac runs as root in a unit named after the command,zenpac-<command>-<pid>(descriptionzenpac <command>), and that’s the name the password dialog shows: “Authentication is required to start transient unit ‘zenpac-upgrade-4321.service’”. The text around it is systemd’s. - Without a terminal there’s nowhere to ask for the password, so a change fails with error code 11 and a hint: run it as root (scripts, image builds).
- Roots you own (
--root ~/img) need no root at all: zenpac writes them as you, without file owners.
Options
The options for every command (--root, -o, --color, --json), the
arguments of each command and the exit codes are in the generated
CLI reference.
Output for scripts
The text output is for people and may change between versions. Scripts use
--json. Every document has schema_version (today 1) and command, and
an incompatible change bumps the version. With --json, errors are JSON too:
{"error": {"code", "message"}}, with a stable numeric code, plus causes
for a request that can’t be met and conflicts (one line per file) for
clashing files. Warnings always
go to stderr, so stdout stays parseable.
Listing
zp list shows the installed packages: each one’s version (and the newer
one a repo has), the repo it came from, and what it is:
$ zp list
glibc 2.42-1 → 2.43-1 zen/core auto
nano 9.3-1 masterfix/tools (user repo) world, held
tool 1-1 local file world, no repo
worldmeans the world names it, by itself or in a virtual group;automeans it’s there because something needs it (see the world).heldkeeps its version.no repo: no enabled repo has a package of its name, e.g. one installed from a file, or from a repo that was removed.- One kind:
--upgradable,--world,--auto,--foreign(theno repoones) or--held, one at a time;--repo <namespace>/<repo>keeps only those installed from that repo, alone or with one of them. - An empty list is a success (exit code 0).
Updating
zp update refreshes every enabled repo at once:
$ zp update
✔ zen/core updated, 2248 packages
✔ masterfix/tools (user repo) unchanged
- Only what changed is downloaded. zenpac remembers the
ETagandLast-Modifiedof each repo’s DB and asks the host whether it changed. An unchanged repo costs one small request and no parsing. - Signed repos are checked before their new DB is used (see signatures and trust).
- A failing repo doesn’t stop the others. It’s reported, and its previous
index stays in use.
updatethen exits with code 3. So does a repo whose DB the host doesn’t have (missing on the host), e.g. because it was removed. For a user repo, a hint says so: what it installed stays installed, andzp repo removestops asking for it (see removing repos). - Where the files go: each repo’s DB, its index and the validators sit
in
/var/lib/zenpac/repos/, named<namespace>-<repo>-<arch>with the endings.db,.idxand.validators. - Software centers’ catalogs: for each enabled official repo,
updatealso installs its AppStream catalog, which Discover and GNOME Software list applications from, in/var/lib/swcatalog/xml/<namespace>-<repo>.xml.gzwith its icons in/var/lib/swcatalog/icons/<namespace>-<repo>/(ADR 0021). It’s asked for conditionally too, and removed when the repo is disabled or removed. A catalog that can’t be downloaded or doesn’t pass zenpac’s checks is reported (AppStream catalog not installed) and doesn’t count as a failed repo: the one from before stays. User repos’ catalogs aren’t installed yet.appstream = falseinzenpac.tomlturns this off, e.g. on a server.
Finding repos
zp repo search looks through the repos a source publishes, by default
the official one (repos.zenlinux.org). Every word must appear in the
repo’s name, its description or its namespace’s description, ignoring
case; without words it lists them all:
$ zp repo search cli
✔ added masterfix/tools (user repo) CLI tools
other/cli (user repo) A shell toolkit
- What it reads: the source’s list of namespaces,
<base>/index.json(spec §8.1). It’s kept in/var/cache/zenpac/discovery/and asked for again only conditionally, so repeated searches cost one small request. - Offline: if the host can’t be reached, the kept copy is used, with a warning.
- Nothing here is trusted. The list is only for finding repos: user
repos are marked
(user repo)whatever the list says, and keys are never taken from it (see signatures and trust).
Adding repos
zp repo add <namespace>/<repo> adds a user repo from its source, by
default the official one:
$ zp repo add masterfix/tools
Repo masterfix/tools (user repo)
Description CLI tools
Owner Master Fix
Packages 12
Updated 2026-10-26T09:00:00Z
Arches x86_64
Key 0123 4567 89AB CDEF 0123 4567 89AB CDEF 0123 4567
Trust this key for masterfix and add masterfix/tools? [y/N] y
✔ masterfix/tools added (written to /etc/zenpac/repos.d/masterfix.toml)
hint: download it with: zp update
- What it reads: the namespace’s index,
<namespace>/index.json(spec §8.2), which says whether the repo exists, what it is, who owns it, and which key signs it. - Trust on first use: check the key’s fingerprint with the namespace’s
owner another way before you say yes; the answer is no by default.
zenpac then downloads
<namespace>/key.asc, takes only the certificate with that fingerprint, if it can sign, and pins it for the repo (see signatures and trust). - Without a terminal (or with
--json),repo addshows the repo and how to decide, and adds nothing. Decide on the command line instead:--fingerprint <fp>takes the key with that fingerprint, if it’s the one the namespace announces;--allow-unsignedadds a repo of a namespace that doesn’t sign, which only HTTPS protects then. A namespace that signs can’t be added unsigned. - Refused: a repo that’s configured already, one in an official
namespace (
zen,zenlinux: those are built in), and one without a DB for the system’s architecture. - Where it’s written:
/etc/zenpac/repos.d/<namespace>.toml, or, if that’s a file of your own, the managedrepos.d/99-zenpac.toml(see configuration). Adding needs root, which zenpac gets after you said yes.
Removing repos
zp repo remove <namespace>/<repo> removes a user repo. If packages came
from it, a terminal asks whether to put them back as without the repo:
$ zp repo remove masterfix/tools
✔ masterfix/tools removed (from /etc/zenpac/repos.d/masterfix.toml)
⚠ 2 installed packages came from it and stay installed: mytool, nano
Put them back as without the repo? (other repos' versions, the rest out) [y/N] y
Downgrade (1)
↓ nano 9.4-1 → 9.3-1 zen/core
Remove (1)
- mytool 1-1 masterfix/tools 1.0 KiB no longer needed
2 packages · download 251 KiB · -1.0 KiB on disk
Continue? [Y/n]
✔ 2 packages: 1 downgraded, 1 removed
- What goes: its entries in the files zenpac writes
(
repos.d/<namespace>.toml, deleted once it’s empty, andrepos.d/99-zenpac.toml), its DB and index in/var/lib/zenpac/repos/, and, if it was its namespace’s last repo, the namespace’s stored keys. - What stays: the packages installed from it, unless you put them
back (no by default). No repo updates them any more;
zp removetakes them out like any other package. - Putting them back (yes at the question, or
--revert) plans one change, shown and confirmed as usual (-ydoesn’t ask):- a package another enabled repo has goes to that repo’s version: a downgrade, an upgrade, or a reinstall of the same version, so that it comes from there;
- the others leave the world, and go unless something still needs them (those stay, still from the removed repo);
- world entries pinned to the repo (
nano@masterfix/tools) lose the pin; - held packages stay as they are.
- Later: without a terminal, and with no, the packages stay, and a hint
says how to put them back:
zp repo remove <repo> --revertworks for a repo removed before, as long as something installed came from it. There’s no-s: removing the repo can’t be simulated. - Refused: an official repo (built in:
zp repo disableturns it off), and a repo that a file of your own configures too. zenpac doesn’t change your files, so the message names the file to remove it from, e.g. withzp edit-sources.
Installing
zp install adds what you name to the world, plans what that needs, shows
the plan, and installs it:
$ zp install nano
New (2)
+ glibc 2.42-1 zen/core dependency of nano
+ nano 9.3-1 zen/core
2 packages · download 1.2 MiB · +5.1 MiB on disk
Continue? [Y/n]
✔ 2 packages installed
- What you can name: a package (
nano), a version constraint ('nano>=9'), a package from a given repo (nano@masterfix/tools), or a package file (./nano-9.3-1-x86_64.pkg.tar.zst). An argument is a file if it starts with/,./or../, or ends like a package file. - Package files are installed as they are, even if a repo has a newer
version, and the world gets their name. Their dependencies come from the
repos. The plan shows them as
local file, and the store records them without an origin. No repo lists them, so there’s no checksum to check them against, and signatures on package files aren’t checked yet. - Several providers: when more than one package could provide what’s
needed (e.g.
sh) and none is installed yet, zenpac asks which one, on a terminal, numbering them in priority order. Enter keeps the first. Without a terminal, with-yor-s, the first is taken. With--json, the plan lists these choices. Your choice stays: as long as the provider is installed, no later install or upgrade swaps it for another one (only a package thatreplacesit takes its place). - The question comes only on a terminal.
-y(--yes) skips it, and without a terminal (scripts, image builds) there’s none. Answering no exits with code 4.-s(--simulate) only shows the plan. - Nothing changes until everything is checked: every package is downloaded and verified, unpacked into staging, and checked for file conflicts first (see transactions). Then hooks and install scriptlets run around the install, and the store records the packages and the new world in one commit.
- Shown live: on a terminal, each phase (unpacking, removing,
installing) has one status line on stderr with the packages done, a bar
and the current package, and becomes a single
✔line when it’s done. Hooks and scriptlets are listed as they start, with their output once they finish. Warnings come at the end. Without a terminal (pipes, logs,--json), there’s no status line, and hooks and scriptlets are listed at the end with the warnings (ui.progress, see configuration). - Into another root: with
--root <dir>, zenpac installs into that directory, and hooks and scriptlets run inside it (withchroot, which needs root). Mount/dev,/procand/sysinto it first if the packages’ scriptlets need them, as image tools do. The whole process is in bootstrapping a root. - Asking for what’s there: naming a package that’s installed only as a dependency installs nothing, but puts it into the world, so it stays.
- Upgrades along the way: if a new package needs a newer version of an installed one, the plan upgrades it too, and shows it.
- One at a time: while one zenpac changes the system, another that
needs the store fails within half a second with error code 12, naming
the process:
local store: in use by process 4242 (zp upgrade). Several can read it together. Queries of the repos (search,show, …) don’t need the store at all.
Partial upgrades
Zen is a rolling release: after zp update, the repos may have newer
versions of what’s installed. Installing a package then without upgrading
the rest is a partial upgrade: the new package (or a library it pulls
in) may be built against newer versions than the system has, and break.
So when a change installs or upgrades anything while installed packages
have newer versions it doesn’t take, zenpac stops and says which:
$ zp install ripgrep
⚠ warning: 2 installed packages have newer versions (glibc, zlib); installing without upgrading them is a partial upgrade
Upgrade the system first? [Y/n]
- Yes (Enter) plans the full upgrade together with the change, as one transaction, and shows that plan.
- No installs without upgrading, if you know it’s safe.
- Without a terminal, there’s no question:
-yupgrades along,--allow-partialinstalls without upgrading, and with neither zenpac refuses with error code 10. -sshows the plan without the upgrade, and the warning. With--json, the plan’sbehindlists what a full upgrade would upgrade that the change doesn’t.- Held packages (
zp mark hold) don’t count: they stay behind on purpose. Removing packages, reinstalling, andzp upgradeitself are never partial upgrades.
The guard covers install, satisfy and world apply.
Build dependencies
Build tools need packages for a while and then want them gone together. Two things help:
- Virtual groups:
zp install --virtual build-deps gcc makemakesbuild-depsone entry of the world, whose members aregccandmake. The group keeps them installed. Running it again with the same name replaces the members. The name can’t be a package’s. zp satisfytakes dependency expressions as recipes write them ('cmake>=3.20',sh): what the installed packages meet already changes nothing, and the rest is installed and joins the world. With--virtual, every expression becomes a member of the group, met or not, so the group keeps all of them:
$ zp satisfy --virtual build-deps 'cmake>=3.20' ninja sh
zp remove build-deps takes the group out of the world, and with it every
package only the group needed.
Upgrading and removing
zp upgrade is always a full upgrade: every package goes to the newest
version the repos offer, and a package another one replaces gives way to
it. The world stays as it is, and so do the providers you chose. zp remove <name> takes entries out of the
world (packages or virtual groups), and every package nothing needs any
more goes with them. A package something else still needs stays installed.
Both show the plan and ask first, like install (-y, -s). Upgrades run
the packages’ upgrade scriptlets and hooks, removals their removal
scriptlets and hooks.
Changed config files are kept. Files a package marks as config
(backup) may have been edited by you, so zenpac never overwrites or
deletes them silently:
- an upgrade replaces a config file you didn’t change;
- if you changed it and the package didn’t, yours stays as it is;
- if you both changed it, yours stays, and the package’s new version goes
next to it as
<file>.zpnew, for you to merge (zenpac says so); - when the package no longer has it, or is removed, a file you changed is
kept as
<file>.zpsave.
Reviewing kept config files
zp diff-config finds the .zpnew and .zpsave files and shows each as
a diff from your file to the other version. On a terminal, it asks what to
do with each one:
$ zp diff-config
/etc/nanorc.zpnew: the package's new version of /etc/nanorc, which you changed
--- /etc/nanorc (yours)
+++ /etc/nanorc.zpnew
@@ -1,3 +1,3 @@
set autoindent
-set mouse
+# set mouse
set tabsize 4
[k]eep yours, [u]se the new version, or [s]kip? k
✔ kept your /etc/nanorc, removed /etc/nanorc.zpnew
- A
.zpnew: keep yours (the new version goes), or use the new version (it replaces yours, whose changes are then gone: merge what you want to keep first). - A
.zpsave: restore it (back to its old name), or delete it. - Skipping (Enter) leaves both as they are, for later.
- Without a terminal, or with
zp diff-config show, it only shows them.zp diff-config keep <file>…andtake <file>…resolve the named files (/etc/nanorc.zpnew) without asking.--jsongives each file with its diff as data. - Where it looks: in
/etc, where Zen’s packages keep their config, and beside every protected file of the installed packages elsewhere. The files themselves are the record: delete one by hand, and it’s gone from the review too.
Auditing
zp audit checks every installed package’s files against what zenpac
recorded when it installed them (zp audit nano checks only nano):
$ zp audit
nano 9.3-1
✖ changed /usr/bin/nano
✖ mode /usr/bin/rnano 0755 → 0700
⚠ config /etc/nanorc changed
✖ 214 packages audited: 1 damaged, 1 changed config file
- What’s checked: each file is there and of the same kind, with the same size and SHA-256, mode, and owner (when zenpac installed it as root); a symlink leads to the same target, and a hard link is still the same file as its target. Directories only have to be there, as a directory or a link to one: packages share them, and one that existed keeps its mode.
- Config files you changed are listed with
⚠, not counted as damage: they’re yours to change (zp diff-confighandles their new versions). - Exit code 5 if any file is changed or gone (config files aside), 0
otherwise, so scripts can tell.
--jsonlists every finding. - Running it as yourself works, but files only root may read are
reported as
unreadableinstead of checked. - Packages an earlier zenpac installed, before it recorded each file
(store schema 5), have only their paths recorded, so only those are
checked;
zp reinstallrecords them in full. - Reading every file takes a while on a full system: it hashes what’s installed.
Repairing
zp fix repairs what’s broken, in one plan, shown and asked like
install:
- Missing dependencies: the world is solved again without upgrading, so a package it needs that isn’t installed is installed (and one nothing needs goes).
- Damaged packages: every package whose files
zp auditfinds changed or gone (config files aside) is reinstalled in its installed version.zp fix nanoreinstalls nano whether it’s damaged or not, instead of auditing everything. - Limits: a package is reinstalled from a repo that still has its
version; once the repos have moved on,
zp upgradeinstead. Since it may install packages, the partial-upgrade guard applies. Without names it audits every package first, which takes a while.
Reinstalling
zp reinstall <name> installs a package again in the version that’s
installed, from a repo that still has it (the one it came from first), so
files that were deleted or damaged come back. Nothing else changes: not
the other packages, not the world. It runs the package’s upgrade
scriptlets and hooks, as an upgrade to the same version, and config files
you changed stay as they are.
$ zp reinstall nano
Reinstall (1)
↻ nano 9.3-1 zen/core 2.1 MiB
1 package · download 2.1 MiB · +0 B on disk
Continue? [Y/n]
✔ 1 package: 1 reinstalled
Once the repos have a newer version, the installed one can’t be
reinstalled: zp upgrade first.
History
Every change of what’s installed or of the world goes into the history, in the same commit as the change itself, so it has exactly the changes that finished:
$ zp history
History (3)
1 2026-10-26 09:12 UTC +214 install base
2 2026-10-26 14:02 UTC +1 ↑2 install ripgrep
3 2026-10-26 14:05 UTC the world changed mark hold glibc
$ zp history show 2
Transaction 2, 2026-10-26 14:02 UTC: install ripgrep
↑ glibc 2.42-1 → 2.43-1 zen/core
+ ripgrep 14.1-1 zen/core
↑ zlib 1.3-1 → 1.3.1-1 zen/core
World (1)
+ ripgrep
- The list counts each kind of change:
+installed,↑upgraded,↓downgraded,↻reinstalled,-removed (^,vand=in ASCII). showlists every package with its versions and repo, and the world entries that came (+), went (-) or changed (~, e.g. now held).- The command is what was typed after
zp, quoted where a shell needs it. - Times are in UTC. With
--json,timeis in seconds since 1970. zp history show <n>for a number the history doesn’t have exits with 1.- Undoing a change from the history comes later.
After an interruption
If zenpac stops while it changes the system (a crash, a power loss, a full disk), nothing is lost: before it changes any file, it records what it’s about to do, and it keeps the files it replaces or removes until the end. The next change then refuses to start:
$ zp install htop
[x] error: an interrupted transaction is pending
hint: finish it with: zp recover finish, or undo it with: zp recover rollback
zp recovershows what the interrupted transaction installs and removes.zp recover finishcarries on where it stopped and ends it as if nothing had happened. The step that was under way runs again, its install scriptlet included.zp recover rollbackputs every file back as it was before. What install scriptlets and hooks did isn’t undone, and a.zpsavecopy of a changed config file stays.
Downloads
Every package download, by fetch and by install, works the same way:
- Checked: against the SHA-256 in the repo DB, and for a signed repo
against the package’s
.sig(see signatures and trust). A file that fails is deleted. - Reused: a file that’s already there with the right SHA-256 isn’t downloaded again.
- Resumed: an interrupted download continues where it stopped.
- Shown live: on a terminal, one status line on stderr shows the files
done, a bar, the bytes and the current file. Pipes and logs don’t get it
(
ui.progress, see configuration). - Stale DBs: if the host doesn’t have a package any more (it was
replaced after your last
update), zenpac runsupdateonce and takes the repo’s current version instead. - Separated from root: downloads (packages, repo DBs, signatures,
keys) run in a separate zenpac process, confined to writing its
downloads. As root it runs as the
zenpacsystem user, which the package creates withsystemd-sysusers; without that user, zenpac warns that downloads run as root. Proxy settings (http_proxy,https_proxy,all_proxy,no_proxy, lowercase or uppercase) reach it; other environment variables don’t. Where the kernel refuses its seccomp filter, as emulated x86_64 containers on Apple Silicon do, it downloads without the filter, and zenpac warns. See ADR 0011 and ADR 0013.
The cache sits in /var/cache/zenpac/pkg, and the download worker’s
unfinished files in /var/cache/zenpac/spool (cache clean empties both). cache prune keeps the versions
the repos offer, so a later install needn’t download them again.
For tests without a server, zp debug ingest <repo> <file.db> indexes a
repo DB file directly.
From zenpac’s documentation: docs/user/commands.md at v0.9.5.