Be inclusive
I'm going to suggest allowing the widest range of characters you can, and let the programmer make good choices for themselves. I'm also going to suggest allowing an even wider range than you can, later on.
Basic identifiers
I've previously allowed identifiers to start with letters (uppercase, lowercase, titlecase, modifier, other), underscores, and all "Symbol, other"s that weren't operator characters (discussed below). Subsequent characters could also be numbers (decimal digits, letter, other), non-spacing marks, spacing combining marks, and enclosing marks, the same other symbols, and the apostrophe.
This is a very broad group and permits text in natural languages as well as mathematical symbols, emoji characters, regional indicators (flags), and other multi-codepoint grapheme clusters. These are a generalisation of specific identifiers that different people wanted available, leaning towards inclusiveness where it wasn't unreasonable.
It's definitely possible to make unwise identifier choices within this, but ultimately that's true even with a narrow range of characters. Likely most programs will use only a tiny part of this range, but different users are interested in different parts of that space. In particular, the emoji characters while seeming "unserious" to some were pretty popular as mnemonic tools for exploring novices, and variable names in CJK characters too. A collaborative coding style guide could well forbid them, but the language doesn't have to make that choice for them.
Operator identifiers
In languages with user-defined operators, these are generally a distinct kind of identifier character — and the suitable characters don't really coincide with the Unicode categories, in both directions. Instead, a better granularity is to pick out specific Unicode blocks (256-codepoint ranges), along with some individual characters from ASCII and Latin-1. These operator codepoints should be excluded from being part of regular identifiers (unless all identifiers are usable as operators, as in some languages).
We used the remaining ASCII keyboard symbols, the symbols from Latin-1, and the entire blocks of (supplemental) mathematical operators and symbols, miscellaneous symbols, (supplemental) arrows, supplemental technical, currency symbols, and geometric shapes. All of these were excluded from ordinary identifiers explicitly, because many of them are in the catch-all Symbol, other category that also includes lots of "noun" codepoints.
Normalise
At the very least, normalise identifiers to either fully-composed (NFC) or fully-decomposed (NFD) form for comparison. 한국
and 한국
should be the same identifier and so should kākāpō
and kākāpō
, but they're not byte-for-byte identical. It's possibly good to normalise further cases away as well, but they mostly head into "malicious" rather than accidental territory and may not be worth the costs. I have not bothered for program source code, but do for network and file tasks.
Allow an escape hatch that permits any character
This isn't something you'll want to use all the time or encourage, but there are times when languages need to interact with other languages or systems with different choices, perhaps directly via a foreign function interface, or as a reification of an external namespace like a database table, or directly wired to a visual user interface. It's helpful to have a way to represent these identifiers on those occasions even if they clash with a language keyword, start with a number, have spaces, contain a character you use for something else, or one you wouldn't use at all.
It's probably good to have this escape hatch be a bit out of the way, but accessible for the cases that really do need it.
F# uses double backticks for this purpose: let ``this, here, is a single identifier in F#! 😹`` = true
. Something else awkward-but-available might work for your purposes.