Track which packages create files in your home directory. Uses eBPF to monitor file operations in real-time, so when you uninstall a package you can find and clean up everything it left behind.
Linux applications scatter files across ~/.cache, ~/.local, ~/.config, /etc/, and other directories. When you uninstall a package, these files remain. The files and folders frequently don't match the package names, making manual cleanup tedious and error-prone.
HDAS attaches eBPF programs to the kernel's openat and openat2 syscalls to trace file operations in real-time with minimal overhead. Each file access is resolved to its originating package through process tree walking and package manager queries. The result is a database mapping every tracked file to the package that created it — queryable, cleanable, and exportable as JSON.
- eBPF-based monitoring — Kernel-level file access tracking with negligible overhead
- Process tree walking — Attributes files from child processes (browser threads, worker pools) to their parent package
- Creator vs accessor tracking — Distinguishes which package created a file from which last accessed it
- Multi-distro support — Auto-detects pacman, portage, dpkg, rpm, xbps, and apk at runtime
- Beyond the package manager — Attributes Flatpak, Snap, Nix, Guix, AppImage, and home-directory binaries (
flatpak:org.mozilla.firefox,nix:hello,local:rg); runs on NixOS and Guix System with no native package manager - Configurable directories — Monitor home dotdirs and absolute paths like
/etc/with per-directory depth settings - Orphan detection — Find files from packages that are no longer installed
- Safe cleanup — Delete files by package with dry-run and confirmation prompts, including symlink-aware deletion
- JSON output —
--jsonflag on all query and cleanup commands for scripting - Colored terminal output — Automatically disabled when piped
- Shell completions — Bash, Zsh, Fish, Elvish, PowerShell
- Man page generation — Built-in via
hdas man-page
- Linux kernel 5.8+ with eBPF support
- A supported package manager (pacman, portage, dpkg, rpm, xbps, or apk), or a Nix/Guix store
clangandlibbpffor building- Root privileges for monitoring
# Stable release
yay -S hdas
# Or latest git
yay -S hdas-gitInstall build dependencies (Rust via your distro or rustup):
| Distro | Command |
|---|---|
| Arch | sudo pacman -S clang libbpf rust |
| Debian / Ubuntu | sudo apt install clang libbpf-dev pkg-config |
| Fedora | sudo dnf install clang libbpf-devel |
| openSUSE | sudo zypper install clang libbpf-devel |
| Gentoo | sudo emerge dev-libs/libbpf llvm-core/clang |
| Void | sudo xbps-install clang libbpf-devel |
| Alpine | sudo apk add clang libbpf-dev linux-headers |
git clone https://github.com/adelmonte/hdas.git
cd hdas
cargo build --release
sudo install -Dm755 target/release/hdas /usr/bin/hdasThen install the service for your init system (below).
# Completions (pick your shell)
hdas completions bash > /usr/share/bash-completion/completions/hdas
hdas completions zsh > /usr/share/zsh/site-functions/_hdas
hdas completions fish > ~/.config/fish/completions/hdas.fish
# Man page
hdas man-page > /usr/share/man/man1/hdas.1The monitor runs as root and watches one user's home directory, given by hdas monitor --user NAME or the HDAS_USER environment variable. Ready-made service files for each init system are in init/; install the one you use. The monitor mounts tracefs itself if the init system hasn't, so no extra mount service is needed.
systemd
sudo install -Dm644 hdas@.service /usr/lib/systemd/system/hdas@.service
sudo systemctl enable --now hdas@YOUR_USERNAME
sudo journalctl -u hdas@YOUR_USERNAME -f # logsdinit
sudo install -Dm644 init/dinit/hdas /etc/dinit.d/hdas
sudo install -Dm644 init/hdas.default /etc/default/hdas # then set HDAS_USER
sudo dinitctl enable hdas
sudo dinitctl catlog hdas # startup messages and errors (events not logged, via --quiet)OpenRC (Gentoo, Alpine, Artix)
sudo install -Dm755 init/openrc/hdas /etc/init.d/hdas
sudo install -Dm644 init/openrc/hdas.confd /etc/conf.d/hdas # then set HDAS_USER
sudo rc-update add hdas default
sudo rc-service hdas start
# logs: /var/log/hdas.log (events aren't logged individually, via --quiet)runit (Void, Artix)
sudo cp -r init/runit/hdas /etc/sv/hdas # then set HDAS_USER in /etc/sv/hdas/conf
sudo ln -s /etc/sv/hdas /var/service/ # Artix: /run/runit/service/
# logs: /var/log/hdas/currents6-rc (Artix)
sudo cp -r init/s6/hdas /etc/s6/adminsv/hdas
sudo install -Dm644 init/hdas.default /etc/default/hdas # then set HDAS_USER
sudo touch /etc/s6/adminsv/default/contents.d/hdas
sudo s6-db-reload && sudo s6-rc -u change hdasCheck it's running with hdas status, which works under any init system.
# List all tracked files
hdas list
# Show files created by a specific package
hdas package firefox
# Show files under a directory
hdas dir ~/.cache
hdas dir /etc/
# Search files by path pattern
hdas query mozilla
# Find files from uninstalled packages
hdas orphans# Delete files created by a package (with confirmation)
hdas clean firefox
# Dry-run — show what would be deleted
hdas clean firefox -n
# Skip confirmation
hdas clean firefox -f
# Delete all files from uninstalled packages
hdas clean-orphans
# Remove stale database records:
# - files that no longer exist on disk
# - records under excluded_paths
# - records from ignored_packages
hdas prune# Service, database, and config overview
hdas status
# Detailed database and configuration statistics
hdas stats
# See how a path gets tracked (depth truncation)
hdas explain ~/.cache/mozilla/firefox/something# Show current configuration
hdas config
# Create default config file
hdas config init
# Edit in $EDITOR
hdas config edit
# Check for errors and warnings
hdas config validateAll query and cleanup commands support --json for scripting:
hdas list --json
hdas stats --json
hdas orphans --json
hdas status --json
hdas clean firefox -n --json
hdas config validate --json# Start the eBPF monitor (requires root); watches the invoking user's home
sudo hdas monitor
# Watch a specific user, without per-event output (as services do)
sudo hdas monitor --user alice --quietOutput indicators:
[+]Direct match — process owns the file[^]Parent match — attributed via ancestor process[~]Ignored process — accessor only, doesn't overwrite creator
Location: ~/.config/hdas/config.toml
Important: In TOML, all top-level keys must appear before any
[[array]]sections. Place all settings above the[[monitored_dirs]]entries.
# Processes that don't overwrite creator attribution
ignored_processes = [
"nvim", "vim", "code", # editors
"cat", "bat", "less", # pagers
"ls", "find", "rg", # file tools
"bash", "zsh", "fish", # shells
]
# Packages to skip entirely — events are never recorded, and
# existing records are retroactively pruned from the database.
# A trailing '*' matches by prefix, e.g. "flatpak:*" or "local:*"
ignored_packages = []
# Paths to exclude from monitoring even if under a monitored_dir
# Existing records under these paths are retroactively pruned.
# New configs are seeded with shared infrastructure dirs (no single
# package owns them, so attributing — and cleaning — them is wrong):
# ~/.config/kdedefaults, ~/.config/dconf, ~/.config/pulse,
# ~/.cache/fontconfig, ~/.cache/mesa_shader_cache,
# ~/.cache/mesa_shader_cache_db, ~/.cache/thumbnails
# Edit freely; add your own (e.g. noisy cert reads):
excluded_paths = [
"/etc/ssl/", # TLS cert reads — very noisy
"/etc/ca-certificates/", # same as above
]
# Default depth for dirs without explicit depth setting
# 1 = app dir (e.g., ~/.cache/mozilla)
# 0 = track full paths (useful for /etc/)
# Note: ~/.local/share, ~/.local/state, ~/.local/lib automatically add +1 depth
tracking_depth = 1
# Auto-remove stale records (deleted files, excluded paths,
# ignored packages) from DB on queries
auto_prune = true
# Package manager: "auto" (from /etc/os-release, then installed binaries),
# or force one of "pacman", "portage", "dpkg", "rpm", "xbps", "apk"
package_manager = "auto"
# Directories to monitor with per-directory depth settings
#
# Depth controls how much of the path is kept after the monitored dir:
# depth=1: ~/.cache/mozilla/firefox/... -> ~/.cache/mozilla
# depth=2: ~/.cache/mozilla/firefox/... -> ~/.cache/mozilla/firefox
# depth=0: full path, no truncation
#
# Use `hdas explain <path>` to see how a path would be tracked.
[[monitored_dirs]]
path = ".cache"
[[monitored_dirs]]
path = ".local"
[[monitored_dirs]]
path = ".config"
[[monitored_dirs]]
path = "/etc/"
depth = 0 # track full paths$ sudo pacman -R discord
$ hdas package discord
Files created by discord (3 total):
Feb 18 22:11 [✓] /home/user/.cache/discord
Feb 18 22:11 [✓] /home/user/.config/discord
Feb 18 22:11 [✓] /home/user/.local/share/discord
$ hdas clean discord
Will delete 3 file(s), 0 director(ies), 0 symlink(s) [142.8M]:
[142.5M] [dir ] /home/user/.cache/discord
[ 256.0K] [dir ] /home/user/.config/discord
[ 64.0K] [dir ] /home/user/.local/share/discord
Proceed? [y/N]: y$ hdas orphans
Files from uninstalled packages:
signal-desktop (2 file(s)):
/home/user/.config/Signal
/home/user/.cache/Signal
telegram-desktop (1 file(s)):
/home/user/.config/telegram
$ hdas clean-orphans$ hdas query mozilla
Found 4 file(s) matching 'mozilla':
Mar 01 14:22 [✓] /home/user/.cache/mozilla
Mar 01 14:22 [✓] /home/user/.config/mozilla
Feb 28 09:15 [✗] /home/user/.local/share/mozilla (deleted)# Get all orphaned packages as JSON
hdas orphans --json | jq '.[].package'
# Total size of files from a package
hdas clean firefox -n --json | jq '.total_size'
# Pipe package file list into other tools
hdas package steam --json | jq -r '.[].path'# In ~/.config/hdas/config.toml — place ABOVE [[monitored_dirs]]
# Skip recording events from noisy packages entirely
ignored_packages = ["electron", "nwjs"]
# Exclude high-traffic paths that clutter the database
excluded_paths = [
"/etc/ssl/",
"/etc/ca-certificates/",
"/etc/ananicy.d/",
]Existing records from ignored packages and excluded paths are automatically
cleaned up by hdas prune or on the next query when auto_prune = true.
HDAS attaches eBPF programs to the kernel's sys_enter_openat and sys_enter_openat2 tracepoints. This captures every file open operation system-wide with minimal overhead.
The eBPF program runs in kernel space and:
- Captures the PID, process name, and filename for each open syscall
- Performs initial path filtering in-kernel using prefix patterns built from your configured
monitored_dirs(loaded into a BPF map at startup) - Sends matching events to userspace via a perf ring buffer
Relative paths are resolved against the opening process's working directory (via /proc/<pid>/cwd) before any attribution happens.
When a file access event is received, HDAS determines the responsible package by:
- Reading
/proc/<pid>/exeto get the executable path - If the executable is an interpreter (python, perl, ruby, node, java, electron, sh, …), reading
/proc/<pid>/cmdlineto find the script it runs and preferring that script's owner — so a packaged Python app is attributed to its own package rather than topython - Querying the system package manager to find which package owns that binary
- Caching results by binary path so repeated accesses don't re-query
The package manager is auto-detected at startup (pacman, portage, dpkg, rpm, xbps, or apk) from ID/ID_LIKE in /etc/os-release, falling back to whichever manager binary is installed. Set package_manager in the config to override it; hdas status shows which one is in use.
Before asking the package manager, HDAS checks whether the process belongs to a source it can identify directly. These packages get a prefix so they never collide with native names:
| Source | Detected via | Package name | Counted as orphaned when |
|---|---|---|---|
| Flatpak | /proc/<pid>/root/.flatpak-info |
flatpak:<app id> |
the app dir is gone from /var/lib/flatpak/app and ~/.local/share/flatpak/app |
| Snap | snap.<name>.* cgroup, or exe under /snap/<name>/ |
snap:<name> |
/snap/<name> is gone |
| Nix | exe under /nix/store/<hash>-<name>-<ver>/ |
nix:<name> |
it is unreachable from the system, user, and home-manager profiles (nix-store -qR) |
| Guix | exe under /gnu/store/ |
guix:<name> |
it is unreachable from the system and user profiles (guix gc -R) |
| AppImage | $APPIMAGE of a process running from a .mount_* dir |
appimage:<name> (version and arch stripped) |
the last-seen .AppImage file is gone |
| Home binaries | exe or script under $HOME, when nothing else in the process tree resolved |
local:<binary> |
the binary is gone |
A package is only reported as orphaned when its source positively says it is gone. If the tool or install directory it needs isn't present, it is treated as installed, so clean-orphans never deletes on a guess.
Flatpak and Snap apps keep their data in ~/.var/app/<id> and ~/snap/<name>. New configs monitor these automatically when they exist; hdas config validate warns if an older config is missing them.
Example: Firefox opens ~/.cache/mozilla/cookies.sqlite
/proc/1234/exe → /usr/lib/firefox/firefox
pacman -Qo /usr/lib/firefox/firefox → "firefox"
Attribution: firefox
Simple PID-to-package resolution fails for many real-world cases:
- Thread pools — Firefox uses
Isolated Web Co,StreamTrans,Cache2 I/Othreads - Forked children — Many apps spawn short-lived worker processes
- Race conditions — Process exits before we can read
/proc/<pid>/exe
HDAS solves this by walking up the process tree:
1. Try to resolve PID's own /proc/<pid>/exe → package
2. If that fails, walk up via PPID:
a. Read /proc/<pid>/stat to get parent PID
b. Try to resolve parent's exe → package
c. Repeat (up to 10 levels, stopping at PID 1)
3. Return the first successful package match
HDAS distinguishes between the process that created a file and processes that later accessed it.
Problem: If you cat ~/.config/app/settings.conf, the file gets attributed to coreutils instead of the app that created it.
Solution: Configured "ignored processes" (editors, pagers, shells) only update the last_accessed_by field, never overwriting created_by. This ensures that opening a config in vim doesn't lose the original creator attribution.
CREATE TABLE files (
path TEXT PRIMARY KEY,
created_by_package TEXT,
created_by_process TEXT,
created_at INTEGER,
last_accessed_by_package TEXT,
last_accessed_by_process TEXT,
last_accessed_at INTEGER
);
-- Binary paths for AppImage and local:* packages, used to detect removal
CREATE TABLE package_paths (
package TEXT PRIMARY KEY,
path TEXT NOT NULL,
seen_at INTEGER
);Existing databases from older versions are migrated automatically on first open.
- Monitoring must be running — Only tracks files accessed while the monitor is active
- Some "unknown" attributions — Binaries outside both the package manager and
$HOME(e.g. hand-installed files in/optor/usr/local) show as "unknown" - No retroactive attribution — Files created before monitoring started won't be attributed
hdas/
├── src/
│ ├── main.rs # CLI, subcommand dispatch
│ ├── monitor.rs # eBPF event processing, process tree walking
│ ├── db.rs # SQLite database, schema migrations
│ ├── query.rs # Query commands, JSON/colored output
│ ├── cleanup.rs # File deletion, symlink handling
│ ├── config.rs # Configuration loading and defaults
│ ├── pkgmgr.rs # Package manager abstraction (pacman, portage, dpkg, rpm, xbps, apk)
│ └── sources.rs # Flatpak, Snap, Nix, Guix, AppImage, and local binary attribution
├── bpf/
│ └── monitor.bpf.c # eBPF kernel program
├── build.rs # Compiles eBPF code at build time
├── hdas@.service # systemd service template
├── init/ # dinit, OpenRC, runit, and s6 services
└── Cargo.toml
GPL-3.0 — See LICENSE for details.
