Skip to content

Maintenance actions

Paul Asadoorian edited this page Aug 26, 2026 · 5 revisions

Maintenance actions

What fettle does when you type it, how to read what it prints back, and what each action actually runs on each distro family.

Reading the output

Every run ends in a Summary, and the mark in front of each line is load-bearing:

Mark Means
it happened
! it did not happen, and that may be fine — you declined a prompt, or a tool was absent
it failed

The distinction between the last two is deliberate and was added because it is genuinely ambiguous: pacman, apt and dnf all exit non-zero both when you answer "no" at their prompt and when they genuinely break. With --yes there was no prompt to decline, so a non-zero exit is a real failure and gets . Without it, fettle says what it knows and no more.

Exit status: 0 unless something reported a failure (), in which case 1. A run you declined exits 0 — you got what you asked for. This matters for cron and CI: a maintenance run whose work was blocked no longer looks like a successful one.

"Could not look" is never reported as "clean." If a check could not run — a tool missing, a repository unreachable, a query that failed — it says so rather than returning the same output as a healthy system. Several of fettle's worst bugs were exactly that confusion, and the QA plan in docs/qa/ exists to keep finding them.

The actions

Run with no action to execute the configured default set. Every action accepts three interchangeable forms — a short flag, a long flag, or a bare word — and they combine freely:

fettle -c            # short flag
fettle --clean       # long flag
fettle clean         # bare word
fettle -c -u         # combine, in the order fettle defines
fettle clean update  # identical to the line above

The table below lists the short flag and the word; the long flag is the word with -- in front of it (--clean, --orphans, --config-drift). fettle -h lists all three, in these same two groups.

· marks the default set — what fettle runs with no arguments (and fettle -a). Everything else is opt-in.

Anything a distro's backend doesn't support is skipped with a note.

Disabled (superseded) snap revisions are offered on every distro that has snapd, not just Debian — each revision confirmed individually.

Flag Action Arch Debian RHEL family
-c · clean paccache — drops packages no longer installed, keeps the last 2 versions of the rest ([clean] keep_versions); AUR build dirs (asks first; --yes skips) apt-get clean, unused flatpaks dnf clean packagesnot clean all, so repo metadata survives; unused flatpaks
-o · orphans foreign pkgs → ~/.fettle/reports/; remove true orphans (-Qtdq) — the package manager confirms the full transaction, which may exceed what you picked obsolete pkgs → ~/.fettle/reports/; orphaned libraries (deborphan, or dpkg reverse-deps where it is gone) + autoremove, same confirmation pkgs from no enabled repo (repoquery --extras) → reports; repoquery --unneeded, kernels never offered
-u / --upgrade · update mirrorlist refresh (Manjaro; [updaters.arch] refresh_mirrors), then pacman/pamac, then yay AUR (with review) apt/nala — the upgrade is skipped if the package lists could not be refreshed — then flatpak, then snap dnf upgrade --refresh, then flatpak/snap; a gpgcheck=0 repo asks once more
-O only-update refresh safely — private cache, never pacman -Sy (no partial-upgrade risk) — then report upgradable apt-get update --error-on=any + flatpak metadata, then report upgradable dnf makecache + report upgradable (check-update; exit 100 means updates exist)
-r · rebuild-check checkrebuild (rebuild with -R) + reboot check: warns if the running kernel's modules were replaced needrestart — services and the kernel state (KSTA), so a pending reboot is reported needs-restarting — reboot hint + services; only exit 0 may mean "no reboot"
-y · python-rebuild-check (arch) rebuild pkgs stranded on an old /usr/lib/python3.X (skips Python interpreters themselves; flags orphaned dirs) — (apt handles transitions) — (dnf handles transitions)
-d · config-drift .pacnew (yours still live) vs .pacorig (yours displaced) and .pacsave .dpkg-dist/.ucf-dist (yours live) vs .dpkg-old/.ucf-old (yours displaced) + dpkg --audit .rpmnew (yours still live) vs .rpmsave/.rpmorig (yours displaced) + dnf check
-x · auto-updates report enabled auto-update timers (known units) report unattended-upgrades state (apt-config + apt-daily-upgrade.timer) dnf-automaticall four timers, since -install applies updates even with apply_updates = no; warns if the host reboots itself
all three also check the timer is actually succeeding — enabled but failing every night is reported, not counted as ON
-f · firmware fwupdmgr (shared) — verdict from fwupd's exit code, so a dead daemon reads as UNKNOWN, not "up to date" same same
-k kernel mhwd-kernel (running series protected; removal is user-named) dpkg -l 'linux-image-*', purge old (running AND newest protected; apt confirms its own transaction; nudges to reboot) informational only — dnf enforces installonly_limit itself, so nothing is offered for removal; flags a pending reboot
-C container-update pull container images — every installed runtime (docker and podman), asking per image; images built here are never offered ([containers]) same same

