Fish Shell 4.6 Review: Best Interactive Shell, Wrong Default
Fish shell 4.6 is the most polished interactive shell in 2026, but the Rust rewrite did not fix POSIX. Here is why I run fish as a front-end and keep zsh for AI agents.
Fish ShellShellDeveloper ToolsClaude CodeRust
2187  Words
2026-04-18

Let me open with the contrarian line. Fish shell 4.6, released March 28, 2026, is the most polished interactive shell I have ever used. It is also the wrong choice for your default shell in the AI agent era.
Those two claims are not in conflict. They are the whole point of this review.
I have been running fish as a front-end for the last nine months, since the 4.0 Rust rewrite landed in February 2025. I read the release notes for every point release, I moved my zsh configuration over, I watched the Claude Code bug tracker fill up with fish in the title, and I eventually settled into a dual-shell setup that I think most AI-heavy developers should copy. This article is that conclusion, backed by numbers.
What actually changed in fish 4.0 through 4.6
The reason anyone is talking about fish in 2026 is the Rust port. Fish 4.0 shipped on February 27, 2025 after two years of work, 2,600+ commits, and more than 200 contributors — effectively a full rewrite. The headline numbers from the fish team’s own post-mortem:
- C++ lines: ~57,000 → Rust lines: ~75,000
- Binary size: 2.4 MB → 4.3 MB (about 25% larger on disk)
- Idle memory floor: 7 MB → 8 MB
- Execution time: “usually slightly better”
Since then the cadence has stayed tight. Fish 4.1 added nearly 1,400 commits of polish. Fish 4.6, the current release, added |& bash-compatible error redirection, systemd environment variable support (SHELL_PROMPT_PREFIX, SHELL_WELCOME), and — my favourite feature of the whole 4.x line — command-scoped abbreviations:
abbr --add --command git back 'reset --hard HEAD^'
That expansion only fires when the current command is git, so you can finally stop polluting the global namespace with aliases that collide across tools. For anyone who types git commands a few hundred times a day, this alone justifies running 4.6.
If you are still on fish 3.x, the upgrade is worth it — not because the shell is faster, but because the maintenance story is now sustainable. The old C++ codebase had 17 contributors with 10+ commits in eleven years. Since the Rust port, PR volume has jumped sharply. That is what you are actually buying with the rewrite.
Myth 1: “Rust rewrite = performance boost”
Every Rust port blog post collects comments like “fish is so much faster now.” It is not, and the fish team has been refreshingly honest about that.
Quoting the official rust-port write-up: the new binary produces “execution time usually slightly better” but with a higher memory floor at rest (8 MB vs 7 MB), with the Rust version winning only on the ceiling during intensive operations like directory globbing. On a cold start of an empty prompt, there is no human-perceivable difference.
The reason is obvious once you read the commit history. The team used autocxx to port components one file at a time while keeping the full test suite green. They were not rewriting for speed; they were translating semantics. Most hot paths kept their existing algorithms — they just got Rust ownership annotations wrapped around them.
Where Rust actually paid off is three places I only learned to care about after a year of daily use:
- Static binaries. The new fish ships as a self-contained file you can
scpto a server and run. No more “the ncurses version on this CentOS box is too old” dance. For SSH-heavy workflows this is a real quality-of-life upgrade. - Fearless concurrency. The C++ team tried a background-execution prototype and abandoned it because “objects were being shared across threads by accident.” Rust’s
Send/Syncnow makes that tractable, and you will see multithreaded features land in 4.7+ that were simply impossible before. - Contributor ergonomics. This one matters most for your long-term bet. The project went from “impossible to get ten new contributors per year” to a healthy flow of Rust-literate newcomers. Shells are forever; pick one whose bus factor is improving.
If you read “Rust rewrite” and expected a 3× speedup on your prompt, recalibrate. What you actually got is a shell that will still be maintained in 2030.
Myth 2: “POSIX compatibility does not matter in 2026”
This is the argument fish evangelists make most often, and it is where I think they are most wrong.
Yes, fish’s syntax is cleaner. No, you do not have to quote every variable. No, you never get bitten by [ "$x" = "$y" ] vs [[ $x == $y ]]. But POSIX compatibility is not about what you type — it is about what the rest of the ecosystem assumes. Specifically:
- Install scripts. The standard one-liner for almost every dev tool on the internet is
curl ... | shorcurl ... | bash. Nine times out of ten it works. The tenth time — typically when it sets environment variables or sources another script — it explodes under fish because it assumed POSIX variable assignment semantics. - Version managers.
nvm,rbenv,pyenv,asdf,sdkmanall ship bash/zsh hook scripts. Fish users rely on community-maintained wrappers (bass,fenv,fish-nvm) that lag the upstream versions by weeks and occasionally break on new releases. - Project scripts. Every repo with a
Makefile, ascripts/bootstrap.sh, or a.envrcassumes bash-flavored semantics. Running them under fish requires either a subshell hop or a translation step.
Here is the concrete test I use. When I evaluate a shell, I grep the install instructions for my last ten tools (Claude Code, uv, mise, bun, pnpm, rustup, nix, starship, atuin, direnv) and count how many have a “fish notes” section. For 2026, it is 6 out of 10. The remaining four work eventually, but require you to copy-paste a community gist and hope it is current.
The fish team is clearly aware of this tension — fish 4.6 added |& specifically “consistent with Bash.” That is the second major syntax borrow from bash in the 4.x line. Read that as quiet acknowledgment that pure syntactic purity costs more than it earns.
The AI agent hole: where fish 4.6 quietly falls apart
This is the section that did not exist three years ago, and it is now the most important one.
If you use Claude Code, Codex CLI, Cursor Agent, or any of the other code-writing agents that shell out to your system for half their work, the shell those agents spawn matters. And the state of the art in April 2026 is “agents run bash or zsh even if you launched them from fish.” Two documented issues make this concrete:
- GitHub issue anthropics/claude-code#7490 — the Bash tool uses the system default shell (usually bash on Linux, zsh on macOS) rather than inheriting the shell that invoked
claude. Your fish functions, abbreviations, and PATH tweaks silently disappear inside the agent. - GitHub issue anthropics/claude-code#13425 —
CLAUDE_CODE_SHELL_PREFIX, the hook users rely on to customize every command, sources zsh-format shell snapshots. Under fish, every command emits a syntax error preamble before running the real command. Effectively unusable.
Codex CLI and Gemini CLI have near-identical stories. An empirical study of 3,800+ publicly reported bugs across Claude Code, Codex, and Gemini CLI flags “shell-specific behaviors related to path parsing” as a recurring failure mode.
The blunt read: the AI coding stack standardized on POSIX shells roughly two years ago, and nobody is planning to rewrite it to be fish-native. So if your daily loop involves an agent running ten, fifty, or a hundred shell commands on your behalf, fish being your login shell actively costs you time — you get mysterious failures, lost environment, and prompts that look different inside and outside the agent.
This is the datum that flipped my opinion. A year ago I would have recommended fish to any developer who could tolerate a one-week transition. Today I would not.
The dual-shell setup: fish on top, POSIX underneath
You do not have to choose. The setup I have been running on an M4 MacBook Pro for seven months:
# ~/.config/fish/config.fish
# fish is my interactive login shell via `chsh -s (which fish)`
# but agents and scripts get a real POSIX shell.
set -Ux SHELL /bin/zsh # what agents inherit
set -gx EDITOR nvim
# Keep a one-liner for "drop me into zsh when I need it"
function z
command zsh $argv
end
Then in my Claude Code config (~/.claude/settings.json):
{
"env": {
"SHELL": "/bin/zsh",
"CLAUDE_CODE_SHELL_PREFIX": "source ~/.zshrc &>/dev/null;"
}
}
What this buys me:
- Interactive typing is fish. Autosuggestions, command-scoped abbreviations, tab completions, the lot.
- Every AI agent invocation is zsh. Claude Code, Codex CLI, any
sh -cfrom a tool — they all land in a POSIX shell that behaves the way their prompts were trained on. - Install scripts just work. When a tool’s docs say
curl ... | bash, I pipe it to bash. When it sayssource ./setup.sh, I do it fromzsh. Fish only handles what I personally type.
Visually, the split looks like this:
flowchart LR
User["Human typing
at the terminal"] --> Fish["fish 4.6
(login shell)"]
Agent["Claude Code
Codex CLI
Cursor Agent"] --> Zsh["/bin/zsh
(POSIX shell)"]
InstallScript["curl ... | bash
install scripts"] --> Bash["/bin/bash"]
Fish -->|"abbreviations
autosuggest
tab completion"| Out["Command Output"]
Zsh -->|"nvm / pyenv hooks
agent tool snapshots
project scripts"| Out
Bash -->|"export VAR=x
source file
POSIX assumptions"| Out
style Fish fill:#018472,color:#fff
style Zsh fill:#2b6cb0,color:#fff
style Bash fill:#4a5568,color:#fff
style Agent fill:#c53030,color:#fff
The rule of thumb: one human, one shell (fish). Every program you did not write, its own shell (zsh or bash). Once you internalise that split, 90% of the compatibility friction disappears.
Here is the decision tree I walk through when someone asks me “should I switch to fish in 2026?”:
flowchart TD
A[Want to switch to fish 4.6?] --> B{Do you use Claude Code / Codex / Cursor Agent
more than 2h/day?}
B -->|Yes| C{Comfortable maintaining
dual-shell config?}
B -->|No| D{Do you write bash scripts
more than 1x/week?}
C -->|Yes| E["Dual-shell: fish + zsh
(recommended for most)"]
C -->|No| F["Stay on zsh + starship
(lower cognitive tax)"]
D -->|Yes| G["Stay on zsh
(POSIX pays off daily)"]
D -->|No| H["Go full fish 4.6
(you will love it)"]
style E fill:#018472,color:#fff
style H fill:#018472,color:#fff
style F fill:#4a5568,color:#fff
style G fill:#4a5568,color:#fff
If you fall into the “full fish” bucket — mostly frontend engineers, data scientists who live in Python, or anyone whose “scripting” is actually Python/Go/TypeScript — fish is a straight upgrade. If you fall anywhere else, either go dual-shell or stay on zsh.
When fish 4.6 genuinely wins
To be clear, I am not negative on fish the project. It is the best version of its idea, and for the right user it is the right answer. Places where I think fish dominates in 2026:
- Zero-config ergonomics. A fresh fish install has autosuggestions, syntax highlighting, directory history, and tab completion with descriptions. Matching that in zsh requires Oh My Zsh plus three plugins plus a week of tuning.
- Startup time. A loaded
.zshrcwith Oh My Zsh and lazy-loaded plugins can still take 700–1200 ms on a modern Mac. Fish 4.6 cold-starts in under 100 ms even with a full config. If you open terminals constantly, that compounds. - Command-scoped abbreviations. Nothing in zsh, bash, or nushell matches fish 4.6’s
abbr --commandfor building muscle memory without globally colliding aliases. - Interactive discoverability. Tab completion in fish shows a full description for every command flag, pulled from man pages. This is how you learn unfamiliar tools in the flow of using them.
When fish 4.6 will cost you time
And honestly, the places it will still hurt in 2026:
- AI agent workflows. Every agent today speaks bash/zsh. Your fish-only PATH/env will silently not apply.
curl | shinstall scripts. Any unmaintained one assumes bash; fish will choke onexport,source, and array syntax differences.- Docker ENTRYPOINT and CI.
alpine:latestships ash; every CI image ships bash. Your local fish muscle memory does not transfer. - Bash one-liners in README files. You will find yourself running
bash -c '...'a few times a week. That friction compounds. - nvm, rbenv, asdf. Fish wrappers exist but lag upstream. If you are on bleeding-edge Node or Ruby versions, expect occasional breakage.
My recommendation, in one line
Run fish 4.6 as the shell you type in, run zsh as the shell your agents type in, and stop pretending you have to pick one. The fish Rust rewrite made the project sustainable enough to bet on for the next decade. It did not, and was never going to, fix the fact that every tool upstream speaks POSIX.
If you want a single takeaway to screenshot: copy the dual-shell config above, and you get fish’s ergonomics with zsh’s compatibility surface. That is the best answer in 2026.
Related reading
- Terminal Tools Guide for Developers — my earlier survey of iTerm2, Warp, tmux, starship, and the rest of the terminal stack.
- Claude Code Best Practices — how I configure environment and shell for agent-heavy workflows.
- Claude Code Skill and Sub-Agent Guide — context on why the agent’s shell environment matters.
- AI Dev Workflow: A Real Guide — where fish / zsh sits inside my day-to-day AI engineering loop.
Comments
Join the discussion — requires a GitHub account