共计 6560 个字符,预计需要花费 17 分钟才能阅读完成。

我最近在使用 [[Claude Code]] 的时候遇到了一个让人恼火的问题:我在 ~/.env 文件里配置了好几个 API Key,通过 .zshrc 加载后在终端里用得好好的,但 Claude Code 内部执行命令时却死活找不到这些环境变量。排查了半天才发现,问题出在 shell 的加载机制上——.zshrc 只在交互式 shell 中执行,而很多工具启动的子进程并不走这条路。更深层的问题是,我的 .zshrc 是放在 [[GitHub]] dotfiles 仓库里公开同步的,如果不小心把 source ~/.env 的逻辑和密钥文件搞混了,密钥就有泄漏的风险。
这件事让我重新审视了一下自己的环境变量管理方式。翻了一圈之后,我发现 [[direnv]] 几乎是为这类问题量身定做的工具——它能按目录自动加载环境变量,进入目录时自动生效,离开时自动清除,而且有一套完善的安全机制来防止未授权的 .envrc 文件被执行。
direnv 是什么
direnv 是一个用 [[Go]] 编写的 shell 扩展工具,核心功能只有一个:根据你所在的目录,自动加载和卸载环境变量。它通过在 shell 的每次提示符(prompt)渲染之前插入一个钩子(hook)来工作。每当你 cd 到一个目录,direnv 会检查当前目录及其所有父目录是否存在 .envrc 文件,如果有,就在一个 bash 子进程中执行这个文件,然后把导出的环境变量差异(diff)注入到你当前的 shell 中。当你离开这个目录时,这些变量会被自动移除,恢复到之前的状态。
这个设计思路非常优雅。传统做法是把所有项目的环境变量都堆在 .zshrc 或 .bash_profile 里,时间长了这些文件就变成了一锅粥——哪些变量是哪个项目用的、哪些已经过期了、哪些之间有冲突,根本理不清。direnv 把环境变量的生命周期和目录绑定在一起,让每个项目只关心自己需要的变量,互不干扰。而且因为 direnv 本身是一个编译好的静态二进制文件,hook 的执行速度快到你完全感觉不到它的存在。
安装和配置
安装
在 [[macOS]] 上用 [[Homebrew]] 安装是最方便的:
brew install direnv
[[Ubuntu]] / [[Debian]] 用户可以直接通过 apt 安装:
sudo apt-get update && sudo apt-get install -y direnv
[[Arch Linux]] 用户:
pacman -S direnv
如果你的系统包管理器中没有 direnv,也可以直接从 [[GitHub]] 下载预编译的二进制文件:
curl -sfL https://direnv.net/install.sh | bash
安装完成后可以用 direnv version 确认一下版本,截至 2026 年 3 月,最新版本是 v2.37.1。
Shell 集成
安装 direnv 本身只是第一步,你还需要把它的 hook 集成到你的 shell 中。这一步很关键,因为 direnv 的自动加载能力完全依赖这个 hook。
对于 [[Zsh]] 用户,在 ~/.zshrc 的末尾添加:
eval "$(direnv hook zsh)"
[[Bash]] 用户,在 ~/.bashrc 的末尾添加:
eval "$(direnv hook bash)"
[[Fish]] 用户,在 ~/.config/fish/config.fish 中添加:
eval (direnv hook fish)
有一点需要注意,direnv 的 hook 应该放在其他 shell 扩展(比如 rvm、git-prompt 之类)的后面。添加完之后重启 shell 或者 source 一下配置文件就生效了。
核心用法
基本操作
direnv 的日常使用其实非常简单。假设你有一个项目目录 ~/projects/myapp,你想为它设置一些专属的环境变量,只需要在这个目录下创建一个 .envrc 文件:
cd ~/projects/myapp
echo 'export DATABASE_URL="postgres://localhost/myapp_dev"' > .envrc
echo 'export API_KEY="sk-your-secret-key"' >> .envrc
第一次创建 .envrc 后,direnv 会显示一个安全提示,告诉你这个文件还没有被授权。你需要手动运行 direnv allow 来批准它:
direnv allow
之后每次进入这个目录,环境变量就会自动加载;离开目录后,变量自动消失。如果你修改了 .envrc 的内容,direnv 会再次要求你执行 direnv allow,因为它是基于文件内容的哈希值来判断授权状态的,任何改动都会使之前的授权失效。
常用命令
direnv 的命令不多,但每一个都很实用:
direnv allow— 授权当前目录的.envrc文件direnv deny— 撤销授权,阻止.envrc加载direnv edit— 用$EDITOR打开.envrc进行编辑,保存后自动执行allowdirenv status— 查看当前 direnv 的状态和调试信息direnv reload— 重新加载当前目录的.envrcdirenv prune— 清理已经不存在的授权记录direnv exec <dir> <command>— 在指定目录的环境下执行命令
其中 direnv edit 是我最推荐的编辑方式,因为它省去了编辑完之后还要手动 allow 的步骤。
加载 .env 文件
很多项目已经在用 .env 文件来管理环境变量(比如 [[Docker Compose]]、[[Node.js]] 项目等)。direnv 可以直接加载这些文件,你只需要在 .envrc 中写一行:
dotenv
或者用更安全的版本,允许 .env 文件不存在:
dotenv_if_exists
这样做的好处是,你可以把 .env 放在 .gitignore 里保存实际的密钥值,而 .envrc 文件只包含加载逻辑,可以安全地提交到版本控制中。回到我开头提到的问题,这恰恰就是我需要的方案——在项目目录下放一个 .envrc 写上 dotenv_if_exists,再把实际的 API Key 放在同目录的 .env 里,两者分离,既不会污染全局环境,也不会有密钥泄漏的风险。
加载全局 .env 文件
如果你和我一样,把通用的 API Key 都放在 ~/.env 里,可以在项目的 .envrc 中加载它:
dotenv_if_exists ~/.env
或者更灵活地,在 direnv 的全局配置文件 ~/.config/direnv/direnvrc 中定义一个自定义函数:
# ~/.config/direnv/direnvrc
load_global_env() {
if [[ -f "$HOME/.env" ]]; then
dotenv "$HOME/.env"
fi
}
然后在任何项目的 .envrc 中调用 load_global_env 就可以了。
stdlib 常用函数
.envrc 本质上是一个 bash 脚本,但 direnv 提供了一套内置的标准库函数(stdlib),让常见操作变得更简洁。这些函数是 direnv 的核心优势之一,远不只是 export 那么简单。
PATH 管理
# 将项目的 bin 目录添加到 PATH 前面
PATH_add bin
# 将项目的 scripts 目录也加上
PATH_add scripts
# 为任意环境变量添加路径
path_add GOPATH "$(pwd)/go"
PATH_add 比手动 export PATH="$PWD/bin:$PATH" 更好的地方在于,它会使用绝对路径,并且在离开目录时自动恢复,不会出现 PATH 越来越长的问题。
语言环境管理
direnv 和各种语言的版本管理器有很好的集成:
# Python:自动创建和激活虚拟环境
layout python3
# Node.js:使用 nvm 切换版本,读取 .nvmrc
use nvm
# Ruby:设置项目级的 GEM_HOME
layout ruby
# Go:设置项目级的 GOPATH
layout go
layout python3 会在项目目录下创建一个 .direnv/python-x.x.x 虚拟环境(或 .venv),并自动激活它。这意味着你不再需要手动 source venv/bin/activate,每次进入项目目录就自动进入虚拟环境,离开就自动退出。
继承父目录配置
# 加载父目录的 .envrc
source_up
# 加载指定路径的 .envrc
source_env ../shared/.envrc
# 安全版本,文件不存在也不报错
source_env_if_exists ../shared/.envrc
source_up 在 monorepo 场景下特别有用。你可以在仓库根目录的 .envrc 中设置通用变量,子目录的 .envrc 用 source_up 继承这些变量,然后再添加自己特有的配置。
实际使用场景
场景一:多项目 API Key 隔离
假设你同时在开发三个项目,它们使用不同的 API Key 和数据库:
# ~/projects/project-a/.envrc
export DATABASE_URL="postgres://localhost/project_a"
export STRIPE_KEY="sk_test_aaaa"
export AWS_PROFILE="project-a"
# ~/projects/project-b/.envrc
export DATABASE_URL="postgres://localhost/project_b"
export STRIPE_KEY="sk_test_bbbb"
export AWS_PROFILE="project-b"
在项目之间切换时,你永远不用担心用错了数据库或者 API Key,因为 direnv 会确保每个目录下只有属于这个项目的变量是生效的。
场景二:Kubernetes 多集群切换
如果你日常需要在多个 [[Kubernetes]] 集群之间切换,可以给每个集群的工作目录设置不同的 kubeconfig:
# ~/k8s/production/.envrc
export KUBECONFIG="$HOME/.kube/config-prod"
export AWS_PROFILE="prod"
# ~/k8s/staging/.envrc
export KUBECONFIG="$HOME/.kube/config-staging"
export AWS_PROFILE="staging"
进入 production 目录就自动切到生产集群,进入 staging 目录就切到测试集群,比手动 kubectl config use-context 安全得多,不会出现在生产集群上误操作的情况。
场景三:前端项目的 Node 版本管理
很多前端项目对 [[Node.js]] 版本有要求,direnv 可以和 [[nvm]] 联动实现自动切换:
# ~/projects/legacy-app/.envrc
use nvm 16
export NODE_ENV=development
PATH_add node_modules/.bin
# ~/projects/modern-app/.envrc
use nvm 22
export NODE_ENV=development
PATH_add node_modules/.bin
配合项目根目录的 .nvmrc 文件使用效果更好,只需要在 .envrc 中写 use nvm(不指定版本),direnv 会自动读取 .nvmrc 中的版本号。
场景四:配合 Docker Compose
很多 [[Docker Compose]] 项目都依赖 .env 文件来传递配置。direnv 可以同时服务于宿主机的开发环境和 Docker 环境:
# .envrc
dotenv_if_exists
export COMPOSE_PROJECT_NAME=myproject
.env 文件同时被 Docker Compose 和 direnv 读取,你在宿主机上运行脚本和在容器里运行服务用的是同一套配置,避免了配置不一致的问题。
安全机制
direnv 的安全模型是它区别于其他同类工具的关键特性。因为 .envrc 本质上是一个 bash 脚本,如果不加限制地自动执行,攻击者只需要在你 clone 的仓库里塞一个恶意的 .envrc 就能在你的机器上执行任意代码。
direnv 通过内容哈希白名单机制来防止这种情况。每次你执行 direnv allow,direnv 会计算 .envrc 文件的 SHA256 哈希值,并把这个哈希存储在 ~/.config/direnv/allow/ 目录下。之后每次加载时,direnv 都会重新计算哈希并与存储的值比对,只有完全匹配才会执行。这意味着文件的任何修改——哪怕只改了一个空格——都需要重新授权。
如果你有一些完全信任的目录(比如自己的项目目录),可以在 ~/.config/direnv/direnv.toml 中配置路径级别的白名单:
[whitelist]
prefix = ["/home/user/projects"]
但这个功能要谨慎使用,因为这些目录下的任何 .envrc 都会被自动信任,如果你 clone 了一个不信任的仓库到这个目录下,里面的 .envrc 也会被自动执行。
和其他方案的对比
在 direnv 之前,管理环境变量的方式主要有几种,各有各的问题。
直接在 .zshrc 或 .bash_profile 里堆 export 语句是最原始的做法,简单粗暴但维护成本高。所有项目的变量混在一起,时间长了就是一团乱麻,而且这些配置文件通常会同步到 dotfiles 仓库,密钥泄漏的风险很大。
[[dotenv]] 是另一个常见方案,尤其在 Node.js 和 [[Python]] 社区很流行。但 dotenv 是语言级别的工具,它在代码运行时加载 .env 文件,并不影响 shell 环境。你在终端里直接跑命令时是拿不到这些变量的。
autoenv 是一个比较早的按目录加载环境变量的工具,思路和 direnv 类似,但它的安全机制不如 direnv 完善,而且已经不太活跃了。
shadowenv 是一个比较新的替代品,用 S-expression 格式而不是 bash 来定义环境变量,安全性更高但学习成本也更大。
相比之下,direnv 在功能完备性、安全性和社区活跃度之间取得了很好的平衡。它的 stdlib 提供了丰富的内置函数,安全模型足够严谨,而且因为底层是 bash,几乎没有额外的学习成本。
最佳实践
经过一段时间的使用,我整理了几条值得注意的实践经验。
第一,.envrc 的执行速度很重要。因为 direnv 的 hook 在每次 prompt 渲染前都会执行,如果 .envrc 中有耗时操作(比如调用网络接口、运行包管理器),你的终端会明显变慢。官方建议 .envrc 的执行时间控制在 500 毫秒以内。如果确实需要执行耗时操作,可以把结果缓存到本地文件,.envrc 中只负责读取缓存。
第二,善用 .envrc 和 .env 的分离。.envrc 只写加载逻辑和非敏感配置,可以提交到 git;.env 存放实际的密钥值,放在 .gitignore 里。团队成员 clone 仓库后,只需要把自己的密钥填到 .env 文件里,direnv allow 一下就能开始工作。
第三,利用 ~/.config/direnv/direnvrc 来定义全局的自定义函数。如果你有一些在多个项目中重复使用的环境配置逻辑,把它们提取成函数放在全局配置里,可以避免在各个项目的 .envrc 中重复相同的代码。
第四,在 CI/CD 环境中,direnv 同样适用。你可以在 CI 脚本中用 direnv exec . <command> 来确保命令在正确的环境变量下运行,而不需要在 CI 配置中重复定义一遍所有变量。
最后
回到我最初的问题,direnv 完美地解决了环境变量管理的痛点。我现在的做法是在需要用到 API Key 的项目目录下放一个 .envrc,内容就一行 dotenv_if_exists ~/.env,密钥本身放在 ~/.env 里不进版本控制。这样既不需要在 .zshrc 里维护一堆 export,也不用担心密钥被同步到 GitHub 上。
direnv 的设计哲学让我很认同:环境变量应该跟着项目走,而不是跟着用户走。每个项目有自己独立的环境,进入时自动生效,离开时自动清除,干净利落。如果你也受够了全局环境变量的混乱,或者经常在多个项目之间切换,direnv 值得花十分钟配置一下,之后它就会安静地在后台为你工作。