Audit & security actions

All read-only — none of these changes the system. Only -P runs under -a; the rest are opt-in. -S is the odd one out and the deepest: it scans firmware, boot and hardware rather than packages, and elevates itself.

Flag Action Arch Debian RHEL family
-S sys-audit firmware/boot/hardware security scan — Secure Boot, TPM, microcode, IOMMU, SPI/BIOS, storage firmware; self-elevates same same
-P · pkg-audit package supply-chain audit → ~/.fettle/reports/ apt/flatpak/snap provenance dnf/yum repo provenance + flatpak/snap/containers/extensions
-V pkg-integrity paccheck --sha256sum against pacman's MTREE (falls back to pacman -Qkk) debsums against the .md5sums dpkg installed (falls back to dpkg --verify) rpm -Va against the rpmdb's file digests
-A aur-audit (arch) AUR health table → ~/.fettle/reports/
-H hardening-audit is this system hardened? seven axes — build flags (needs checksec), filesystem, services, kernel, sshd, firewall, TLS certificates → ~/.fettle/reports/ same, via dpkg-buildflags baseline same, via rpm's %{build_cflags} macros; binaries attributed with rpm -qf
-p aur-precheck (arch) per-package pre-install check (RPC + IoC); bare = every installed AUR pkg
-U upgrade-check (experimental) AI pre-upgrade safety check; needs ANTHROPIC_API_KEY same same
-M compromise-check is something ALREADY here? boot persistence, loader/kernel integrity, running processes, boot chain. Reports anomalies to investigate, never a fix same same
advisory-check installed packages with known CVEs (fix available, or no fix yet) same same

update asks before upgrading (the package manager shows its plan and prompts); pass --yes to skip the confirmation and run non-interactively.

Mirror refresh before upgrading — Arch family (-u)

On Manjaro, fettle -u regenerates /etc/pacman.d/mirrorlist before it upgrades anything. That is a change to system configuration, so it is worth knowing about — and as of 0.54.0 it is configurable.

It runs pacman-mirrors -f, which probes mirrors and rewrites the list in speed order. This is on by default, because a mirror that has fallen behind serves an old package database and the upgrade then resolves against versions that mirror no longer holds — a real and recurring cause of failed upgrades, not a theoretical one.

[updaters.arch]
refresh_mirrors = true    # default — regenerate the mirrorlist before upgrading
# refresh_mirrors = false # never touch the mirrorlist
# refresh_mirrors = 5     # rank the fastest 5 mirrors only

Consider setting a number. Bare pacman-mirrors -f is not a moderate default: its argument is optional and defaults to "no limit", so every upgrade speed-tests every mirror it knows about. refresh_mirrors = 5 keeps the protection and bounds the cost.

Vanilla Arch and EndeavourOS have no equivalent wired up. pacman-mirrors is Manjaro-only; when the setting is on and the tool is absent, fettle says so and points at reflector rather than skipping in silence. Nothing is done to your mirrorlist there.

No other distribution needs this. Fedora resolves mirrors through metalink, the RHEL family through a mirrorlist service, and Debian/Ubuntu through a CDN or apt's own failover — in each case the server picks, per request, so there is no local file to regenerate.

Checking for updates (-O)

fettle -O answers "what is waiting for me?" and changes nothing else. It refreshes repo metadata, then prints the transaction an upgrade would perform — including new dependencies, not just version bumps — and stops.

It is not a prerequisite for update. Every backend already refreshes as part of upgrading (pacman -Syuu, apt-get update before the upgrade, dnf upgrade --refresh), so -O is the standalone look, not a step you must run first.

