pacman removes packages. pacrid removes the mess they leave behind.
When you uninstall a package, pacman deletes its binaries - but your configuration files, caches, and data directories are intentionally left alone. Over time these accumulate invisibly: ~/.config/steam, ~/.cache/discord, ~/.local/share/kiwix-desktop. pacrid fixes this with a pacman PostTransaction hook that automatically finds and removes orphaned files every time you uninstall a package, with no manual steps required.
curl -sSf https://raw.githubusercontent.com/ParkerrDev/pacrid/refs/heads/master/install.sh | bash -s -- --binaryArch Linux only. Works transparently with
pacman,paru,yay, and any pacman wrapper. Prefer to build it yourself? Drop the-s -- --binaryand the installer will compile from source.
How it works
pacrid hooks into pacman's PostTransaction event. Every time a package is removed, pacrid receives the package names on stdin, runs its scanner pipeline against your home directory, and acts on what it finds — all before pacman exits.
Two scanners run for each removed package:
1. XDG database scanner (xdg_db)
At build time, pacrid downloads and compiles the xdg-unused-data community database into a perfect hash map (PHF). This database contains known leftover paths for hundreds of applications, keyed by executable name. When a package is removed, pacrid checks the database for that package's known data paths and evaluates each one.
Additional vendored entries live in data/apps/ for packages not yet upstream (e.g. steam).
2. Name heuristic scanner (name_heuristic)
For packages not in the database, pacrid generates candidate filesystem paths from the package name and its common variants (e.g. kiwix-desktop → kiwix_desktop, kiwixdesktop) and probes the standard XDG directories:
| Path pattern | Category |
|---|---|
~/.config/<name> |
Config |
~/.cache/<name> |
Cache |
~/.local/share/<name> |
Data |
~/.local/state/<name> |
State |
~/.<name> and ~/.<name>rc |
Config (legacy dot-files) |
/etc/<name>, /etc/<name>.conf, /etc/<name>.d |
Config (system) |
/var/cache/<name>, /var/log/<name> |
Cache (system) |
/var/lib/<name> |
State (system, always Low confidence) |
Before scanning any paths for a package, pacrid checks whether the package's binary is still present on PATH using the which crate. If the executable is still found — e.g. a Flatpak or AppImage shadow-installs the same app — all findings for that package are suppressed entirely. pacrid will never touch files for software that is still actually installed by another means.
Every finding is scored before being acted on:
| Confidence | Conditions |
|---|---|
| High | XDG database entry + exe confirmed gone, OR exact name match + exe confirmed gone + path is under ~/.config, ~/.cache, ~/.local/share, or ~/.local/state |
| Medium | Exact name match alone, OR pacman orphan scan found it in /etc/, OR XDG database entry without exe confirmation |
| Low | Substring match only, path under /var/lib/, item > 1 GB, or anything else |
When you run pacrid clean <pkg> interactively, all findings are shown in a checkbox UI pre-selected at your configured threshold.
pacman captures a hook's stdout and stderr so it can fold them into its own log, so neither is a terminal while the hook runs. pacrid opens /dev/tty directly and points both streams at it for the duration of the review, which means the hook prompts you — the same checkbox UI as pacrid clean, in the middle of the pacman transaction:
:: Running post-transaction hooks...
(4/6) pacrid: scanning for leftover files
:: kilo: 3 paths left behind (52.84 MiB)
-> 3 paths pre-selected (52.84 MiB) at the 'high' threshold
:: Remove?
> [x] High ~/.config/kilo 48.49 MiB name_match + exe_gone
[x] High ~/.local/share/kilo 2.37 MiB name_match + exe_gone
[x] High ~/.cache/kilo 1.98 MiB name_match + exe_gone
[↑/↓ move · space toggle · → all · ← none · enter confirm · esc skip]
:: pacrid: removed 3 paths, 52.84 MiB reclaimed
-> restore with: pacrid undo
The columns are sized to your terminal; on a narrow one the evidence column drops before the path is ever squeezed. Confidence then only decides what is pre-checked: High items are ticked by default, anything below your auto_confirm threshold is listed unticked and one spacebar away.
This matters most for large directories. Anything over 1 GB is forced to Low confidence no matter how strong the evidence, because getting a 1.4 GB deletion wrong is expensive — but it is still shown to you rather than silently skipped.
If there is no controlling terminal — a cron job, a GUI package manager, an unattended script — there is nobody to ask, so pacrid falls back to auto-confirming at your configured threshold and never blocks the transaction.
Automation that runs pacman with a terminal attached but no human watching it (CI, provisioning) can't be told apart by that check, so there are two ways to force the fallback: hook_prompt = false in the config for a whole machine, or PACRID_NO_PROMPT=1 in the environment for a single invocation. Esc or Ctrl-C at the prompt means "remove nothing" and the transaction continues normally.
The pacman hook runs as root, which means $HOME is /root. pacrid works around this by detecting the real user's home from (in order):
SUDO_USERenvironment variable (sudo)DOAS_USERenvironment variable (doas)PKEXEC_UIDenvironment variable (polkit)- All UID ≥ 1000 entries from
/etc/passwd(multi-user fallback)
Deleted files are never permanently erased by default. pacrid uses two safe-removal modes:
- XDG Trash (
~/.local/share/Trash) — used for user home paths when running interactively. Files restored via your file manager. - Quarantine (
/var/lib/pacrid/quarantine/) — used when running as root (the hook) or when trash fails. Restored viapacrid undo.
Every deletion is written to the journal at /var/lib/pacrid/journal/<timestamp>.json before any file is moved. If pacrid crashes mid-deletion, the journal is intact and pacrid undo can restore whatever was moved.
Installation
Downloads a prebuilt binary from the latest GitHub Release, verifies its SHA256, and installs the pacman hook. No Rust toolchain required.
curl -sSf https://raw.githubusercontent.com/ParkerrDev/pacrid/refs/heads/master/install.sh | bash -s -- --binaryThe script will:
- Verify you are on Arch Linux
- Detect your architecture (
x86_64oraarch64) and pick the matching release tarball - Download it from
releases/latest/download/pacrid-<arch>-linux.tar.gz - Verify the published SHA256 checksum
- Install the binary to
/usr/bin/pacrid - Install the pacman hook to
/usr/share/libalpm/hooks/pacrid.hook - Create
/var/lib/pacrid/journal/
Drop the --binary flag to compile locally instead. The script will install Rust via rustup if needed.
curl -sSf https://raw.githubusercontent.com/ParkerrDev/pacrid/refs/heads/master/install.sh | bash# Prerequisites: rust (stable ≥ 1.80), git
git clone https://github.com/ParkerrDev/pacrid
cd pacrid
cargo build --release
sudo install -Dm755 target/release/pacrid /usr/bin/pacrid
sudo install -Dm644 hooks/pacrid.hook /usr/share/libalpm/hooks/pacrid.hook
sudo mkdir -p /var/lib/pacrid/journalsudo rm /usr/bin/pacrid /usr/share/libalpm/hooks/pacrid.hook
sudo rm -rf /var/lib/pacrid # removes quarantine + journal
rm -rf ~/.config/pacrid # removes user config (optional)| Requirement | Notes |
|---|---|
| Arch Linux (or derivative) | Requires pacman and libalpm hooks |
| Rust ≥ 1.80 | Build-time only; not needed at runtime |
| Internet access at build time | build.rs fetches the xdg-unused-data database |
CLI reference
| Flag | Description |
|---|---|
--dry-run |
Show what would be removed without deleting anything |
--non-interactive |
Skip prompts; auto-confirm at the configured threshold |
--auto-confirm <LEVEL> |
Override threshold: high, medium, low, none |
--purge |
Permanently delete instead of trashing/quarantining |
--json |
Machine-readable JSON output |
-v / -vv |
Increase verbosity (debug / trace) |
-q |
Suppress non-error output |
Manually scan and remove leftovers for one or more packages. Presents an interactive checkbox UI unless --non-interactive is passed.
pacrid clean steam
pacrid clean discord slack --dry-run
pacrid clean kiwix-desktop --auto-confirm mediumSystem-wide orphan scan using pacman's file database. Flags files not owned by any installed package. Slow — not run on the hook path.
pacrid sweep
pacrid sweep --root /opt --root /srvList (pacman -Qdt) or remove orphan dependency packages.
pacrid orphans # list
pacrid orphans --remove # remove via sudo pacman -RnsRestore files from the last (or a specific) journal entry.
pacrid undo # restore most recent batch
pacrid undo 2026-05-24T21-38-27Z # restore a specific entryFiles moved to XDG Trash can only be restored via your file manager. Files in quarantine are restored by rename(2) — atomic, no data loss.
Permanently delete everything in the quarantine directory. Run this once you are confident you no longer need to undo past removals.
pacrid empty --dry-run # preview
pacrid emptyShow a log of all past pacrid actions with timestamps, package names, path counts, and sizes.
Debug: show what the XDG database knows about a package. Useful when adding new data/apps/ entries.
pacrid db check steam
pacrid db check discordConfiguration
pacrid looks for a config file at ~/.config/pacrid/config.toml. A system-wide default can be placed at /etc/pacrid/config.toml; the user config takes precedence on every field.
No config file is required. The defaults are safe for most users.
# Auto-confirm level for the pacman hook (non-interactive).
# "high" — only remove High-confidence findings automatically (default)
# "medium" — also remove Medium-confidence findings automatically
# "low" — remove everything automatically (not recommended)
# "none" — never remove anything automatically
auto_confirm = "high"
# Move files to XDG Trash instead of quarantine when running interactively.
# The hook always uses quarantine (runs as root, can't write user trash).
use_trash = true
# Enable/disable the pacman PostTransaction hook entirely.
hook_enabled = true
# Let the hook prompt on the controlling terminal instead of silently
# auto-confirming at the threshold above. Falls back to auto-confirm on its
# own when no terminal is attached; set false to never prompt from the hook.
hook_prompt = true
# Remove orphan dependency packages automatically after each transaction.
auto_remove_orphan_deps = false
# Extra filesystem roots to scan for leftover files.
scan_paths_extra = ["/opt/myapp-data"]
[scanners]
# XDG database scanner — looks up packages in the community database.
xdg_db = true
# Name heuristic scanner — probes standard XDG dirs by package name.
name_heuristic = true
# Full pacman orphan scan — slow, off by default (only used by `pacrid sweep`).
pacman_orphan = false
[ignore]
# Packages whose leftovers should never be touched.
packages = ["wine", "proton"]
# Specific paths that should never be removed regardless of confidence.
paths = [
"/home/user/.config/shared-app",
]- High — pacrid is very confident this is a leftover. Auto-removed by the hook. Example:
~/.config/steamwhensteamis not on PATH and the XDG database confirms it. - Medium — probably a leftover, but worth a human glance. Example:
/etc/myapp(system config that might be shared or hand-edited). - Low — suspicious but risky to auto-remove. Example:
/var/lib/myapp(state data that might be shared between packages).
Set auto_confirm = "medium" to make pacrid more aggressive. Set auto_confirm = "none" to approve every removal manually via pacrid clean <pkg>.
Architecture
pacrid is a single Rust binary with a library core, built with Cargo. The codebase is deliberately flat — no async runtime, no global state.
pacrid/
├── build.rs # Fetches xdg-unused-data at build time, codegens PHF map
├── data/apps/ # Vendored XDG entries (e.g. steam.json)
├── hooks/pacrid.hook # Pacman PostTransaction hook definition
├── install.sh # One-line installer script
└── src/
├── main.rs # CLI entry point (clap derive)
├── lib.rs # Library root + global lint config
├── config.rs # TOML config deserialization + defaults
├── confidence.rs # Scoring: (reasons × path × size) → Confidence
├── exec_check.rs # Executable presence check via `which`
├── executor.rs # Safe deletion: validate → journal → move
├── hook.rs # Pacman hook entry point; per-home scanning loop
├── journal.rs # JSON undo journal (write-before-delete)
├── review.rs # Interactive checkbox UI (inquire::MultiSelect)
├── tty.rs # Redirects stdout/stderr to /dev/tty so the hook can prompt
├── ui.rs # pacman-style output: `::` headers, columns, ~ paths
├── user_homes.rs # Real user home detection when running as root
├── util.rs # format_bytes, compute_size, expand_xdg_with_home
├── pacman/
│ ├── db.rs # /var/lib/pacman/local/*/files parser
│ ├── owns.rs # Ownership check: is this path pacman-owned?
│ └── query.rs # pacman -Qdt and related queries
└── scanners/
├── mod.rs # Finding, Confidence, Reason, ScanContext, Scanner trait
├── xdg_db.rs # XDG database scanner (include! generated PHF map)
├── name_heuristic.rs # Name-based filesystem probe scanner
├── pacman_orphan.rs # System-wide unowned-file scanner (sweep command)
└── orphan_deps.rs # Orphan dependency package lister
pacman remove event
│
▼
hook.rs::run_hook()
│ reads package names from stdin
│ detects real user home(s) via user_homes.rs
│ (SUDO_USER → DOAS_USER → PKEXEC_UID → /etc/passwd UID≥1000)
│
├──► XdgDbScanner.scan()
│ looks up pkg in compiled PHF map (zero I/O)
│ expands $HOME/$XDG_* vars using ctx.home_dir
│ checks executable_gone() → suppresses all findings if exe present
│
└──► NameHeuristicScanner.scan()
generates name variants (foo, foo-bar, foo_bar, foobar)
probes XDG dirs + dot-files + /etc + /var/cache + /var/lib
includes ExecutableGone reason when exe is confirmed absent
│
▼
confidence::score(reasons, path, size, home_dir)
│
▼
interactive_review()
auto_select() in non-interactive/hook mode
MultiSelect checkbox UI in interactive mode
│
▼
executor::execute()
validate_path() — refuses /, /usr, /home (bare), pacman-owned, ..
compute_size() before moving
write journal entry BEFORE touching any file
trash::delete() → quarantine fallback if trash unavailable
build.rs runs before compilation and:
- Clones
xdg-unused-datainto$OUT_DIR/xdg-data/(shallow clone) - Reads additional JSON entries from
data/apps/ - Parses each entry into
XdgEntry { executables, locations } - Writes
$OUT_DIR/xdg_db.rscontaining aphf::Map<&str, XdgEntry>keyed by executable name src/scanners/xdg_db.rsbrings this in viainclude!(concat!(env!("OUT_DIR"), "/xdg_db.rs"))
The result is zero-cost, zero-allocation lookups at runtime — the entire app database is a baked-in perfect hash map with no I/O on the hot path.
- Never delete pacman-owned paths.
executor::validate_path()checksPacmanDb::owns()before any deletion. - Never operate on bare top-level paths. A hardcoded
FORBIDDEN_PREFIXESlist rejects/,/home,/etc,/usr,/var,/boot, etc. - Never follow symlinks. All probes use
symlink_metadata(). Quarantine usesrename(2). - Never delete if the exe is still present. The executable gate runs before any path is probed.
- Never auto-remove items > 1 GB. Anything over 1 GB is forced to Low confidence.
- Write the journal before moving any files. If pacrid crashes mid-deletion, the journal is still intact for
undo. - Never exit non-zero from a hook. Hook errors are logged but never propagate to pacman.
- Never recurse.
PACRID_IN_HOOK=1is set before any child process is spawned. - Never read
$HOMEfrom environment in scanners.ctx.home_diris always the detected real user home. - Never touch
/var/lib/paths at High confidence. State paths are forced to Low regardless of other signals.
| Crate | Purpose |
|---|---|
clap |
CLI argument parsing (derive macros) |
phf / phf_codegen |
Compile-time perfect hash map for the XDG database |
trash |
XDG Trash spec implementation |
which |
Executable presence check via PATH |
dialoguer |
Interactive checkbox UI (MultiSelect) |
walkdir |
Symlink-safe recursive directory traversal |
chrono |
Timestamp generation for journal IDs |
serde / serde_json / toml |
Config and journal serialization |
tracing / tracing-subscriber |
Structured logging with level filtering |
anyhow / thiserror |
Ergonomic error propagation |
libc |
isatty(3) for TTY detection |
humansize |
Human-readable byte sizes |
tempfile |
Isolated temporary directories in tests |
Contributing
Contributions are welcome. The hard requirement: cargo clippy -- -D warnings must pass clean on every commit.
git clone https://github.com/ParkerrDev/pacrid
cd pacrid
cargo build # debug build
cargo test # all tests
cargo clippy -- -D warnings # must be clean
cargo build --release # release buildThe most impactful contribution is adding a data/apps/<appname>.json entry for a package that leaves files behind:
{
"name": "MyApp",
"executables": ["myapp", "myapp-helper"],
"locations": [
{"file": "$HOME/.myapp"},
{"file": "$XDG_CONFIG_HOME/myapp"},
{"file": "$XDG_CACHE_HOME/myapp"},
{"file": "$XDG_DATA_HOME/myapp"}
]
}Supported path variables: $HOME, $XDG_CONFIG_HOME, $XDG_CACHE_HOME, $XDG_DATA_HOME, $XDG_STATE_HOME.
Verify it compiled in correctly:
cargo build
pacrid db check myappConsider also submitting the entry upstream to xdg-unused-data so all tools using that database benefit.
- Fork the repository on GitHub.
- Create a branch:
git checkout -b feat/add-discord-entryorfix/describe-the-fix. - Make focused commits — one logical change per commit, no unrelated cleanup bundled in.
- Run the full check suite before pushing:
cargo test cargo clippy -- -D warnings cargo build --release - Open a PR against
mainwith a description that explains:- What problem this solves or what it adds
- How you tested it (especially for scanner changes — what package, what leftover path)
- Any edge cases you considered
- No descriptive comments. Well-named identifiers describe what the code does. Comments explain why: hidden constraints, invariants, workarounds for specific bugs.
- No clippy suppressions without a justification comment.
- No
unwrap()on the hot path. Use?andanyhow::Context. - Asserts document invariants, not recoverable errors.
assert!(!reasons.is_empty())is correct;assert!(file.exists())is not. - The hook must never panic or return non-zero. Wrap risky hook code in
std::panic::catch_unwind. - Never read
$HOMEin scanner code. Always usectx.home_dir.
cargo test confidence # unit tests for the scoring function
cargo test integration_steam # end-to-end: fake $HOME, all six steam paths
cargo test -- --nocapture # show println! output during tests
cargo test -vv # verbose test outputReporting bugs and getting help
Please include:
- The package name(s) involved
- The output of
pacrid clean <pkg> -vv --dry-run - The contents of
~/.config/pacrid/config.tomlif you have one - The output of
pacrid db check <pkg>to show what the XDG database knows
Common issues:
| Symptom | Likely cause |
|---|---|
| Hook fires but finds nothing | The executable is still on PATH (Flatpak/AppImage). Run which <pkg> to check. |
| Hook finds things but removes nothing | auto_confirm is "none" or the findings are below the threshold. Run pacrid clean <pkg> interactively. |
| pacrid removed something it shouldn't have | Run pacrid undo immediately. Then open an issue with details. |
| Build fails | Check that you have internet access (build.rs fetches xdg-unused-data). Run cargo build -vv for details. |
Open an issue at: https://github.com/ParkerrDev/pacrid/issues
License
pacrid is released under the GNU General Public License v3.0 or later.
You are free to use, modify, and distribute this software under the terms of the GPL-3.0.
The xdg-unused-data database is fetched at build time under its own license.