[How To] Use a pacman hook to deny a package installation

Continuing the discussion from Blacklist AUR paket like libgdata:

The idea of having a dummy package is good, but the use is limited to a single unwanted package.

I like puzzles :smiley: call it an academic interest :grin:

WARNING

If the intention is to avoid malware from AUR. Don’t proceed.
:information_source: Although the hook will deny an AUR package, the actual building is another process and the actions defined in the PKGBUILD will not be affected by these ideas.

The usual disclaimers for experiments apply - no warranties of any kind.

:warning::high_voltage:
:warning: Denying installation of packages from the official repo may have unexpected sideeffects
:high_voltage: Only apply the ideas after careful consideration of eventual sideeffects.

You are the system administrator.
This is your decision.
Do not ever complain if your system fails in unexpected ways.

:reminder_ribbon: this is purely academic - just for the fun of it

Assumptions

I am operating in root context

sudo su

In the folder /etc/pacman.d

cd /etc/pacman.d

I will be using the micro terminal editor.

Location of files

The libalpm library expects locally defined hooks in /etc/pacman.d/hooks.
I will store the companion script in /etc/pacman.d/scripts.
To help in future maintenance - name the script after the hook it serves.

In this little exersice I create these files (the last one is the final data file)

  • hooks/alpm-deny.hook
  • scripts/alpm-deny.sh
  • scripts/alpm-deny-targets

Before we begin - create the folders (assuming you are in the folder /etc/pacman.d

mkdir hooks scripts

Initial thoughts

When ever a package manager targeting a Arch Linux based system is installing a package, it will use the libalpm library.

This library provides hooks to execute actions based on defined criteria - please see

man alpm-hooks

Designing a hook

Let us look at the hook blueprint

[Trigger] (Required, Repeatable)
Operation = Install|Upgrade|Remove (Required, Repeatable)
Type = Path|Package (Required)
Target = <Path|PkgName> (Required, Repeatable)

[Action] (Required)
Description = ... (Optional)
When = PreTransaction|PostTransaction (Required)
Exec = <Command> (Required)
Depends = <PkgName> (Optional)
AbortOnFail (Optional, PreTransaction only)
NeedsTargets (Optional)

The behavior of the optional criteria is described

AbortOnFail
   Causes the transaction to be aborted if the hook exits non-zero.
   Only applies to PreTransaction hooks.

NeedsTargets
   Causes the list of matched trigger targets to be passed to the
   running hook on stdin.

Create the alpm-deny.hook

We also want the transaction to deny the installation of the given package.

Trigger section

  • Trigger on installation
  • Trigger type is specific package
  • Target is the package we want to deny

Action section

  • Description is always good
  • AbortOnFail is needed as we want a signal to propagate back to the hook
  • NeedsTargets this is required to display a meaningful message to the user
  • We want to run the hook before the transaction takes place
  • Define the script or program to execute
micro hooks/alpm-deny.hook
[Trigger]
Operation = Install
Type = Package
Target = octopi

[Action]
Description = alpm deny hook
AbortOnFail
NeedsTargets
When = PreTransaction
Exec = /etc/pacman.d/scripts/alpm-deny.sh

Create the alpm-deny.sh

micro scripts/alpm-deny.sh
#!/usr/bin/sh

# Read each target (one per line) from stdin
while IFS= read -r pkg; do
    echo "ERROR: Installation of $pkg is denied by alpm-deny hook."
done

# return a non-zero exit status
exit 1

It is easy to expand which packages to deny - simply add a new Target = to the [Trigger] section - something like this

[Trigger]
Operation = Install
Type = Package
Target = octopi
Target = <package1>
Target = <package2>
Target = <package3>

Ideas For Improvments

  • Change the hook to execute on all packages
    • Inline a list of package to deny inside the script
    • Read the array of denied packages from list of packages

Thoughts on Improvements

If we change the hook to execute on all packages, we must also change the script to return different values depending on the denied packages.

If the package passed on to the script is not in our set of denied packages the script should return 0 instead of 1.

Improved Script

Our first task is to modify the hook’s Trigger section.
Change the Target to use a * wildcard which would be all packages.
It is also possible to work with a subset e.g. using e.g. deepin*

micro hooks/alpm-deny.hook
[Trigger]
Operation = Install
Type = Package
Target = *

Then we modify script

micro scripts/alpm-deny.sh
#!/usr/bin/sh

# my attempt to learn the differences between bash and posix shell
# Arrays for example is not available in posix shell

# ------------------------------------------------------------------
# Deny list - because arrays is not a part of posix
# one element per line no spaces
# ------------------------------------------------------------------
targets="
octopi
paru
yay
"

# Convert the multiline string into a space‑separated list so we can use
# the `case` construct later.  (POSIX sh does not have real arrays.)
target_list=$(printf '%s\n' "$targets" | tr '\n' ' ')

# Read every target name that pacman sent on stdin.
while IFS= read -r pkg; do
    # Skip empty lines 
    # they can appear if the hook is run with no
    # matching packages (unlikely here but harmless)
    [ -z "$pkg" ] && continue

    # Does $pkg exist in our target_list?
    case " $target_list " in
        *" $pkg "*)
            echo "ERROR: Installation of package '$pkg' is denied by alpm-deny hook."
            exit 1   # non‑zero → AbortOnFail triggers abort
            ;;
        *)
            # allowed – nothing to do, just continue with next line
            ;;
    esac
done

# All packages passed the check; let pacman continue.
exit 0
Example run
 # pacman -Syu yay
:: Synchronizing package databases...
 core is up to date
 extra is up to date
 multilib is up to date
 sublime-text is up to date
:: Starting full system upgrade...
resolving dependencies...
looking for conflicting packages...

Packages (13) graphviz-15.1.1-1  haskell-djot-0.1.3-1  haskell-pandoc-3.6.1-11
              haskell-pandoc-lua-engine-0.4.1-16  haskell-pandoc-server-0.1.0.11-10  libxfont2-2.0.9-1
              ollama-0.32.6-1  pandoc-cli-3.6.1-20  pango-1:1.58.2-1  python-argcomplete-3.7.2-1
              syncthing-2.1.3-1  uv-0.12.2-1  yay-13.0.1-1

Total Download Size:    54,52 MiB
Total Installed Size:  224,64 MiB
Net Upgrade Size:       10,74 MiB

:: Proceed with installation? [Y/n] 
:: Retrieving packages...
[ .... ]
:: Running pre-transaction hooks...
(1/2) alpm deny hook
ERROR: Installation of package 'yay' is forbidden by the alpm-deny hook.
error: command failed to execute correctly
error: failed to commit transaction (failed to run transaction hooks)
Errors occurred, no packages were upgraded.

Final Script

Instead of the in-lined list of packages we create a data file - place it next to the script

Create and edit the data file alpm-deny-targets

micro scripts/alpm-deny-targets

With content - example

octopi
paru
yay

Alter the script to use the file as source for denied packages

micro scripts/alpm-deny.sh
#!/usr/bin/sh

# my attempt to learn the differences between bash and posix shell
# Arrays for example is not available in posix shell

# ------------------------------------------------------------------
# Load the package names from the external file.
#   Stored as /etc/pacman.d/scripts/alpm-deny-targets
# ---------------------------------------------------------------
#   The file contains one name per line; we convert it into a space‑separated
#   list so that later we can test membership with `case`.
# ------------------------------------------------------------------
if [ -r /etc/pacman.d/scripts/alpm-deny-targets ]; then
    target_list=$(printf '%s\n' "$(sed '/^\s*#/d;/^\s*$/d' /etc/pacman.d/scripts/almp-deny-targets)" | tr '\n' ' ')
else
    # if the file does not exist - there is nothing to deny and we just exist
    exit 0
fi

# Read every target name that pacman sent on stdin.
while IFS= read -r pkg; do
    # Skip empty lines 
    # they can appear if the hook is run with no
    # matching packages (unlikely here but harmless)
    [ -z "$pkg" ] && continue

    # Does $pkg exist in our target_list?
    case " $target_list " in
        *" $pkg "*)
            echo "ERROR: Installation of package '$pkg' is denied by alpm-deny hook."
            exit 1   # non‑zero → AbortOnFail triggers abort
            ;;
        *)
            # allowed – nothing to do, just continue with next line
            ;;
    esac
done

# All packages passed the check; let pacman continue.
exit 0

:reminder_ribbon: this is purely academic - just for the fun of it

11 Likes