All I wanted for Christmas (well, Memorial Day) was Skittles-themed remote shell prompts for different user@host pairs, color-blindness-friendly edition. Yes, remote machines are still a thing in 2026, your AI cloud is just somebody else’s data center.

My goal was really simple: when a terminal says user@host0042, the prompt background should give me one more visual cue about which machines I am on cough my agents are currently trying to ruin. The objective here was mapping a structured string like user@host0042 into one of 16 color palette slots.

That required a tiny string hash that could run inside a shell prompt without dragging in an external command. I asked an AI coding assistant to write the shell-level arithmetic. It chose DJB2 hash.1

That was not a completely outrageous answer. At least not in the previous century. DJB2 is famous, small, and simple, at first glance:

h = 5381;
for each byte:
    h = h * 33 + byte;

But it is also not a good answer. DJB2 is a neat historical artifact, not a hash function I would choose for new code. A quick search still finds people defending it, and coding agents appear to have absorbed that old folk wisdom too. As they say: “The Internet is forever.” (it is not), and AI coding assistants are using dragnet tactics for scour for information, not common sense.

So, I muttered something about “token cost” to myself, and rolled up my sleeves. The result of this little excursion is a tiny Public Service Announcement with a terminal prompt as the excuse:

If you need a simple non-cryptographic string hash today, my advice is do not reach for DJB2. For this kind of shell-friendly, non-adversarial use, FNV-1a-32 is just as implementable and behaves better on inputs you care about.

A Practical Constraint: Hash in Shell Arithmetic Link to heading

The implementation had to be boring enough to live in a shell prompt theme. I wanted simple integer arithmetic and no external command in the prompt path.

The current prompt code uses a 16-color palette and indexes it with the hash modulo the palette length:

