以下が問題の箇所。
前提として、

  • wp_login_failed アクションが実行される前にすでにユーザー認証は済んでいる
  • $user には認証済みなら WP_User、エラーならWP_Error がすでに入っている

未入力の場合のそれぞれのエラーコードはempty_usernameempty_password

$ignore_codes = array( 'empty_username', 'empty_password' );

ここで除外リストに入っているのでif 文の条件は満たされず、wp_login_failed アクションは実行されない・・・。
フィルターやアクションでのアプローチは無理っぽいです。

$user = apply_filters( 'authenticate', null, $username, $password );

if ( null == $user ) {
	// TODO: What should the error message be? (Or would these even happen?)
	// Only needed if all authentication handlers fail to return anything.
	$user = new WP_Error( 'authentication_failed', __( 'Error: Invalid username, email address or incorrect password.' ) );
}

$ignore_codes = array( 'empty_username', 'empty_password' );

if ( is_wp_error( $user ) && ! in_array( $user->get_error_code(), $ignore_codes, true ) ) {
	$error = $user;

	/**
	 * Fires after a user login has failed.
	 *
	 * @since 2.5.0
	 * @since 4.5.0 The value of `$username` can now be an email address.
	 * @since 5.4.0 The `$error` parameter was added.
	 *
	 * @param string   $username Username or email address.
	 * @param WP_Error $error    A WP_Error object with the authentication failure details.
	 */
	do_action( 'wp_login_failed', $username, $error );
}

wp-login.php からのログイン失敗時はリダイレクトしたくないですが、そこはリファラーで問題なさそう。

function my_login_failed() {
    if ( strpos ( $_SERVER['HTTP_REFERER'], 'wp-login.php' ) === false ) {
        wp_redirect( get_bloginfo( 'url' ) . '?login=failed' );
        exit;
    }
}
add_action( 'wp_login_failed', 'my_login_failed' );