The filename that works in bash and breaks in zsh

I was writing a capture script the other day. It replays a client API's requests and saves each response as a JSON fixture, one file per request, with the request parameters baked into the filename. The API joins compound keys with a pipe, so a request looks like invhist/1/987654|12345. A pipe in a filename is technically legal too, but I'm not a monster, so I reached for a substitute character and typed the first shell-safe-looking thing that came to mind:
invhist_1_987654~12345.jsonThe review comment arrived almost immediately: "A tilde!? In a filename!? On Linux!? Of all possible characters!?"
My first instinct was to defend it. The tilde is mid-word. Expansion doesn't touch it. I even had the receipts ready. Then I sat down to write out exactly why it was safe, and the write-up turned out to be longer than the fix. That's usually the answer right there, but the rules themselves are worth knowing, because they're weirder than I remembered.
What the filesystem thinks
Nothing. ext4, xfs, btrfs, all of them accept any byte in a filename except the slash and NUL. This is perfectly fine:
touch 'invhist_2_Y~12345.json'The kernel doesn't blink. If legality were the bar, the discussion would be over.
What bash thinks
Bash performs tilde expansion only when the ~ starts a word, or follows = or : in a variable assignment. That last part surprises people:
x=~/bin; echo $x
# /Users/dusan/binNo quotes, no word start, still expanded, because it follows the = of an assignment. The same happens for each ~ after a colon in something like PATH=~/bin:~/tools.
Mid-word, though, nothing happens. ls invhist_2_Y~12345.json finds the file every time.
So what about a file that starts with a tilde? Here's where it gets interesting:
bash -c 'echo ~nosuchuser.json'
# ~nosuchuser.jsonBash tries to interpret nosuchuser.json as a login name, fails to find such a user, and, per the manual, leaves the word unchanged. So rm ~backup.json deletes the file named ~backup.json. Not because the operation is safe, but because a username lookup failed on the way there. The command works by accident.
What zsh thinks
Same word, different shell:
zsh -c 'echo ~nosuchuser.json'
# zsh:1: no such user or named directory: nosuchuser.jsonzsh treats the failed lookup as an error and refuses to run the command at all. The file that bash handles fine is untouchable in an unquoted zsh command line. And zsh is the default shell on every Mac your coworkers are typing on.
That's the part that killed my defense. The tilde wasn't dangerous where I'd put it. But whether a tilde in a filename behaves depends on its position in the name, its position in the word, the shell you're using, and in one branch of that decision tree, the contents of /etc/passwd. Nobody should need that flowchart to handle a fixture file.
What everything else thinks
There's one more layer. A trailing tilde is the classic editor backup convention: file.txt~ is what Emacs and a family of older tools leave behind. Which means *~ shows up in cleanup one-liners, in Makefile clean targets, and in more .gitignore templates than you'd expect. A generated file that ends in a tilde is one inherited ignore rule away from silently vanishing from version control.
What I did
Replaced it with a comma:
_save "${file}_${key}_${param//|/,}.json" ...invhist_1_987654,12345.json. A comma is inert in every shell, legal on every filesystem I deploy to, can't collide with the data (the params are account numbers and date stamps), and reads like what it is: a list separator. One character changed, zero rules to remember.
Takeaway
"Is it legal?" is the wrong question for filenames. The right question is whether the next person, or the next script, needs to know a positional expansion rule to handle the file safely. If the answer is yes, pick a more boring character. Save the tilde for your home directory, where it's doing its best work.
Got a shell quirk that survived a code review argument? Send me the details and I'll take a look.
Previous: Dart Sass quietly reordered my CSS