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 and zshenv 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.