Best Shells for FreeBSD: sh, bash, zsh, fish, and More
Your shell is the single piece of software you interact with most on a FreeBSD system. Every command you type, every script you write, every automation you build runs through it. Choosing the right shell affects your daily productivity, your scripting portability, and how much time you spend fighting your tools instead of using them.
FreeBSD ships with two shells in the base system: /bin/sh (a POSIX-compliant Bourne shell) and /bin/csh (the C shell, specifically tcsh). Everything else -- bash, zsh, fish, mksh -- lives in the ports and packages collection and requires a separate install. This is a deliberate design choice. FreeBSD keeps the base system lean and auditable, and the shell that drives the entire boot and init process (/bin/sh) is maintained alongside the kernel.
This guide compares six shells that are practical choices for FreeBSD in 2026: sh, bash, zsh, fish, tcsh, and mksh. Each section covers installation, configuration, scripting capabilities, and honest trade-offs. If you are setting up a new FreeBSD VPS or migrating from Linux, the shell you pick early on will shape everything that follows.
Quick Recommendation
If you want a single answer:
- System scripting and portability: Use sh. It is always present on FreeBSD, requires no installation, and produces scripts that run on any POSIX system. Every rc.d script, every Makefile shell command, and every base system utility uses sh.
- Interactive daily use for most users: Install zsh with Oh My Zsh. It combines powerful completion, plugin support, and broad community resources with excellent POSIX compatibility when you need it.
- Linux users transitioning to FreeBSD: Install bash. It removes one variable from an already significant transition. Once you are comfortable on FreeBSD, consider switching to zsh or fish.
- Users who want a modern shell without configuration: Install fish. Auto-suggestions, syntax highlighting, and web-based configuration work immediately after installation with no dotfile editing.
- BSD traditionalists and csh scripters: Stick with tcsh. It is in the base system, it is what FreeBSD used as the default root shell for decades, and your existing csh scripts will keep working.
- Korn shell fans and minimalists: Install mksh. It is small, fast, POSIX-compliant with Korn shell extensions, and runs well in jails and embedded FreeBSD systems.
The rest of this article explains why.
sh -- The FreeBSD Base System Shell
FreeBSD's /bin/sh is not the same shell you find on Linux. It is derived from the Almquist shell (ash) and has been maintained as part of FreeBSD's base system for decades. It is the shell that runs every rc.d service script, every periodic job, and every Makefile shell invocation. When you write #!/bin/sh on FreeBSD, you are getting a fast, audited, POSIX-compliant shell with no external dependencies.
Why Use sh
The primary reason to use sh is portability and reliability. Scripts written for /bin/sh will run on any POSIX-compliant system. There is no dependency on a package that might not be installed, might be a different version, or might behave differently after an upgrade. For system administration scripts, cron jobs, and anything that needs to run during early boot (before packages are available), sh is the only correct choice.
FreeBSD's sh is also faster than bash for script execution. It has a smaller memory footprint, starts faster, and parses scripts with less overhead. For scripts that spawn many subshells or run in tight loops, the difference is measurable.
Limitations
sh is deliberately minimal as an interactive shell. There is no command history search with Ctrl+R (though you can enable emacs or vi editing mode), no tab completion beyond basic filename expansion, no syntax highlighting, and no plugin ecosystem. Most FreeBSD administrators use sh for scripting and a different shell for interactive work.
Configuration
sh reads ~/.profile at login. A minimal useful configuration:
sh# ~/.profile export EDITOR=vim export PAGER=less export LANG=en_US.UTF-8 # Enable emacs-style line editing set -o emacs # Basic PATH for packages export PATH=/usr/local/bin:/usr/local/sbin:$PATH
When to Use sh
Use sh for every system script, every cron job, every automation task that must work without assumptions about installed packages. Use a different shell for interactive sessions where you type commands by hand.
bash -- The Most Portable Interactive Shell
bash (Bourne Again Shell) is the default shell on nearly every Linux distribution, macOS (prior to Catalina), and most CI/CD environments. If you are coming to FreeBSD from Linux, bash is what you know. Installing it removes one learning curve from an already significant platform transition.
Installation
shpkg install bash
This installs bash to /usr/local/bin/bash and adds it to /etc/shells automatically.
Why Use bash
bash has the largest ecosystem of any shell. Stack Overflow answers, tutorials, books, and automation tooling overwhelmingly assume bash. If you paste a shell snippet from the internet, it will almost certainly be bash syntax. The completion system (bash-completion) covers thousands of commands. Programmable completion, associative arrays, process substitution, and [[ test expressions make interactive use comfortable and scripting flexible.
bash also has strong job control, excellent history management (with HISTCONTROL, HISTIGNORE, and reverse search via Ctrl+R), and a mature set of built-in commands that reduce the need for external processes.
Configuration
bash reads ~/.bash_profile at login and ~/.bashrc for interactive non-login shells. A practical FreeBSD configuration:
bash# ~/.bashrc export EDITOR=vim export PAGER=less export CLICOLOR=1 # History HISTSIZE=10000 HISTFILESIZE=20000 HISTCONTROL=ignoredups:erasedups shopt -s histappend # Prompt with hostname and working directory PS1='\u@\h:\w\$ ' # Enable programmable completion if [ -f /usr/local/share/bash-completion/bash_completion ]; then . /usr/local/share/bash-completion/bash_completion fi
Install the completion package separately:
shpkg install bash-completion
Limitations on FreeBSD
bash is not in the base system. This means:
- Scripts starting with
#!/bin/bashwill fail on a fresh FreeBSD install. - Using bash for system scripts creates a package dependency where none is needed.
- The bash binary lives at
/usr/local/bin/bash, not/bin/bash. Scripts from Linux that hardcode/bin/bashwill break unless you create a symlink or use#!/usr/bin/env bash.
For system scripting, always prefer /bin/sh. Use bash for interactive sessions and user-level scripts where you control the environment.
zsh -- The Power User's Shell
zsh is the shell that tries to be everything: POSIX-compatible, bash-compatible (mostly), feature-rich, and infinitely customizable. It has been the default shell on macOS since Catalina, which brought a surge of community investment in themes, plugins, and frameworks.
Installation
shpkg install zsh
For the full experience, also install the syntax highlighting and auto-suggestions plugins:
shpkg install zsh-syntax-highlighting zsh-autosuggestions
Why Use zsh
zsh's completion system is the best of any shell. It can complete command options, file paths, package names, git branches, SSH hosts, and virtually anything else with context-aware intelligence. The compinit system is powerful enough that most commands have full completion definitions maintained by the community.
Beyond completion, zsh offers:
- Glob qualifiers -- select files by type, size, modification time, and ownership directly in glob patterns.
ls (.)lists only regular files.ls (.m-7)lists files modified in the last seven days. - Recursive globbing --
*/.confexpands to all.conffiles in all subdirectories, nofindrequired. - Spelling correction -- zsh suggests corrections for mistyped commands.
- Extended history -- timestamps for every command, shared history across sessions, and substring search.
- Parameter expansion flags -- transform variables inline:
${(U)var}uppercases,${(j:,:)array}joins an array with commas.
Oh My Zsh and Plugin Frameworks
Oh My Zsh is the most popular zsh framework, providing a curated collection of plugins, themes, and helper functions:
shsh -c "$(fetch -o - https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
Popular plugins include git (aliases and status in prompt), sudo (press Escape twice to prepend sudo), docker, kubectl, and fzf.
For prompt customization, Powerlevel10k delivers a fast, information-dense prompt that shows git status, execution time, background jobs, and more:
shgit clone --depth=1 https://github.com/romkatv/powerlevel10k.git \ ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
Set ZSH_THEME="powerlevel10k/powerlevel10k" in ~/.zshrc and run p10k configure for an interactive setup wizard.
Configuration
zsh reads ~/.zshrc for interactive shells. A minimal FreeBSD configuration without a framework:
zsh# ~/.zshrc autoload -Uz compinit && compinit autoload -Uz promptinit && promptinit # History HISTFILE=~/.zsh_history HISTSIZE=50000 SAVEHIST=50000 setopt SHARE_HISTORY setopt HIST_IGNORE_DUPS # Key bindings bindkey -e # emacs mode bindkey '^R' history-incremental-search-backward # Prompt PROMPT='%n@%m:%~%# ' # Plugins (if installed via pkg) source /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh 2>/dev/null source /usr/local/share/zsh-autosuggestions/zsh-autosuggestions.zsh 2>/dev/null
Scripting with zsh
zsh scripts can use POSIX syntax (with emulate sh or emulate ksh), but native zsh scripting is its own dialect. This is a double-edged sword: zsh scripts are powerful but not portable. For scripts that only run on machines you control (where zsh is installed), the extended syntax saves time. For anything shared or deployed broadly, write in sh.
fish -- The Modern Interactive Shell
fish (Friendly Interactive Shell) takes a fundamentally different approach from every other shell on this list. It does not try to be POSIX-compatible. It does not read .bashrc or .zshrc. Instead, it provides a clean, consistent syntax and out-of-the-box features that other shells only achieve through plugins and configuration.
Installation
shpkg install fish
Why Use fish
fish is the only shell where the default experience is genuinely good. On first launch, you get:
- Auto-suggestions from command history, displayed in gray as you type. Press the right arrow to accept.
- Syntax highlighting in the command line itself. Valid commands appear in one color, invalid commands in red, valid file paths are underlined.
- Tab completion with descriptions for each candidate, not just filenames.
- Web-based configuration via
fish_config, which opens a browser interface for setting colors, prompts, and functions. - Man page completions generated automatically from installed man pages.
There is no configuration step. No framework to install. No plugin manager to bootstrap. fish works well immediately.
Configuration
fish uses ~/.config/fish/config.fish instead of the traditional dotfile locations:
fish# ~/.config/fish/config.fish set -gx EDITOR vim set -gx PAGER less # Add local binaries to PATH fish_add_path /usr/local/bin /usr/local/sbin # Custom greeting (or disable it) set fish_greeting ""
fish functions live in ~/.config/fish/functions/ as individual files. To create a function:
fishfunction ll --description "Long listing" ls -lah $argv end funcsave ll
The POSIX Incompatibility Trade-off
fish's syntax is not POSIX-compatible, and this is the single biggest decision point. Variable assignment is set var value, not var=value. Command substitution is (command), not $(command) or backticks. Conditionals use if; ...; end, not if; then; ...; fi. Piping stderr requires 2>|, not 2>&1 |.
This means:
- You cannot paste bash one-liners into fish without translation.
- Shell scripts with
#!/bin/fishare not portable. - Many tutorials and Stack Overflow answers will not work verbatim.
- CI/CD environments almost never use fish.
For interactive daily use, these differences become natural within a few days. For scripting, write your scripts in sh or bash and call them from fish. Fish is an interactive shell, not a scripting language.
Plugin Management
fish has a package manager called Fisher:
shcurl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher
Popular plugins include jethrokuan/z (directory jumping), PatrickF1/fzf.fish (fuzzy search integration), and jorgebucaran/nvm.fish (Node version management).
tcsh -- The BSD Heritage Shell
tcsh (TENEX C Shell) has been part of FreeBSD's base system since the beginning. For years it was the default interactive shell for root on FreeBSD. It is the enhanced version of csh, adding command completion, history editing, and programmable word completion to the original C shell syntax.
Why Use tcsh
tcsh requires no installation. It is /bin/csh and /bin/tcsh on every FreeBSD system, always available, even on the most minimal install. If you administer many FreeBSD machines and need a consistent interactive experience without relying on packages, tcsh is the pragmatic choice.
tcsh also supports:
- Programmable completion with the
completebuiltin. - History search and editing.
- Spelling correction.
- Periodic commands that run at configurable intervals.
- Login/logout watch for monitoring user sessions.
Configuration
tcsh reads ~/.cshrc for interactive shells and ~/.login at login:
csh# ~/.cshrc set prompt = '%n@%m:%~%# ' set autolist set color set colorcat set history = 5000 set savehist = (5000 merge) # Tab completion complete cd 'p/1/d/' complete man 'p/1/c/' # Environment setenv EDITOR vim setenv PAGER less setenv PATH /usr/local/bin:/usr/local/sbin:${PATH}
Limitations
tcsh's scripting language is widely considered unsuitable for serious programming. The C shell syntax has well-documented pitfalls (Tom Christiansen's "Csh Programming Considered Harmful" remains relevant). Variable handling, quoting, and control flow are inconsistent and error-prone compared to Bourne-family shells.
Use tcsh for interactive sessions if you prefer its syntax. Write scripts in sh.
mksh -- The Korn Shell for Minimalists
mksh (MirBSD Korn Shell) is a compact, standards-compliant shell descended from pdksh. It implements the Korn shell language with POSIX compliance and adds useful extensions without bloat. mksh is the default shell on Android (as part of the AOSP toolkit), which makes it one of the most widely deployed shells in terms of device count.
Installation
shpkg install mksh
Why Use mksh
mksh is small. The binary is a fraction of the size of bash or zsh, it starts instantly, and it uses minimal memory. This makes it an excellent choice for:
- FreeBSD jails where you want a capable interactive shell without pulling in large dependency trees.
- Embedded FreeBSD systems running on constrained hardware.
- Scripting where you want Korn shell features (coprocesses,
selectloops,printbuiltin, extended pattern matching) with POSIX portability.
mksh is also fully compatible with most bash scripts that do not use bash-specific features like associative arrays or [[ with regex matching. It handles many common bashisms through a compatibility mode.
Configuration
mksh reads ~/.mkshrc for interactive shells:
sh# ~/.mkshrc export EDITOR=vim export PAGER=less export HISTFILE=~/.mksh_history export HISTSIZE=10000 PS1='${USER}@$(hostname -s):${PWD}$ ' set -o emacs
Comparison Table
| Feature | sh | bash | zsh | fish | tcsh | mksh |
|---|---|---|---|---|---|---|
| In FreeBSD base | Yes | No | No | No | Yes | No |
| POSIX compliant | Yes | Mostly | Yes (emulate) | No | No | Yes |
| Install command | -- | pkg install bash | pkg install zsh | pkg install fish | -- | pkg install mksh |
| Binary path | /bin/sh | /usr/local/bin/bash | /usr/local/bin/zsh | /usr/local/bin/fish | /bin/tcsh | /usr/local/bin/mksh |
| Auto-suggestions | No | No | Plugin | Built-in | No | No |
| Syntax highlighting | No | No | Plugin | Built-in | No | No |
| Tab completion | Basic | Good | Excellent | Excellent | Good | Basic |
| Plugin ecosystem | None | Moderate | Large (Oh My Zsh) | Moderate (Fisher) | None | None |
| Scripting quality | Excellent | Excellent | Good | Poor (non-POSIX) | Poor | Good |
| Startup speed | Fastest | Moderate | Moderate-slow | Moderate | Fast | Fast |
| Memory usage | Lowest | Moderate | Moderate-high | Moderate | Low | Low |
| Learning curve | Low | Low | Moderate | Low | Moderate | Low |
| Best for | Scripting, system | General use | Power users | Casual/modern | BSD traditionalists | Jails, minimal |
Changing Your Default Shell on FreeBSD
FreeBSD uses chsh to change a user's login shell. The new shell must be listed in /etc/shells.
Check Available Shells
shcat /etc/shells
On a fresh install with bash and zsh installed via pkg, this will show:
shell/bin/sh /bin/csh /bin/tcsh /usr/local/bin/bash /usr/local/bin/zsh
Change Your Shell
shchsh -s /usr/local/bin/zsh
This changes the shell for the current user. You will need to log out and back in for the change to take effect.
For the root user, be cautious. If you set root's shell to a package-installed shell and that package breaks or is removed, you will be unable to log in as root. The standard practice is to keep root's shell as /bin/sh or /bin/tcsh (both in the base system) and use su - or sudo to access a different shell:
shsudo -u youruser /usr/local/bin/zsh
Adding a Shell to /etc/shells
If you install a shell and it does not appear in /etc/shells, add it manually:
shecho "/usr/local/bin/fish" >> /etc/shells
Most FreeBSD packages handle this automatically during installation, but verify before running chsh.
Scripting Comparison: sh vs bash vs zsh
For system scripts on FreeBSD, use /bin/sh. Period. Here is why, illustrated with a practical example -- checking if a service is running and restarting it:
POSIX sh (portable, runs everywhere):
sh#!/bin/sh service_name="nginx" if service "$service_name" status > /dev/null 2>&1; then echo "$service_name is running" else echo "$service_name is down, restarting..." service "$service_name" start fi
bash (adds convenience, requires package):
bash#!/usr/local/bin/bash service_name="nginx" if service "$service_name" status &>/dev/null; then echo "$service_name is running" else echo "$service_name is down, restarting..." service "$service_name" start fi
zsh (works, but adds nothing here):
zsh#!/usr/local/bin/zsh service_name="nginx" if service "$service_name" status &>/dev/null; then echo "$service_name is running" else echo "$service_name is down, restarting..." service "$service_name" start fi
The sh version works on every FreeBSD system, every Linux system, every POSIX environment. The bash and zsh versions require their respective packages. For system automation, portability wins.
Where bash and zsh scripting become genuinely useful is in complex user-level scripts: associative arrays, advanced string manipulation, regex matching in conditionals, and richer error handling. If you are writing a personal tool that will only run on machines you control, bash or zsh scripts can be more expressive. For anything shared or deployed, stick with sh.
Customization: Dotfiles, Prompts, and Plugins
Prompt Customization
Every shell has its own prompt syntax:
- sh/bash:
PS1='\u@\h:\w\$ '-- username, hostname, working directory. - zsh:
PROMPT='%n@%m:%~%# '-- similar information, different escape syntax. - fish: Use
fish_promptfunction or thefish_configweb UI. - tcsh:
set prompt = '%n@%m:%~%# '-- similar to zsh.
Dotfile Organization
A common approach for managing shell configuration across multiple FreeBSD machines is to keep dotfiles in a git repository and symlink them:
shgit clone https://github.com/youruser/dotfiles.git ~/dotfiles ln -sf ~/dotfiles/.zshrc ~/.zshrc ln -sf ~/dotfiles/.bashrc ~/.bashrc
Tools like GNU Stow (available as pkg install stow) automate this symlink management.
Plugin Managers by Shell
| Shell | Plugin Manager | Install |
|---|---|---|
| bash | basher, bash-it | Manual |
| zsh | Oh My Zsh, zinit, antidote | Script |
| fish | Fisher, Oh My Fish | Script |
| tcsh | None | Manual config |
| mksh | None | Manual config |
Performance Considerations
For most interactive use, shell startup time and execution speed are irrelevant -- you will not notice the difference between sh starting in 5ms and zsh starting in 50ms. But there are scenarios where performance matters:
- Jails and containers: If you run hundreds of jails, the memory overhead of each shell adds up. sh and mksh have the smallest footprint.
- Script-heavy workloads: Batch jobs that spawn thousands of subshells will run measurably faster with sh than bash. FreeBSD's sh is optimized for this.
- Slow zsh startup: A heavily configured zsh with many plugins can take 500ms or more to start. Powerlevel10k's instant prompt feature mitigates this by showing the prompt immediately while plugins load in the background.
- fish's universal variables: fish stores variables in a file and synchronizes across sessions, which adds a small I/O cost per variable change. This is negligible for interactive use but visible in benchmarks.
If startup time matters to you, profile it:
shtime /bin/sh -i -c exit time /usr/local/bin/bash -i -c exit time /usr/local/bin/zsh -i -c exit time /usr/local/bin/fish -i -c exit
Frequently Asked Questions
What is the default shell on FreeBSD?
The default shell for the root user on FreeBSD is /bin/sh (since FreeBSD 14.0; prior releases used /bin/csh). For regular users created during installation, the default depends on the installer options but is typically /bin/sh. Both sh and tcsh are available in the base system without installing any packages.
Can I use bash on FreeBSD?
Yes. Install it with pkg install bash, then change your shell with chsh -s /usr/local/bin/bash. Note that the binary is at /usr/local/bin/bash, not /bin/bash. Scripts from Linux that use #!/bin/bash will need the path adjusted to #!/usr/local/bin/bash or changed to #!/usr/bin/env bash for portability.
Should I use zsh or fish on FreeBSD?
It depends on your priorities. Choose zsh if you want maximum customization, POSIX compatibility when needed, and access to the largest plugin ecosystem. Choose fish if you want the best out-of-the-box experience with no configuration. zsh with plugins can match fish's features, but fish delivers them by default.
Is it safe to change root's shell to bash or zsh?
It is not recommended. If the package providing your shell is removed, corrupted, or unavailable during a boot failure, you will not be able to log in as root. Keep root's shell set to /bin/sh or /bin/tcsh (both in the base system). Use su - youruser or sudo -i to access your preferred shell after logging in.
Why does FreeBSD use sh instead of bash?
FreeBSD's base system is developed and maintained independently from third-party packages. sh is part of the base system because it is small, audited, POSIX-compliant, and has no external dependencies. bash is a GNU project with a different license (GPLv3), a larger codebase, and a separate release cycle. Using sh for system scripts ensures they work on every FreeBSD installation regardless of what packages are installed.
How do I make my bash scripts work on FreeBSD?
The most common fix is changing the shebang line from #!/bin/bash to #!/usr/bin/env bash, which finds bash wherever it is installed. For scripts that use only basic features, consider rewriting them with #!/bin/sh for true portability. If your script uses bash-specific features like arrays, [[ ]], or <<< here-strings, ensure bash is installed on the target system.
Which shell is best for FreeBSD jails?
For minimal jails, use /bin/sh -- it is always available and adds zero package overhead. If you need a comfortable interactive shell inside a jail, mksh is the lightest option that still provides Korn shell features. For development jails where size is not a concern, install whatever shell you prefer.
Final Recommendation
There is no single best shell for FreeBSD. There is the best shell for your use case:
- Write all system scripts in sh. No exceptions. Portability and reliability matter more than convenience.
- For interactive daily use, install zsh with a minimal set of plugins. It gives you the best balance of features, compatibility, and community support.
- If you are new to Unix or want zero configuration, install fish. Accept the POSIX incompatibility trade-off and write your scripts in sh.
- Keep root's shell as /bin/sh or /bin/tcsh. Never set it to a package-installed shell.
The shell is a tool. Pick one, learn it well, and spend your time building things instead of tweaking your prompt. You can always switch later -- chsh is one command away.