Proxmox hookscript failing when it does more than one thing? It’s not your script logic — it’s the execution context. The phase argument, the locked VM config, and root’s stripped environment all change what commands actually work. Restructure around those and it stops failing.

What a hookscript actually gets
Proxmox calls your script with a single argument: the phase. pre-start, post-start, pre-stop, post-stop. That’s it. No VMID, no config path, nothing else. If you need the VMID, read it from $1? Wrong — that’s the phase. The VMID comes from the environment variable VMID, but only sometimes. In pre-start it’s set; in post-stop it might not be. Don’t rely on it.
The VM config is locked during pre-start and pre-stop. That means qm set fails with a lock error. You can read the config with qm config $VMID, but you can’t write to it. If you need to change something, do it in post-start or post-stop when the lock is released.
Why multi-step scripts break
Most people paste a snippet that works for one phase and then add more commands. That’s where it falls apart. A command that works in post-start might fail in pre-start because the VM isn’t running yet. A command that works manually as root fails in the hookscript because the environment is different — no PATH, no HOME, no USER. Use absolute paths for everything.
Also, the hookscript runs as root, but with a minimal environment. If you call systemctl or virsh, they might not be found. which systemctl in your shell gives /usr/bin/systemctl, but in the hookscript systemctl alone fails. Use /usr/bin/systemctl or set PATH at the top of your script.

Restructure it like this
Split your script by phase. One case statement, one job per phase. Don’t try to do everything in pre-start. Example:
#!/bin/bash
PHASE="$1"
VMID="$2" # not always passed, check
case "$PHASE" in
pre-start)
# read-only checks, no qm set
/usr/sbin/qm config "$VMID" | grep -q 'some setting' || exit 1
;;
post-start)
# now the VM is running, config is unlocked
/usr/sbin/qm set "$VMID" --some-option value
;;
pre-stop)
# VM still running, config locked
/usr/bin/systemctl stop some-service
;;
post-stop)
# VM stopped, config unlocked
/usr/sbin/qm set "$VMID" --delete some-option
;;
esac
That’s the whole trick. One phase, one job. If you need to do multiple things in a phase, put them in a function and call it. But don’t mix phases.
Debug it without guessing
Add logging to a file. echo "$(date) $PHASE $VMID" >> /var/log/hookscript.log at the top. Then run the script manually with each phase to see what fails. bash /var/lib/vz/snippets/hookscript.sh pre-start 100. That’s faster than pasting snippets from forums.
And if it still fails, check the invocation path — the script has to be executable and stored on a snippets-enabled storage.
Honestly, the docs are a mess on this. They show one trivial example and leave you to figure out the locking and environment on your own. But once you know the phase argument and the lock, it’s not that hard.