← All posts

How I use a single .zshrc file across macOS and Windows (WSL2)

By Tal Koren ·

Ever since The Matrix came out in 1999, I've been into command line interfaces. The feeling of knowing how to navigate your way around verbose interfaces has its magic (that is also part of the reason why I like CSS, vim and RegEx).

Today I have 3 machines - my personal MacBook Pro, and two desktop Windows gaming PCs (long story, don't ask). Being the CLI geek that I am, I started playing around with WSL back in 2016 (and afterwards WSL2, which runs an actual Linux kernel inside a lightweight virtual machine).

Since then I've been a huge Zsh user, and I use it across all my machines. This also comes with the burden of managing dotfiles, and .zshrc among them.

Maintaining separate .zshrc.mac and .zshrc.wsl files would inevitably make these two drift apart from each other, and as a developer with ADHD, I know it's just a matter of time until I update one and forget the other. Hence my rule for managing this was to have a file that's shared by default, and conditional when the OS actually matters.

Managing my dotfiles via git

To keep this file (and a few others) from drifting across machines, I manage my dotfiles as a bare git repo with $HOME as the working tree, without symlinks or a separate checkout directory. df, a small script wired into my .zshrc with its own tab completion, is my git interface for it: df sync "message", df push, df pull. The full setup, including bootstrapping, lives in the repo itself.

Cutting to the chase

We start with distinguishing between WSL (on Windows), and macOS:

if grep -qi microsoft /proc/version 2>/dev/null; then
    IS_WSL=1
fi

I set the IS_WSL variable to distinguish between the operating systems. The q flag is for quiet (we don't want the grep output to print every time a shell starts), and i makes the check case-insensitive, so it does not depend on how Microsoft is capitalized.

SSH

I use the 1Password ssh agent to avoid keeping my private keys in ~/.ssh (sorry AI agents and supply chain attackers), and it's supported in macOS, Linux, and Windows. This distinction matters because the 1Password ssh agent runs as part of the Windows app and not inside WSL, so to reach it, WSL needs to go through Windows' own ssh client rather than spinning up its own agent.

After configuring 1Password to enable me to use their CLI and configuring the ssh agent, all I had to do was:

if [ "$IS_WSL" = 1 ]; then
    alias ssh='ssh.exe'
    alias ssh-add='ssh-add.exe'
fi

This works in the interactive shell, but git doesn't go through Zsh at all - it runs ssh via a PATH lookup, so the alias never applies. That's why I need GIT_SSH_COMMAND set to ssh.exe.

Also, if I want support for scp, Windows has me covered in that aspect as well.

The resulting conditional in that case, is:

if [ "$IS_WSL" = 1 ]; then
    alias ssh='ssh.exe'
    alias ssh-add='ssh-add.exe'
    alias scp='scp.exe'
    export GIT_SSH_COMMAND='ssh.exe'
fi

Now I can handle all ssh-related operations across macOS and WSL.

Using Finder and Explorer

Windows has explorer.exe (the default UI shell being used to browse files and folders), and macOS has Finder. Simply calling open . in macOS opens Finder in the current directory you're in, while calling explorer.exe . in Windows does the same with Explorer. You see where this is going...

Since open is already a system command on macOS, I only need to overwrite it when using WSL on Windows:

open() {
    explorer.exe "$(wslpath -w "${1:-.}")" 2>/dev/null
}

And since this is irrelevant to macOS, it makes sense to only set it under WSL, making our conditional:

if [ "$IS_WSL" = 1 ]; then
    alias ssh='ssh.exe'
    alias ssh-add='ssh-add.exe'
    alias scp='scp.exe'
    export GIT_SSH_COMMAND='ssh.exe'

    open() {
        explorer.exe "$(wslpath -w "${1:-.}")" 2>/dev/null
    }
fi

Simplicity is king

The result is one .zshrc that behaves like a native configuration on both systems. Most of it stays shared, and the handful of Windows-specific differences remain visible in one small conditional. Predictable, readable, and sane to maintain (mainsane?).

Enjoyed this?

I curate stuff like this daily on TechPicks, for ~1,500 developers who like to stay ahead of the curve.