Your question does not mention how you try to use your expression. Regular expressions in the bash shell are POSIX extended regular expressions. These do not support positive or negative lookaheads or look-behinds.
Instead of using your expression to try to fit something more into it, I'm suggesting breaking the test down into several separate tests.
It's easiest to understand the implementation and to keep it maintained if each test is done independently, without trying to craft a monster expression to do it all.
#!/bin/bash
pw_is_valid () {
local avoid='[<>`"'"'"'~]'
[ "${#1}" -ge 12 ] &&
[[ $1 =~ [[:upper:]] ]] &&
[[ $1 =~ [[:lower:]] ]] &&
[[ $1 =~ [[:digit:]] ]] &&
[[ $1 =~ [[:punct:]] ]] &&
[[ ! $1 =~ $avoid ]]
}
until
IFS= read -r -s -p 'Enter password: ' pw
pw_is_valid "$pw"
do
echo 'Try again' >&2
done
I'm using a variable, avoid, to hold the regular expression [<>`"'~] to reduce the quoting needed to represent it.
In the POSIX aka C locale, the POSIX [:punct:] character class contains the characters in the following set:
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
In regular locales, it would typically contain a lot more¹.
I'm using it here as a stand-in for "special characters".
¹ for example, on Ubuntu 20.04 and in the en_GB.UTF-8 locale, it includes 147495 different characters. YMMV depending on the system, version thereof and locale.