Skip to content

Instantly share code, notes, and snippets.

@krzyzanowskim
Last active May 26, 2026 20:24
Show Gist options
  • Select an option

  • Save krzyzanowskim/07450322713433af08798a6ab0c0ce8f to your computer and use it in GitHub Desktop.

Select an option

Save krzyzanowskim/07450322713433af08798a6ab0c0ce8f to your computer and use it in GitHub Desktop.
Load .env files from $HOME down to the current directory.
# load-dotenv-up.zsh
#
# Load .env files from $HOME down to the current directory.
# Deeper .env files override parent .env files.
# On directory change, variables touched by the previous .env chain are restored
# to their original values or unset if they did not exist before.
#
# NOTE: .env files must be valid zsh syntax.
# NOTE: Regular files and FIFO/named-pipe .env files are loaded.
# A FIFO .env may block the shell until something writes to it.
#
# Installation:
# 1. Save this file as:
# ~/.zsh/plugins/load-dotenv-up.zsh
#
# 2. Add this line to ~/.zshrc:
# source ~/.zsh/plugins/load-dotenv-up.zsh
#
# 3. Reload zsh:
# source ~/.zshrc
autoload -Uz add-zsh-hook
typeset -gA _dotenv_up_prev_value
typeset -gA _dotenv_up_prev_was_set
typeset -ga _dotenv_up_touched
_dotenv_up_snapshot_env() {
emulate -L zsh
local target="$1"
local name value
case "$target" in
before) before=() ;;
after) after=() ;;
*) return 1 ;;
esac
while IFS='=' read -r name value; do
# Environment names should be shell identifiers. Skip unusual names to avoid
# treating them as arithmetic expressions in associative-array subscripts.
[[ "$name" == [A-Za-z_][A-Za-z0-9_]* ]] || continue
case "$target" in
before) before[$name]="$value" ;;
after) after[$name]="$value" ;;
esac
done < <(env)
}
_dotenv_up_restore_previous() {
emulate -L zsh
local name
for name in "${_dotenv_up_touched[@]}"; do
if [[ "${_dotenv_up_prev_was_set[$name]}" == "1" ]]; then
export "$name=${_dotenv_up_prev_value[$name]}"
else
unset "$name"
fi
done
_dotenv_up_touched=()
_dotenv_up_prev_value=()
_dotenv_up_prev_was_set=()
}
_dotenv_up_load() {
emulate -L zsh
_dotenv_up_restore_previous
local cwd="${PWD:A}"
local home="${HOME:A}"
# Only load .env files when current directory is inside $HOME.
[[ "$cwd" == "$home" || "$cwd" == "$home"/* ]] || return 0
local -A before after
_dotenv_up_snapshot_env before
local dir="$cwd"
local -a dirs
dirs=()
while true; do
dirs=("$dir" "${dirs[@]}")
[[ "$dir" == "$home" ]] && break
dir="${dir:h}"
done
local envfile
for dir in "${dirs[@]}"; do
envfile="$dir/.env"
if [[ -f "$envfile" || -p "$envfile" ]]; then
setopt allexport
source "$envfile"
unsetopt allexport
fi
done
_dotenv_up_snapshot_env after
local name
# Variables added or changed by .env files.
for name in "${(@k)after}"; do
if [[ -z "${before[$name]+x}" || "${before[$name]}" != "${after[$name]}" ]]; then
_dotenv_up_touched+=("$name")
if [[ -n "${before[$name]+x}" ]]; then
_dotenv_up_prev_was_set[$name]=1
_dotenv_up_prev_value[$name]="${before[$name]}"
else
_dotenv_up_prev_was_set[$name]=0
fi
fi
done
# Variables removed by .env files.
for name in "${(@k)before}"; do
if [[ -z "${after[$name]+x}" ]]; then
_dotenv_up_touched+=("$name")
_dotenv_up_prev_was_set[$name]=1
_dotenv_up_prev_value[$name]="${before[$name]}"
fi
done
}
_dotenv_up_load
add-zsh-hook chpwd _dotenv_up_load
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment