Use adduser.local Hooks Without Breaking Account Management

You want something local to happen every time an account is created, and adduser.local is the documented hook for that. This also covers a matching cleanup hook for deluser. The examples target the installed Debian adduser package, version 3.137ubuntu1, and use only the documented hook interface.

Allow 15 to 20 minutes if you are testing on a disposable account. You need an administrator shell, a text editor and a clear decision about what local file or directory the hook should manage. The hooks run as part of account administration, so a mistake can affect every future user creation or removal.

Checkpoint: this guide installs files under /usr/local/sbin. Do not place experimental code there until it has been tested separately.

1. Confirm the hook contract

The administrator installs an executable named adduser.local in /usr/local/sbin. The adduser command calls it after creating the account. A similarly named executable, deluser.local, is called by deluser for local cleanup.

Both hooks receive four positional arguments in this order:

  1. the username;
  2. the numeric user ID;
  3. the numeric primary group ID; and
  4. the home-directory path.

The installed command confirms the paths and order:

sed -n '920,930p' /usr/sbin/adduser
sed -n '398,407p' /usr/sbin/deluser

Do not assume the fourth argument is always an existing directory. A delete hook may need to cope with a home directory that has already been removed, depending on the deletion operation and its options.

2. Design the action before writing code

Choose one local responsibility and make it explicit. Good candidates include creating a directory used by a local service, recording an account in an inventory file, or removing that account's local service data. Keep the hook idempotent: running it twice should leave the same result as running it once.

Warning: never call adduser or deluser from inside a hook. The hook runs while the adduser or deluser lock is active, and calling back into either command can deadlock, recurse, or interfere with the operation still holding the lock.

Safety warning: treat the username and home path as untrusted input. Use exact path construction, quote every expansion, and refuse unexpected values before creating or removing anything. Never turn the home argument into a recursive deletion target without checking it is inside the directory tree you actually intend to manage.

3. Install a read-only diagnostic hook first

Test the interface before adding a state-changing action. Create this temporary hook as an administrator:

sudo install -m 0755 /dev/null /usr/local/sbin/adduser.local
sudo sh -c 'cat > /usr/local/sbin/adduser.local' <<'EOF'
#!/bin/sh
printf 'adduser.local: user=%s uid=%s gid=%s home=%s VERBOSE=%s\n' \
    "$1" "$2" "$3" "$4" "${VERBOSE-}" >&2
exit 0
EOF

The command needs elevated privileges because /usr/local/sbin is administrator-owned. Check the mode and interpreter before using it:

sudo test -x /usr/local/sbin/adduser.local
sudo head -n 2 /usr/local/sbin/adduser.local

Use a disposable account name that does not already exist. Account creation changes system state, so check first and keep the account available for the paired removal test:

getent passwd hook-test-user || sudo adduser --disabled-password --gecos '' hook-test-user

Expected diagnostic output resembles this, though the numeric IDs and home path will vary:

adduser.local: user=hook-test-user uid=1501 gid=1501 home=/home/hook-test-user VERBOSE=1

On this package, VERBOSE is exported as 0 for --quiet, 1 for ordinary operation and 2 for --debug. Use it to reduce optional logging, not to decide whether the hook should perform essential work.

4. Replace the diagnostic with a bounded action

Once the arguments look correct, replace the test with the real operation. This example creates a per-user directory under a fixed local service tree, and deliberately refuses a username containing a slash or a home path outside /home:

#!/bin/sh
set -eu

user=$1
home=$4
case "$user" in
    ''|*[!A-Za-z0-9._-]*) exit 1 ;;
esac
case "$home" in
    /home/*) ;;
    *) printf 'refusing unexpected home path: %s\n' "$home" >&2; exit 1 ;;
esac

install -d -o "$user" -g "$user" -m 0750 "/var/lib/local-service/$user"
exit 0

Validate the script with a shell syntax check before installing it:

sh -n /path/to/your/adduser.local

Use the numeric arguments when ownership must be unambiguous across name-service changes: pass "$2" and "$3" to tools that accept numeric IDs. Do not assume a primary group shares the user's name just because that is common on a particular adduser configuration.

5. Add deletion cleanup only after creation works

A delete hook has the same four arguments, but its job is usually cleanup rather than creation. Start with a log-only deluser.local, then add a narrowly scoped removal once you have confirmed exactly what must be retained.

Destructive action: recursive removal is irreversible unless you have a backup. A safe cleanup hook should delete only a directory it owns, verify the expected account name, and avoid following links. If the service data matters, archive it or require a separate operator command instead of deleting it automatically.

After the account test, inspect the result and then remove the disposable account through the normal command:

getent passwd hook-test-user
sudo deluser hook-test-user

The delete hook's return value is ignored by adduser and deluser, so a failed cleanup cannot reliably make the account removal fail. Log failures clearly and arrange a separate reconciliation check for important data.

6. Check failures without calling back into adduser

Run the hook itself only when you need a controlled diagnostic, and pass all four arguments explicitly:

sudo env VERBOSE=2 /usr/local/sbin/adduser.local \
    hook-test-user 1501 1501 /home/hook-test-user

This tests your argument handling, not the complete adduser transaction. The real hook is called while the account-management lock is held, so a direct run cannot prove your code is safe to call from inside that lock. Do not treat a direct test as permission to invoke adduser, deluser or another command that takes the same lock.

If the hook is executable but produces no visible output, remember hook return values are ignored and normal adduser output may not include hook diagnostics. Write deliberate diagnostics to standard error or to a protected log, and include the username and action without recording secrets.

Done means