Skip to main content

Command Palette

Search for a command to run...

The Two-Letter Bug in My Shell Swap

Updated
•2 min read•View as Markdown
S
I'm Sushanth, a high school senior in Hyderabad, India. This is where I write about the things I spend time on: learning from mistakes, building things in code, and the tools I actually work with day to day. I hold the Candidate Master title and a FIDE International Master norm, earned across roughly 140 rated tournaments in six countries. I stopped competing in 2025, but the habit competition taught me, sitting down after every loss and figuring out exactly where I went wrong, shapes how I approach everything else. On the technical side, I've gone deep on C/C++ through Duke University and University of London coursework. I'm also someone who likes a good terminal workflow and enjoys exploring (and abandoning) productivity tools. Expect posts on Neovim, tmux, what broke this week, and whatever I'm tinkering with.

After years in fish, I moved my whole shell config to zsh in one afternoon. Everyone warns you the hard part is rewriting every function and abbreviation. The hard part turned out to be one variable name.

My config was already portable in spirit. Twenty-six functions and seventy-three abbreviations lived in files both shells could read, so the port was mostly moving things into ~/.config/zsh and pointing zsh at them. zsh-abbr reads the abbreviations at startup. One line in .zshrc told it where they live, and every keybinding I had muscle-memorized just worked. The parts I expected to relearn, I never touched.

My fingers did not notice the switch, which is the nicest compliment I could give it. Most of a shell swap is muscle memory, and this one came through clean.

Then the bug. A few days in, my API keys stopped loading. The apikey function failed quietly, every key came back empty, and nothing printed an error. The only clue was the AI proxy rejecting calls with a missing-key error. A debug run of the function printed the key list perfectly, so the logic was fine. The failure was environmental, which is why I stared at it so long.

The culprit was local path in a ported function. In fish, path is a perfectly ordinary local variable. In zsh, it is not. zsh keeps $PATH in an array variable also named path, and declaring a local one shadows the real thing. Inside that function, PATH was empty, so op, the binary I use to read keys, could not be found. Every key load died without a sound.

The fix was renaming it to ref. One changed name, one line, done. A crash would have pointed at the line. A silent miss like this cost me a morning.

This is the pattern I keep rediscovering in migrations: the config moves easily, the gotchas do not. The features and syntax I expected to fight were the obedient part. The trap was language-specific naming, the kind of thing no migration guide lists because it only bites you inside a function.

zsh's path-as-PATH is a convenience for people who think in arrays. For someone like me who mostly thinks in strings, it was a landmine with a silent fuse.

Good shell. I am staying.