If the refresh fails, it says so and exits non-zero. A mirror can be down, a key can expire, a laptop can be on a train. In that case the last-known list is still printed — it is useful — but marked (from stale metadata), because newly published updates, including security fixes, would not appear in it. Note that apt-get update exits 0 even when it reached no repository at all, which is why fettle passes --error-on=any on apt 2.1+.

On Arch and Manjaro the system database is never synced. pacman -Sy without a full upgrade is the classic partial-upgrade footgun, so the preview is resolved against a private temporary database (the checkupdates technique) and /var/lib/pacman/sync is left untouched. It also honours IgnorePkg, so packages you have pinned are not reported.

An upgrade will not run from lists it could not refresh (-u)

On Debian and Ubuntu, fettle -u refreshes the package lists and then checks whether that worked before upgrading anything. If it did not, the upgrade is skipped and the run exits non-zero:

✗ update: upgrade SKIPPED — the package lists could not be refreshed, and upgrading from
  stale lists would report success while installing nothing (run `sudo apt-get update` to
  see which repository failed)

This exists because the failure is otherwise invisible. apt-get update exits 0 when it could not reach a single repository, and full-upgrade then finds nothing to install and exits 0 as well — so a host that has been cut off from its repositories for months reports a clean, successful upgrade every night. Measured on Debian 12 with every repository pointed at an unreachable host, that is exactly what happened.

fettle passes --error-on=any (apt 2.1+, probed rather than assumed) so apt reports the failure honestly. nala update already exits non-zero unaided and needs no flag.

One unreachable repository is enough to stop the upgrade. This is deliberate — the lists are incomplete, and fettle cannot know whether the repository it could not reach was holding a security update — but it does mean a host carrying a long-dead third-party repository will refuse to upgrade until that repository is fixed or removed. The message names the command to run to find out which one it was.

Extras stop too. flatpak and snap are not updated after a system upgrade that did not complete, and the run says so rather than going quiet.

Is the patch actually in effect? (-r)

fettle -r answers the question that matters after an upgrade: is the new code actually running, or is something still on the old version? An update you have installed but not activated is not an update. It is in the default action set for that reason.

It reports two different things:

  • A pending reboot. On Debian/Ubuntu from needrestart's kernel state, on RHEL from needs-restarting -r, and on Arch/Manjaro by noticing that the running kernel's module directory has been replaced — at which point that kernel can no longer load any module it has not already loaded, so a USB device plugged in after the upgrade simply will not work.
  • Services still running old libraries, which need restarting but not a reboot.

It never restarts or reboots anything itself; it tells you and stops. On Arch, -R will offer to rebuild packages built against since-upgraded libraries.

If the check cannot run, it says so. A missing or failing checkrebuild, empty needrestart output, or a dnf4 host without yum-utils are all reported as "not determined" rather than as a clean result — the distinction matters most here, because "nothing to do" is exactly what a broken check looks like.

Three AUR checks, and which to reach for

Three actions look at AUR packages. The clearest way to tell them apart is when you run them, not what they query — they share most of their queries on purpose.

-P pkg-audit -A aur-audit -p aur-precheck
when routine, after the fact after the fact before an install
scope every ecosystem: AUR, apt, flatpak, snap, containers, editor + shell extensions AUR only the package names you give it
output findings, for reading a census table CRIT/WARN lines, for a hook to parse
not in the AUR any more
orphaned / flagged out-of-date / stale
on a known-malicious package list
maintained by a known-malicious account
malicious JS dependency trace
an IoC feed could not be read
maintainer changed since last run
votes, reverse dependents, removal candidates
in the default -a set

Which one do I want?

  • -P — the routine one, and the only one in the default set. Everything below, plus every other install channel on the box, plus the one thing no other view has: it tells you when an IoC feed could not be read, so a quiet result is never mistaken for a clean one.
  • -A"what should I clean up or stop trusting?" The full census: age, votes, maintainer, and the reverse-dependency analysis that finds AUR packages nothing on the system needs any more. Only -A tells you what is safe to remove.
  • -p PKG … — the gate, not an audit: it runs before a package is built, and its output is a line contract for the yay hook and for -u's pre-upgrade check. Run it by hand when you are about to install something and want a second opinion.

