post://veet-the-linux-uninstaller-that-cleans-what-package-managers-leave-behind

Veet: the Linux uninstaller that cleans what package managers leave behind

read: 9 min words: 1,639
Veet: the Linux uninstaller that cleans what package managers leave behind
toc://sections
outline

    I created github.com/swadhinbiswas/veet on 15 August 2026 after getting tired of disk space disappearing on a machine where I had already removed the apps. The repo is Go, MIT licensed, 150 stars and 6 forks when I pulled these notes, with 30 commits on main and 2 open issues. The short description fits in one line: universal Linux application uninstaller and deep clean residual purger.

    Standard removal only touches package managed files under paths like /usr/bin and /usr/share. It leaves user space behind. I kept finding the same leftovers:

    • ~/.config/<app> with settings and credentials
    • ~/.cache/<app> with runtime and shader caches
    • ~/.local/share/<app> with local databases and state
    • ~/.local/state/<app> with session history
    • ~/.var/app/<app> for Flatpak data
    • ~/snap/<app> for Snap data
    • /var/log/<app> and journal archives

    One app never costs much. Fifty apps across native packages, Flatpak, Snap, AppImage, Homebrew, Nix, and language toolchains add up to gigabytes. I built Veet to scan all of those sources at once, show me exactly what would go, and remove it in one confirmed action.

    What it scans

    Veet queries 17 sources concurrently and skips tools that are not installed at zero cost. The detector files under internal/detector/ name them plainly: pacman.go, apt.go, dnf.go, zypper.go, flatpak.go, snap.go, appimage.go, brew.go, nix.go, npm.go, pipx.go, cargo.go, gem.go, goinstall.go, plus cache.go, orphans.go, and systemlogs.go.

    That list covers system managers, sandboxed containers, portable bundles, and language toolchains including npm -g, pipx, cargo, gem, and go install. Orphan detection uses the native queries you would run by hand: pacman -Qdt, apt autoremove, and dnf repoquery --unneeded. Journal handling shells out to journalctl --vacuum for bloated systemd storage alongside rotated compressed logs.

    flowchart TD
      SCAN[Concurrent scan of 17 sources] --> STAGE[Stage residual paths]
      STAGE --> REVIEW[Inspect paths in TUI or dry run]
      REVIEW --> CONFIRM[Confirm once]
      CONFIRM --> REMOVE[Atomic removal with single sudo batch]
    

    The stack is small and current. go.mod states Go 1.24.2 with Bubble Tea 1.3.10, Lip Gloss 1.1.0, Cobra 1.10.2, Viper 1.21.0, Afero 1.15.0, and golang.org/x/sys 0.38.0. The layout is easy to walk: main.go holds the Cobra entrypoint, internal/model/ holds types like AppInfo and RemovableFiles, internal/detector/ holds source handlers, internal/uninstaller/ holds staging and filesystem removal, internal/history/ holds the audit log, and internal/ui/ holds Bubble Tea components with a theme engine.

    TUI and CLI

    I use both modes. The TUI is the daily driver. The CLI is for scripts and audits.

    Launch the dashboard:

    veet
    

    List everything without interaction:

    veet scan
    veet scan --json
    

    Remove with a preview of staged paths:

    veet clean app1 app2
    veet clean code --dry-run
    veet clean code --yes
    

    Orphans and caches get their own subcommands:

    veet orphans --clean --yes
    veet cache --clean --yes
    veet history
    veet history --clear
    

    main.go defines scan, clean, orphans, cache, history, and tui. The clean command accepts --source to limit matching to one origin, --yes for non interactive runs, and --dry-run to stage and print paths without deleting. orphans and cache accept --json, --clean, and --yes with a 60 second context timeout. history accepts --json, --clear, and --limit with a default of 50 entries. Output tables print APP/PACKAGE, VERSION, SOURCE, and SIZE, with - when size is unknown.

    One behavior surprised me the first time: the TUI refuses to run as root. Elevation happens per command through sudo instead. That choice keeps the interactive process unprivileged while system paths still get removed in one batched transaction.

    Staging before deleting

    Veet never deletes on scan. It stages candidate paths for inspection, then executes after confirmation. runClean in main.go prints each match with reclaimable size and file count, lists every path with a [requires sudo] tag where needed, and totals the run before asking [y/N]. Dry run stops there with the line Dry-run mode: no files or packages were removed.

    The comparison in the README states the difference well:

    Task pacman or apt remove flatpak uninstall bleachbit veet
    Binary removal Yes Yes No Yes across 17 sources
    Config cleanup in config dir No No Partial Yes, staged for inspection
    Cache cleanup No No Yes Yes, staged for inspection
    Sandbox cleanup in var app and snap dir No Partial No Yes, deep detection
    Orphan pruning Manual query No No Yes, live detection
    AppImage discovery No No No Yes
    Dry run with full path list Partial Partial Yes Yes with --dry-run
    Single sudo prompt per batch Per command Not applicable Full root required Yes

    I rely on the staged list because config directories sometimes hold credentials or project state I want to back up first. Seeing /home/swadhin/.config/Code in the list before anything runs is the whole point.

    Search that matches how I think

    Press / in the TUI to focus search. The modifiers are few and they compose:

    Syntax Example Meaning
    @source @flatpak Limit to one manager
    >size >100M Larger than given size
    <size <50M Smaller than given size
    protected:bool protected:true Filter protected components
    name gimp Fuzzy match on name

    @flatpak >200M gimp finds Flatpak apps over 200 MB matching gimp. I use @aur >500M after a few months of Arch use to find the packages that grew without me noticing.

    Safety

    Veet protects glibc, linux, systemd, and the active shell from selection. Config can extend that list. runClean refuses with refusing: %s is a protected system component when a match is protected, and config protected_packages in ~/.config/veet/config.yaml adds project specific entries like a custom kernel module or proprietary driver.

    System paths batch into a single elevated transaction instead of prompting per file. Filesystem removal goes through Afero, which also makes the uninstaller testable without touching a real home directory. History appends to ~/.local/share/veet/history.log with date, app, source, status, freed size, and detail, so I can review what left the machine last month.

    Themes and config

    Veet ships 7 palettes: cyan as default, plus catppuccin, nord, dracula, gruvbox, tokyo-night, and monokai. I run catppuccin because it matches the rest of my terminal. Config lives at ~/.config/veet/config.yaml:

    theme: catppuccin
    icons: auto
    protected_packages:
      - custom-kernel-module
      - my-critical-service
      - proprietary-driver
    

    Icons accept auto, nerd, unicode, or ascii. Auto uses Nerd Fonts unless the session is a raw Linux console or dumb terminal. Unicode falls back to symbols like check and cross marks. ASCII uses plain labels like [pac] and [aur] with zero font requirements. The installer downloads Nerd Font Symbols v3.3.0 with SHA256 verification when fontconfig reports no Nerd Font, and otherwise leaves fonts alone.

    Install and remove

    Four paths, same binary. Arch users get an AUR package:

    yay -S veet
    paru -S veet
    yay -S veet-git
    

    The script path compiles with -ldflags="-s -w", installs to ~/.local/bin/veet, and writes bash, zsh, and fish completions:

    curl -sSL https://raw.githubusercontent.com/swadhinbiswas/veet/main/install.sh | bash
    curl -sSL https://raw.githubusercontent.com/swadhinbiswas/veet/v1.1.0/install.sh | VEET_VERSION=v1.1.0 bash
    ./install.sh
    

    Go users install directly with Go 1.24 or newer:

    go install github.com/swadhinbiswas/veet@latest
    

    Manual build uses the Makefile:

    git clone https://github.com/swadhinbiswas/veet.git
    cd veet
    make build
    make install
    

    The Makefile has build, run, test with go test -v -cover ./..., fmt, lint through go vet, install to ~/.local/bin, and clean. If ~/.local/bin is missing from PATH, I add export PATH="$PATH:$HOME/.local/bin" to shell rc.

    Removal is symmetric:

    ./uninstall.sh
    rm -f ~/.local/bin/veet
    rm -rf ~/.local/share/veet ~/.config/veet
    

    The manual path also removes bash, zsh, and fish completion files. I keep audit logs until I am sure a removal did not take something I wanted, then clear them with veet history --clear.

    Keys I press

    Press Effect
    1 to 6 Jump to stat group for All, Flatpak, Snap, Tools, Cache, Reclaim
    bracket or arrow keys Cycle stat categories
    up, down, j, k Move cursor
    space Toggle multi select
    d Delete highlighted app with Yes or No popup
    a Select all visible rows except protected
    o Cycle sort by size, name, source, date
    p Toggle staged path inspector
    / Focus search
    Tab Cycle source filter
    Enter Stage and open confirmation preview, Enter again confirms
    c Clear audit log in history, quick purge for cache in table
    e Export report to apps-report.json
    r Rescan all detectors
    l Open history
    s Open settings and protected registry
    h Toggle keybinding overlay
    Esc Close modal or blur search
    q Quit

    Enter twice to confirm feels right in practice. The first Enter stages and shows the full preview modal. The second runs it.

    Limits I accept

    Veet is Linux only. It does not manage macOS or Windows apps. It stages paths it can see from detectors and home directory scans, so a weird custom install prefix still needs a manual look. Audit export writes to ~/.local/share/veet/apps-report.json, history caps display at 50 entries unless I raise --limit, and scan contexts time out at 60 seconds per the Cobra handlers.

    Contributions go through CONTRIBUTING.md. Detector guides and testing protocols live there.

    If your root disk keeps filling after removals, start with veet scan, then veet cache, then one veet clean <name> --dry-run on the largest match. That sequence shows the exact paths before anything leaves the disk.

    react://veet-the-linux-uninstaller-that-cleans-what-package-managers-leave-behind
    comments://veet-the-linux-uninstaller-that-cleans-what-package-managers-leave-behind

    No comments yet.