将 .env 机密隐藏,远离 AI 的窥探。
(注意:该项目原先名为 enveil,现已更名为 enject)
AI 编码工具(如 Claude Code、Copilot、Cursor 等)可以读取项目目录中的文件,这意味着纯文本格式的 .env 文件就像一颗随时可能引爆的秘密泄露炸弹。这并非理论上的问题。这是一个已知问题,我曾多次遇到(即使在 Claude Code 的 settings.json 中明确告诉 Claude 不要偷看)。enject 通过确保明文的秘密绝不被存储在磁盘上来解决这个问题。你的 .env 文件只包含符号引用;真实值存储在一个加密的本地仓库中,并在启动子进程时直接注入。
此项目灵感来自 Filip Hric 的方案/博客文章,该方案使用了类似的概念,但依赖 1Password。我希望得到一个不依赖第三方服务的自包含解决方案,于是便有了这个项目。没错,这个项目几乎完全由 Claude Code 构建,并经过大量手动验证和测试。
本设计主要用于缓解 AI/LLM 工具意外读取 .env 中机密的已知问题。其他优势包括:防止 .env 意外提交到仓库时泄露机密、能够分享包含引用而非明文的 .env 文件,以及可选地分享加密存储本身。
该项目并非防止 AI 代理获取机密的万能药。例如,代理仍可能(因意外或通过提示注入)编写代码,在运行时将机密泄露到终端输出或文件中。我们强烈建议不要依赖此工具(或一般的 .env 文件)来存储生产环境机密。
你的 .env 文件看起来像这样:
DATABASE_URL=en://database_url
STRIPE_KEY=en://stripe_key
PORT=3000
从技术上讲,它可以安全地提交(不过最好还是别这么做),更重要的是:任何 AI 工具意外(或许并非那么意外)窥探它时都是安全的。
当你运行 enject run -- npm start 时,它会:
en:// 引用存储文件是一个二进制 blob。没有主密码,它与随机噪声无异。每次写入时都会生成全新的 nonce,因此 AES-GCM nonce 重用不可能发生。对密文的任何修改——哪怕只翻转一位——都会导致认证失败,拒绝解密。
此版本仍为 alpha 版本,因此在调用 cargo install 时需要附加最新版本号。
cargo install enject --version 0.2.0-alpha
需要 Rust 1.70+。
git clone https://github.com/greatscott/enject
cd enject
cargo build --release
编译后的二进制文件位于 target/release/enject。将其安装到 PATH 中的某个位置,以便从任何项目运行:
macOS / Linux (bash 或 zsh)
# 选项 A: ~/.local/bin(无需 sudo,Linux 常用)
mkdir -p ~/.local/bin
cp target/release/enject ~/.local/bin/
# 选项 B: /usr/local/bin(需要 sudo,系统全局可用)
sudo cp target/release/enject /usr/local/bin/
# 选项 C: ~/.cargo/bin(如果你使用 rustup,此目录已位于 PATH)
cp target/release/enject ~/.cargo/bin/
如果你使用了选项 A 且 ~/.local/bin 尚未在 PATH 中,请将以下内容添加到 shell 配置(~/.zshrc、~/.bashrc 或 ~/.bash_profile):
export PATH="$HOME/.local/bin:$PATH"
然后重新加载:
source ~/.zshrc # 或 ~/.bashrc
验证是否成功:
enject --version
二进制文件是全局安装的——你不需要重新安装它。但每个项目都有自己的加密存储:
cd your-project
enject init
这会在当前目录创建 .enject/,包含项目的配置和加密存储。将其添加到 .gitignore——它绝不应被提交。
每个项目运行一次,在项目根目录:
enject init
这会生成一个随机的 32 字节盐,写入 .enject/config.toml,创建一个空的加密存储文件 .enject/store,并提示你设置主密码。将 .enject/ 添加到你的 .gitignore——存储文件绝不应被提交。
enject set some_database_url
# 提示: Value for 'database_url': (隐藏输入)
enject set some_api_key
值始终以交互方式输入。无法将值作为命令行参数传递——这可以防止机密出现在 shell 历史或 ps 输出中。
.env 中引用机密DATABASE_URL=en://some_database_url
MY_API_KEY=en://stripe_key
PORT=3000
纯 KEY=VALUE 行保持不变。只有 en:// 引用会被解析。
enject run -- npm start
enject run -- python manage.py runserver
enject run -- cargo run
-- 之后的所有内容都会原样传递给操作系统。子进程继承你的完整 shell 环境(因此 PATH、HOME 等存在),并在其基础上叠加 .env 中的值。
enject list # 打印存储的密钥名称(不显示值)
enject delete <key> # 删除一个机密
enject import <file> # 加密纯文本 .env 中的所有值,将其重写为 en:// 模板
enject rotate # 使用新的主密码重新加密存储
没有 get 和 export 命令。将机密值打印到标准输出会创造一个 AI 可读的泄露渠道——enject 的全部意义就在于将值保留在磁盘和任何可读输出流之外。
每个安全不变量都有对应的自动化测试和手动检查路径。
cargo test
31 个测试,全部覆盖以下声明。
自动化: store::password::tests::test_encrypt_decrypt_roundtrip
保存一个机密,持久化存储,从磁盘重新加载,解密,并检查值是否往返正确。只有当磁盘上的字节是有效的密文时测试才会通过——明文会导致解密失败。
cargo test store::password::tests::test_encrypt_decrypt_roundtrip
手动检查:
enject init # 密码: test123
enject set mykey # 值: my-super-secret
xxd .enject/store | head -5
strings .enject/store
xxd 将显示二进制数据。strings 将返回空——没有可提取的 ASCII 序列。前 12 个字节是随机 nonce;之后是 AES-GCM 密文,末尾附加了 16 字节的认证标签。
自动化: store::password::tests::test_nonce_changes_on_each_save
连续保存存储两次,每次读取文件的前 12 个字节,并断言它们不同。
cargo test store::password::tests::test_nonce_changes_on_each_save
手动检查:
xxd .enject/store | head -1 # 注意前 12 个字节
enject set anotherkey # 任何写入都会轮换 nonce
xxd .enject/store | head -1 # 前 12 个字节现在不同
自动化: store::password::tests::test_wrong_password_returns_err
用一个密码创建存储,然后尝试用另一个密码解锁,并断言返回 Err。
cargo test store::password::tests::test_wrong_password_returns_err
手动:
enject list # 输入错误密码
# 输出: "Wrong master password or corrupted store."
# 退出码: 1
AES-GCM 会在密文上生成一个 16 字节的认证标签。任何修改——哪怕只翻转一位——都会导致验证在解密前失败。明文永远不会暴露。
自动化: store::password::tests::test_tampered_ciphertext_returns_err
在存储文件的密文区域(前 12 字节 nonce 之后)翻转一个字节,然后尝试解密并断言 Err。
cargo test store::password::tests::test_tampered_ciphertext_returns_err
手动:
# 翻转第 20 个字节(密文内部,nonce 之后)
python3 -c "
data = open('.enject/store', 'rb').read()
bad = data[:20] + bytes([data[20] ^ 0xFF]) + data[21:]
open('.enject/store', 'wb').write(bad)
"
enject list
# 输出: "Wrong master password or corrupted store."
en:// 引用都会导致硬错误如果 .env 中的引用在存储中没有匹配的密钥,enject run 会立即退出并返回非零退出码。子进程永远不会被启动。
自动化: env_template::tests::test_unknown_ev_ref_returns_err
调用 resolve() 并传入一个没有匹配条目的引用,断言返回 Err。
cargo test env_template::tests::test_unknown_ev_ref_returns_err
手动:
echo "DB=en://nonexistent_key" > .env
enject run -- env
# 输出: Secret 'nonexistent_key' not found in store. Add it with: enject set nonexistent_key
# 退出码: 1 (`env` 子进程从未运行)
实现可选的/附加的系统级存储,以便更轻松地维护跨多个项目使用的机密。
减少每次更新时手动输入存储密码的需求。