Bare fettle -p (no package names) points the gate at everything already installed. It works, and it says so when you run it — but -P reports the same facts and more.

-I / aur-ioc-scan was retired in v0.73.0. Every check it performed is in -P, across every ecosystem. It had already been dropped from the default set because running both fetched the AUR RPC and the IoC feeds twice and reported each finding twice; retiring the flag finished that. fettle -I now tells you where the capability went. The feed-coverage reporting that the retired -I uniquely had — "the scan matched nothing, but the lists were never read" — moved into -P as part of the retirement, because losing it would have reintroduced the exact bug its QA sweep fixed.

They keep separate maintainer-change baselines. Sharing one meant whichever action ran first consumed the difference and rewrote the file, so a maintainer takeover was reported once and was invisible to the other — the exact signal all three exist to catch.

Did an upgrade change your config? (-d)

Package managers leave a file behind whenever an upgrade meets a config file you had edited — but which file they leave tells you two very different things, and fettle -d now says which:

What happened Arch Debian RHEL
New default shipped; your file is still in effect .pacnew .dpkg-dist, .ucf-dist .rpmnew
Your file was moved aside — the package's version is in effect now .pacorig .dpkg-old, .ucf-old .rpmsave, .rpmorig
Your file kept after the package was removed .pacsave

The middle row is the one worth waking up for: a setting you deliberately made has silently stopped applying. Those are reported as warnings and counted separately:

✓ 4 config file(s) to review — 1 where YOUR version is no longer in effect

The scan walks /etc on every distro. On Arch that is deliberate rather than delegating to pacdiff, which only reports leftovers whose base file still exists — a .pacsave is created when a package is removed, so it has no base file and pacdiff never mentions it. pacdiff and rpmconf are still suggested as the tools to merge with.

Removing orphans (-o)

fettle -o lists packages nothing depends on any more and offers to remove them, one at a time. Two things worth knowing before you say yes:

The real transaction can be larger than your selection. Removing an orphan also removes dependencies that orphan was the last thing needing — choosing one package can remove several. fettle therefore lets the package manager show and confirm its own transaction, so the full set is a decision point rather than a surprise. Declining there removes nothing.

The count reported is what actually went, measured from the installed set before and after, and it names anything removed beyond what you picked:

✓ 2 package(s) removed (including 1 unused dependency(ies): lua54)

keep_orphans in the config protects packages from ever being offered, and they are named when held back. On RHEL, kernels are never offered at all — and if the query that identifies them fails, nothing is offered rather than guessing.

--yes means "delete them" here. For automation that is the point; it is also the one action where an unattended run removes software. --dry-run first if in doubt.

Cache cleaning (-c)

fettle -c reclaims disk from downloaded package files — the copies your package manager keeps after installing. It never removes installed software, and it always asks first (--yes skips the prompt, --dry-run shows what would run and changes nothing).

The summary states what actually happened, measured from the cache directory rather than taken from the package manager's word for it:

✓ caches cleaned — 39.2 MiB reclaimed
✓ caches already clean — nothing to reclaim
✓ would clean caches                        # --dry-run

Arch / Manjaro / EndeavourOS. The cache is also your offline rollback path: a bad upgrade is undone with pacman -U /var/cache/pacman/pkg/<older>.pkg.tar.zst, which only works while that file is still there. So cleaning is split by rollback value —

  1. cached packages no longer installed at all are removed outright (no rollback value);
  2. superseded versions of installed packages are trimmed to the last keep_versions (default 2), so every installed package keeps a working rollback target plus a spare.

A stopped snapd no longer hangs the run. If the snap binary is installed but the daemon is not answering, fettle says so and moves on:

! snapd is installed but not responding (the service is stopped or wedged) — snaps were
  NOT audited; `systemctl start snapd.socket` if you use snaps, or remove the snapd
  package if you do not

This matters more than it sounds on Arch and Manjaro, where snapd ships disabled by default. Install it, never enable the socket, and every snap command blocks forever — including the read-only inventory clean runs. Before v1.16.0 fettle inherited that hang outright: -c, -P and -a never returned, --dry-run included, because a read-only query deliberately bypasses the dry-run gate.

