TL;DR
If you're using zsh on FreeBSD, put your system-wide scripts in /usr/local/etc
, not /etc
. This behavior differs from Linux and isn’t always obvious—but once you know, everything falls into place.
Solving Zsh System-Wide Configuration Issues on FreeBSD
Recently, I was setting up a FreeBSD server and ran into a confusing issue: my system-wide zsh
login script wasn’t being sourced during SSH logins. I had installed zsh, and changed my default shell, and now wanted to source a few scripts for all logged in sessions. I first created the file /etc/zprofile
per the man zsh
documentation, which was responsible for sourcing any scripts residing in /etc/profile.d
and /usr/local/etc/profile.d/
. However, when I logged in, the scripts were not loaded. This post documents the debugging process and the solution I eventually discovered, which may save you time if you're doing something similar, and will hopefully help me out the next time im setting up a new machine.
The Problem
I wanted to configure system-wide shell behavior for all users using Zsh. On Linux, the typical way to do this is:
- Place your initialization code in
/etc/zprofile
or/etc/zshenv
- Optionally, source scripts from
/etc/profile.d/
.
So I created a file at /etc/zprofile
like this:
# /etc/zprofile
echo "zprofile loaded" >> /tmp/zsh.log
However, nothing was logged during SSH logins. The file wasn’t being sourced at all.
Debugging the Issue
Here’s what I checked:
- Zsh was my login shell (
chsh
) - The file existed and was readable (
ls -l /etc/zprofile
) - Permissions were correct (
chmod 644 /etc/zprofile
) - I even added debug lines to log to
/tmp
, but no output appeared
I tried using /etc/zshenv
to explicitly source /etc/zprofile
, and still nothing. I was beginning to suspect a deeper issue.
The Discovery
Eventually, I learned the crucial detail:
On FreeBSD, Zsh installed via
pkg
or ports is configured to load global config files from/usr/local/etc
, not/etc
.
That means:
- Files like
/etc/zprofile
and/etc/zshenv
are ignored - Zsh only loads
/usr/local/etc/zshenv
,/usr/local/etc/zprofile
, etc.
You can verify this with:
strings $(which zsh) | grep etc
The Solution
I moved my system-wide Zsh config to:
/usr/local/etc/zprofile
And voilà—everything worked during SSH logins.
If you want modular support for multiple profile scripts (like /etc/profile.d/*.sh
), you can use this:
# /usr/local/etc/zprofile
setopt null_glob
for dir in /usr/local/etc /etc; do
for f in "$dir"/profile.d/*.sh; do
[ -f "$f" ] && . "$f"
done
done
This structure respects FreeBSD’s separation of base and third-party config, while maintaining flexibility.
Key Takeaways
- Zsh on FreeBSD (via
pkg
or ports) is built with--sysconfdir=/usr/local/etc
- Therefore, system-wide config files like
zprofile
andzshenv
must be placed in/usr/local/etc
/etc/zprofile
will be ignored entirely- Debugging this requires checking Zsh's build paths and knowing how FreeBSD handles system configuration
Bonus Tip
If you're unsure which config files Zsh is sourcing, try this:
zsh -xv -l 2> /tmp/zsh-trace.log
This will dump all the shell startup steps into a log file—extremely helpful for tracing load order problems.