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:

  1. You run zp install ripgrep as yourself: zenpac plans it and shows the plan, reading the system’s store without changing it.
  2. You confirm, and zenpac runs itself again as root through run0, sudo or doas, which ask for your password once.
  3. 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: elevate in zenpac.toml (see configuration): auto takes the first of run0, sudo and doas that’s installed; name one to use only that, or never to always run zenpac as root yourself.
  • With run0, zenpac runs as root in a unit named after the command, zenpac-<command>-<pid> (description zenpac <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
  • world means the world names it, by itself or in a virtual group; auto means it’s there because something needs it (see the world). held keeps 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 (the no repo ones) 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 ETag and Last-Modified of 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. update then 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, and zp repo remove stops 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, .idx and .validators.
  • Software centers’ catalogs: for each enabled official repo, update also installs its AppStream catalog, which Discover and GNOME Software list applications from, in /var/lib/swcatalog/xml/<namespace>-<repo>.xml.gz with 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 = false in zenpac.toml turns 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 add shows 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-unsigned adds 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 managed repos.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, and repos.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 remove takes them out like any other package.
  • Putting them back (yes at the question, or --revert) plans one change, shown and confirmed as usual (-y doesn’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> --revert works 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 disable turns 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. with zp 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 -y or -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 that replaces it 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 (with chroot, which needs root). Mount /dev, /proc and /sys into 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: -y upgrades along, --allow-partial installs without upgrading, and with neither zenpac refuses with error code 10.
  • -s shows the plan without the upgrade, and the warning. With --json, the plan’s behind lists 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, and zp upgrade itself 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 make makes build-deps one entry of the world, whose members are gcc and make. The group keeps them installed. Running it again with the same name replaces the members. The name can’t be a package’s.
  • zp satisfy takes 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>… and take <file>… resolve the named files (/etc/nanorc.zpnew) without asking. --json gives 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-config handles their new versions).
  • Exit code 5 if any file is changed or gone (config files aside), 0 otherwise, so scripts can tell. --json lists every finding.
  • Running it as yourself works, but files only root may read are reported as unreadable instead 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 reinstall records 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 audit finds changed or gone (config files aside) is reinstalled in its installed version. zp fix nano reinstalls 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 upgrade instead. 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 (^, v and = in ASCII).
  • show lists 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, time is 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 recover shows what the interrupted transaction installs and removes.
  • zp recover finish carries 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 rollback puts every file back as it was before. What install scriptlets and hooks did isn’t undone, and a .zpsave copy 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 runs update once 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 zenpac system user, which the package creates with systemd-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.