Checking whether /run/snapd.socket exists does not answer the question: the socket file survives the daemon, so it is present on exactly the host where snap does not work. fettle asks snap something cheap under a short clock instead, once per run.

It will refuse to clean while a transaction is running. /var/lib/pacman/db.lck is pacman's transaction lock, and if it is held, some other pacman or AUR helper is mid-write. fettle names the process holding it and stops:

✗ clean REFUSED — a transaction is running (pacman, pid 4821)

fettle never deletes that file. Until v1.8.0 it did, unconditionally, on every -c — which is the documented way to corrupt a package database, because removing the lock does not stop the process holding it, it just lets a second writer in alongside the first. A stale lock (no process holds it) is reported with the rm command to run, and the clean proceeds; the deletion stays your decision, because only you know whether the machine crashed mid-upgrade. If the holder cannot be identified — the lock exists but /proc is not readable — that is also a refusal, because "I could not tell" is not "it is safe".

Both use paccache from pacman-contrib. Without it, fettle falls back to pacman -Sc, which removes only packages that are no longer installed — correct, less thorough, no extra dependency. AUR helper build directories (~/.cache/yay, ~/.cache/paru, ~/.cache/pamac, and pamac's /var/tmp build tree) are removed too; this is the only family where the prompt mentions build directories, because it is the only one that has any.

pacman -Scc is deliberately not used. With --noconfirm it removes nothing at all (its prompt defaults to No), and answering yes would delete the cached copy of every installed package — destroying offline rollback to reclaim a little more disk.

Debian / Ubuntu / Mint / Pop!_OS. apt-get clean empties /var/cache/apt/archives. Package lists under /var/lib/apt/lists are untouched, so no apt update is forced afterwards. apt-get autoclean is not run: clean has already emptied the directory, so it would have nothing to consider. Unused flatpak runtimes are removed when flatpak is present.

RHEL / CentOS Stream / Rocky / AlmaLinux / Oracle. dnf clean packages, deliberately not clean all. Measured on a RHEL 10.1 host, /var/cache/dnf held 796 MB of which 736 MB was .rpm files and 60 MB was repo metadata — clean packages frees the 736 MB, while clean all would also discard the metadata and force a slow re-download on the very next dnf command. Note these systems ship keepcache=0, so a large RPM cache usually means an interrupted transaction, which is exactly when reclaiming it helps. Both dnf generations are handled: dnf5 caches under /var/cache/libdnf5.

There is no version-retention knob on apt or dnf — neither keeps a version history to trim, so keep_versions applies to the Arch family only.

Configuration (see the full config example):

[clean]
keep_versions = 2   # Arch family: cached versions kept per INSTALLED package.
                    # 0 keeps none — frees the most, leaves no offline rollback.

Packages you no longer have installed are removed regardless of this setting; retention only ever protects things you could actually roll back to.

Three more flags are shortcuts to subcommands (not part of the action pipeline): -Ssys-audit --all (security scan), -Uupgrade-check (AI advisor), -paur-precheck (AUR pre-flight; bare = scan all installed). Use the subcommand form for their own options.

Default set (run when you pass no action, or -a/--all): clean, orphans, update, rebuild-check, python-rebuild-check, config-drift, auto-updates, firmware-check, and — last, read-only — pkg-audit (-P), so a full run also reports where your packages came from and whether any of them matches a known-compromise feed. -I was retired in v0.73.0: -P already runs all three of its checks (see Three AUR checks). Excluded from the default set — request explicitly: -O, -k, -A, -H.

--everything — one run, everything safe to leave running

--everything runs every action that is safe start-to-finish without supervision, in an order chosen so each one describes the state the previous left behind. It works locally and over fettle remote.

fettle --everything            # or: fettle remote myhost --everything --yes

It adds what -a leaves out — pkg-integrity, hardening-audit, aur-audit, and the two that were previously only reachable as their own subcommands, sys-audit and advisory-check, plus compromise-check. Actions your distro does not have are filtered out and reported, so the same command is correct everywhere; there is no per-distro list to maintain.

The order is not alphabetical, and not the order you would type them. clean runs first, because it frees space before the upgrade needs it — which is the whole point on a box with a small /var. It costs nothing to do first: clean removes cached packages that are no longer installed and trims the rest to keep_versions, while the upgrade downloads new versions that were never in the cache.

update follows, so everything downstream describes the system you will actually be running rather than the one you booted. The two rebuild checks come next because they exist to catch what the update just made stale. orphans and config-drift follow because an upgrade is what creates both — packages left with no dependents, and .pacnew/.rpmnew files waiting to be merged. pkg-integrity runs after the update or it would verify packages about to be replaced. And advisory-check runs last, where it reports what is still unfixed rather than listing everything the update was about to fix anyway. compromise-check runs after even that — the final question, and the one an update cannot answer: patching removes a vulnerable package, it does not remove an implant that is already running.

The remaining audits are genuinely order-independent. pkg-audit and aur-audit are kept adjacent only because both query the AUR RPC and the second benefits from the first's TTL-cached fetch.

One caveat if you have set keep_versions = 0: cleaning first then removes every cached version including the one you are currently running, so an upgrade that goes wrong leaves you no offline rollback. The default of 2 keeps the running version cached.

Two actions are deliberately absent. kernel can remove your ability to boot — it is kept out of the default set for that reason and stays out here — and container-update pulls images over the network. Both are one flag away. --only-update is redundant once update runs.

Its exit status answers "did the run complete", not "is the machine clean", and that is a deliberate difference from a single action. Fourteen checks on a real host will essentially always find something — advisory-check alone reported 142 fix-available on the machine this was developed on — and a status that is red every single time is one nobody reads. Findings are in the summary. If you want to gate automation on a specific condition, run that action on its own: fettle -V still exits non-zero when a packaged file's contents have changed.

It is not quick. On a workstation with ~4,400 binaries the hardening audit alone is minutes, and sys-audit runs chipsec. The step counter tells you where it is.

auto-updates (-x) is a read-only, informational report of whether the system is set up to update itself unattended — on Debian/Ubuntu whether unattended-upgrades is installed and its apt-daily-upgrade.timer / APT::Periodic knobs are on; on Arch whether a known auto-updater systemd timer (e.g. arch-update.timer, pacman-auto-update.timer) is enabled. It states the fact and offers no opinion; a custom-named Arch timer won't be recognized.

-R / --auto-rebuild turns the -r / -y checks from "list" into "offer to rebuild". Destructive steps (orphan/kernel removal, disabled-snap pruning) always prompt per item unless you pass --yes.

When fettle hands off to a package manager (yay/pacman/apt), it brackets that tool's live output in a labeled banner (──── yay ──── output below is yay's, not fettle's ────) so you can always tell fettle's messages from the tool's.

Previewing an upgrade

fettle -u --dry-run resolves and lists every package the upgrade would install — version upgrades, the new dependencies they pull in, and any removals — grouped by source, before it prints the commands it would run. It changes nothing and needs no sudo.

▸ [1/1] Updating packages
  14 package(s) would be installed/changed:
    official repos (12):
      linux              6.12.1-1 -> 6.12.4-1
      systemd            257.2-1  -> 257.3-1
      + libfoo           2.0-1                 (new dependency)
      - obsolete-lib     1.2-3                 (remove)
      …
    AUR (2):
      brave-bin          1:1.92.134-1 -> 1:1.92.138-1
  would run: pacman -Syuu
  …

On Arch this uses checkupdates' trick — a throwaway package DB synced in /tmp via fakeroot — so the preview reflects fresh mirror data without touching your system DB or needing root (pacman-contrib + fakeroot recommended; it degrades to cached data with a note otherwise). On Debian/Ubuntu it's apt's native apt-get -s dist-upgrade simulation. Pass --no-sync to skip the refresh and preview against the cached data (faster; may be stale). AUR -git/-devel packages that rebuild from source may not show a version bump until yay fetches them — noted in the output.

On the RHEL family the preview is deliberately partial by default, and says so: dnf's full resolver (dnf upgrade --assumeno) refuses to run without root, and there is no rootless apt-get -s equivalent, so a plain --dry-run lists upgrades and reports that new dependencies and removals are missing. --full-preview elevates to resolve the real transaction — on a live RHEL 10.1 box that is 345 packages against the 337 the rootless query can see. -O already runs as root, so it gets the complete set without the flag.

Clone this wiki locally