BGCOL=${host_backgrounds[$(( hash % ${#host_backgrounds} + 1 ))]}

The full shell implementation of FNV-1a-32 is short enough to quote whole:

# Return a 32-bit FNV-1a hash for a byte-oriented string.
function fnv1a32_string() {
    emulate -L zsh

    local string="$1"
    local -i hash=2166136261
    local -i prime=16777619
    local -i i byte
    local LC_ALL=C

    for (( i = 1; i <= ${#string}; i++ )); do
        byte=$(printf '%d' "'${string[i]}")
        hash=$(( ((hash ^ byte) * prime) & 16#ffffffff ))
    done

    echo "${hash}"
}

The magic numbers are not mine. RFC 9923 gives FNV-1a’s basic form as “xor the next octet, multiply by the FNV prime modulo the hash size” and lists the 32-bit offset basis as 0x811C9DC5, which is 2166136261 in decimal. The 32-bit prime is 0x01000193, or 16777619. The RFC also says plainly that FNV is non-cryptographic and has low-bit weaknesses; that matters because this is a shell prompt, not a password store, a message-authentication code, or an internet-facing hash table under adversarial load. 2

Theory and Practice Align For DJB2 Link to heading

The issues with DJB2 are not theoretical, by the way. Similar-looking hostnames collided into the same prompt color (I know, shock and horror!). With only 16 buckets, collisions are inevitable, but patterned inputs should not cluster visibly on the first handful of real hosts I checked.

The more interesting problem appeared when I stopped looking at one or two hostnames and generated structured key families:

user000123@host0042.example.net
host0042.example.net/user000123

Each family had 1,000,000 keys: 1,000 users crossed with 1,000 hosts. The bucket table had 65,536 buckets, and the bucket index used the low bits:

bucket = hash & (65536 - 1);

Using only low bits is a standard practice, power-of-two table sizes are common because masking is cheap. A hash that falls apart under low-bit indexing is failing a very ordinary usage pattern.

Sometimes, I Miss Academia a Little Link to heading

Why not? Let’s dive head-first down the hash function rabbit hole, again.

I compared:

  • DJB2.add: h = h * 33 + byte
  • DJB2.xor: h = h * 33 ^ byte (apocryphally, DJB’s favored variant?)
  • DJB2.add.fmix: DJB2.add plus a small avalanche finalizer
  • DJB2.xor.fmix: DJB2.xor plus the same finalizer
  • FNV-1a-32
  • FNV-1a-32.fmix: FNV-1a-32 followed by Murmur3’s fmix32
  • Murmur3-32

The finalizer tested with DJB2 was:

hash=$(( (hash ^ (hash >> 16)) & 0x7fffffff ))
hash=$(( (hash * 0x45d9f3b) & 0x7fffffff ))
hash=$(( (hash ^ (hash >> 16)) & 0x7fffffff ))
hash=$(( (hash * 0x45d9f3b) & 0x7fffffff ))
hash=$(( (hash ^ (hash >> 16)) & 0x7fffffff ))

The FNV-1a-32.fmix variant used Murmur3’s final avalanche:

h ^= h >> 16;
h *= 0x85ebca6b;
h ^= h >> 13;
h *= 0xc2b2ae35;
h ^= h >> 16;

That finalizer comes from Austin Appleby’s MurmurHash3 implementation. Murmur3-32 itself is a better hash than FNV-1a-32 for this general class of work, but it is also more code, especially if the implementation has to remain pleasant in shell arithmetic. 3

For keys like user000123@host0042.example.net, the results were:

Hash Full 32-Bit Collisions Buckets Used Worst Bucket Max/Avg Low 8-Bit Range χ²/Bucket
FNV-1a-32 32 65,536 33 2.16 3,633..4,172 0.99
FNV-1a-32.fmix 32 65,536 35 2.29 3,704..4,062 1.00
DJB2.add 0 65,536 32 2.10 1,078..6,910 4.40
DJB2.xor 408 32,768 51 3.34 0..8,248 16.20
DJB2.add.fmix 0 65,536 33 2.16 3,759..4,068 0.99
DJB2.xor.fmix 428 65,536 35 2.29 3,756..4,131 1.00
Murmur3-32 140 65,536 34 2.23 3,750..4,108 0.99

For keys like host0042.example.net/user000123, the results were:

Hash Full 32-Bit Collisions Buckets Used Worst Bucket Max/Avg Low 8-Bit Range χ²/Bucket
FNV-1a-32 128 65,536 35 2.29 3,798..4,007 0.98
FNV-1a-32.fmix 128 65,536 34 2.23 3,760..4,072 1.00
DJB2.add 0 65,494 32 2.10 1,078..6,910 4.50
DJB2.xor 0 32,768 51 3.34 0..8,248 16.32
DJB2.add.fmix 0 65,536 37 2.42 3,740..4,067 0.99
DJB2.xor.fmix 328 65,536 33 2.16 3,730..4,124 1.01
Murmur3-32 114 65,536 33 2.16 3,693..4,063 1.00

Several details are worth separating:

  • plain DJB2.add had zero full 32-bit collisions on these generated username and hostname strings. That sounds great until you look at the low byte. A uniform distribution over 1,000,000 keys would put about 3,906 keys in each low-byte bucket. DJB2.add produced a range of roughly 1,078..6,910, a large enough skew to matter for bucket indexing.

  • plain DJB2.xor used only 32,768 of 65,536 power-of-two buckets. Half the table was unreachable under this indexing scheme. Do not use it.

  • adding a finalizer repaired the low-bit bucket distribution for this narrow input family. That is useful, but it is not magic. The finalizer is a bijection, mapping equal hash values to equal final values. It can spread weak output bits; it cannot uncollide values that have already collapsed to the same 32-bit hash.

  • FNV-1a-32’s full 32-bit collision counts, 32 and 128 over one million keys, are not alarming. The birthday estimate for one million keys into 2^32 possible outputs is about 116 expected colliding pairs. FNV-1a-32 is not perfect, but these numbers are in the range one expects from a 32-bit hash behaving roughly randomly on that metric.

SMHasher3 Spoils the Victory Lap Link to heading

The tables above just answer my question: what should I use for user@host-style strings when the implementation has to fit comfortably in shell arithmetic?

It does not prove that FNV-1a-32 is a modern high-quality hash. (Spoiler: It is not.) RFC 9923 itself points out that direct FNV has weak diffusion in its lower output bits, and SMHasher3 is quite happy to find problems in FNV-1a-32 too. 2

SMHasher3 is designed for exactly this sort of curiosity. It evaluates non-cryptographic hashes for output distribution, collisions, and some performance behavior. 4 Broader SMHasher3 tests were less forgiving. DJB2.add.fmix still failed badly on structured cases. One Bitflip test on 3-byte keys expected about three orders of magnitude less collisions than it observed! Another expected 4,000x less collisions. A related case expected 400x less.

The key point is this: DJB2.add.fmix can look respectable on a friendly bucket-distribution test and still be a weak hash family under broader test batteries. The finalizer improves the symptom that hurt my prompt-coloring use case. It does not turn DJB2 into a good new default. Don’t quote me on Reddit saying “just add a finalizer to DJB2 and it’s mint”.

For reference, the relevant SMHasher3 output snippets were:

Expected 1,151.7 collisions, actual 2,860,090
Expected   288.0 collisions, actual 1,289,578
Expected 1,151.7 collisions, actual 479,574

Speed Was Not the Escape Hatch Link to heading

There is another old argument for DJB2: it is simple, therefore it must be fast.

On the SMHasher3 benchmark, the DJB2 variants did not win a speed argument either:

Hash Small Avg Cycles/Hash Bulk Bytes/Cycle Range Bulk Bytes/Cycle
sum32hash baseline 15.82 28.24 28.23
DJB2.add 38.85 0.69 0.68
DJB2.xor 53.80 0.35 0.34
DJB2.add.fmix 48.07 0.73 0.72
DJB2.xor.fmix 66.18 0.35 0.33

This because the recurrence is loop-carried:

h = h * 33 + byte;

Each byte depends on the previous byte’s hash state. That dependency chain limits instruction-level parallelism. My optimized SMHasher3 DJB2 implementation can fold chunks for the add variant, but chunk-to-chunk state is still serial, and the XOR variant is harder to vectorize while preserving exact behavior.

None of that matters much for a shell prompt hashing a short hostname.

What I Would Use Link to heading

For a simple non-adversarial shell application, I would use FNV-1a-32.

If the result is used directly as a power-of-two table index, I would consider adding fmix32, especially when the extra arithmetic is acceptable. FNV-1a-32 already behaved well on my structured prompt keys, but final mixing is a cheap guardrail against low-bit weirdness.

If implementation complexity is acceptable, I would use Murmur3-32 instead. It has better quality, and the shell code is not terrible (as far as shell code goes…). It is just more machinery than I want in a shell prompt theme. 3

What I would not do is choose plain DJB2 for new code. Compatibility with existing DJB2 values is a legitimate reason to keep it. Nostalgia, Stack Overflow muscle memory, Reddit vibes, or “the AI suggested it” are not.

A Lesson about Vibe Coding Link to heading

There is a small lesson here about vibe coding, too. Coding assistants are very good at retrieving common snippets. That is useful when the common answer is still a good answer. DJB2 is exactly the sort of old snippet that remains easy to generate because it is short, famous, and heavily represented in public examples.

Folklore is a fine place to start a question. It is a poor place to end one.

A Note on the Color Palettes Link to heading

My shell prompt color palette is a separate problem from the hash function. For this use case, I wanted the colors to remain distinguishable under common forms of color-vision deficiency, and terminal light/dark themes, so Okabe-Ito and Paul Tol style palettes are better starting points than arbitrary terminal colors. 5 6 The hash only decides which slot a user@host key gets; it cannot make a bad palette accessible.

References Link to heading


  1. Daniel J. Bernstein’s DJB2, commonly cited through Ozan Yigit’s hash-function page. The original page is intermittently unavailable; see the djb2 crate documentation for the usual attribution, constants, and add/xor variants with a link back to the YorkU source. ↩︎

  2. Landon Curt Noll, Kiem-Phong Vo, Donald E. Eastlake 3rd, and Tony Hansen, RFC 9923: The FNV Non-Cryptographic Hash Algorithm, February 2026. ↩︎ ↩︎

  3. Austin Appleby, MurmurHash3 source in SMHasher, including MurmurHash3_x86_32 and the fmix32 finalizer. ↩︎ ↩︎

  4. Frank J. T. Wojcik, SMHasher3, a test suite for non-cryptographic hash functions. The GitLab project summary describes its focus on output distribution, collisions, and performance tests. ↩︎

  5. Masataka Okabe and Kei Ito, Color Universal Design resources, commonly associated with the Okabe-Ito categorical palette. ↩︎

  6. Paul Tol, Colour Schemes, technical notes on color-blind-safe qualitative, sequential, and diverging palettes. ↩︎