name: env-doctor description: 环境变量健康检查工具,扫描代码引用、检测硬编码密钥、验证格式 triggers:
- "env doctor"
- "env check"
- "检查环境变量"
- "检查 .env"
Skill: env-doctor
描述
环境变量健康检查工具。扫描项目代码中所有环境变量引用,对比 .env.example 和实际 .env,检测硬编码密钥,验证变量格式,自动生成 .env.example 模板。
触发条件
当用户说出以下内容时激活:
- "检查环境变量"
- "env check"
- "env doctor"
- "检查 .env"
支持的项目类型
- JavaScript / TypeScript(
process.env.XXX、import.meta.env.XXX) - Python(
os.environ、os.getenv、os.environ.get) - Go(
os.Getenv、os.LookupEnv)
执行步骤
阶段 1:扫描代码中的环境变量引用
JS/TS 项目
# 扫描 process.env.XXX(支持大写、VITE_*、REACT_APP_*、NEXT_PUBLIC_* 等框架前缀)
grep -rn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.cjs' \
-oE 'process\.env\.[A-Za-z_][A-Za-z0-9_]*' . 2>/dev/null | \
sed 's/.*process\.env\.//' | sort -u
# 扫描 import.meta.env.XXX(Vite 项目,含 VITE_* 前缀)
grep -rn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \
-oE 'import\.meta\.env\.[A-Za-z_][A-Za-z0-9_]*' . 2>/dev/null | \
sed 's/.*import\.meta\.env\.//' | sort -u
# 扫描解构模式 const { API_KEY, DB_HOST } = process.env
# 先提取整行,再用 sed 去除花括号外内容,最后逐个提取变量名
grep -rn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.cjs' \
-E 'const[[:space:]]+\{[^}]+\}[[:space:]]*=[[:space:]]*process\.env' . 2>/dev/null | \
sed 's/.*{//' | sed 's/}.*//' | tr ',' '\n' | \
sed 's/[[:space:]]//g' | grep -oE '[A-Za-z_][A-Za-z0-9_]*' | sort -u
# 扫描动态访问 process.env['KEY'] 或 process.env["KEY"]
grep -rn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.cjs' \
-oE "process\.env\[['\"][A-Za-z_][A-Za-z0-9_]*['\"]\]" . 2>/dev/null | \
grep -oE '[A-Za-z_][A-Za-z0-9_]+' | sort -u
Python 项目
# 扫描 os.environ["XXX"]、os.environ.get("XXX")、os.getenv("XXX")
# 支持大写和小写变量名(Python 社区有使用小写环境变量名的惯例)
grep -rn --include='*.py' \
-oE '(os\.environ\[["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']\]|os\.environ\.get\(["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']|os\.getenv\(["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"'])' . 2>/dev/null | \
grep -oE '[A-Za-z_][A-Za-z0-9_]*' | grep -v '^os$' | grep -v '^environ$' | grep -v '^getenv$' | grep -v '^get$' | sort -u
# 扫描 python-dotenv config() 模式
grep -rn --include='*.py' -E 'config\(["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']' . 2>/dev/null | \
grep -oE '["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']' | tr -d "\"'" | sort -u
# 扫描 pydantic BaseSettings 类
grep -rn --include='*.py' -E 'class\s+\w+\(BaseSettings\)' . 2>/dev/null
# 如果发现 BaseSettings 类,扫描其中的字段名(含 Field alias)作为环境变量
# 提取字段名和 env= 别名
grep -rn --include='*.py' -A 100 'class.*BaseSettings' . 2>/dev/null | \
sed '/^--$/q' | \
grep -E '^\s+[a-zA-Z_][a-zA-Z0-9_]*\s*[:=]' | \
sed 's/^\s*//' | grep -oE '^[a-zA-Z_][a-zA-Z0-9_]*' | sort -u
grep -rn --include='*.py' -A 100 'class.*BaseSettings' . 2>/dev/null | \
grep -oE "env=[\"'][A-Za-z_][A-Za-z0-9_]*[\"']" | \
grep -oE "[\"'][A-Za-z_][A-Za-z0-9_]*[\"']" | tr -d "\"'" | sort -u
Go 项目
# 扫描 os.Getenv("XXX")、os.LookupEnv("XXX")
grep -rn --include='*.go' \
-oE '(os\.Getenv|os\.LookupEnv)\(["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']\)' . 2>/dev/null | \
grep -oE '["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']' | tr -d "\"'" | sort -u
# 扫描 viper 库(viper.GetString, viper.Get, viper.BindEnv 等)
grep -rn --include='*.go' \
-oE 'viper\.(GetString|GetInt|GetBool|GetFloat64|GetDuration|Get|BindEnv|SetDefault)\(["'"'"'][A-Za-z_][A-Za-z0-9_.]*["'"'"']' . 2>/dev/null | \
grep -oE '["'"'"'][A-Za-z_][A-Za-z0-9_.]*["'"'"']' | tr -d "\"'" | sort -u
通用(配置文件中的变量引用)
# 扫描 docker-compose.yml、Dockerfile 中的环境变量
grep -rn --include='docker-compose*.yml' --include='docker-compose*.yaml' --include='Dockerfile*' \
-oE '\$\{[A-Za-z_][A-Za-z0-9_]*\}' . 2>/dev/null | \
grep -oE '[A-Za-z_][A-Za-z0-9_]*' | sort -u
汇总扫描结果
# 汇总所有扫描结果到 CODE_VARS
CODE_VARS=$(
# JS/TS: process.env.XXX
grep -rEoh 'process\.env\.[A-Za-z_][A-Za-z0-9_]*' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.cjs' --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build . 2>/dev/null | sed 's/.*process\.env\.//' ;
# JS/TS: import.meta.env.XXX
grep -rEoh 'import\.meta\.env\.[A-Za-z_][A-Za-z0-9_]*' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build . 2>/dev/null | sed 's/.*import\.meta\.env\.//' ;
# JS/TS: 解构模式 const { X } = process.env
grep -rEh 'const[[:space:]]+\{[^}]+\}[[:space:]]*=[[:space:]]*process\.env' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.cjs' --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build . 2>/dev/null | sed 's/.*{//' | sed 's/}.*//' | tr ',' '\n' | sed 's/[[:space:]]//g' | grep -oE '[A-Za-z_][A-Za-z0-9_]*' ;
# JS/TS: 动态访问 process.env['KEY']
grep -rEoh "process\.env\[['\"][A-Za-z_][A-Za-z0-9_]*['\"]\]" --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.cjs' --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build . 2>/dev/null | grep -oE '[A-Za-z_][A-Za-z0-9_]+' ;
# Python: os.environ/os.getenv/os.environ.get
grep -rEoh '(os\.environ\[["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']\]|os\.environ\.get\(["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']|os\.getenv\(["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"'])' --include='*.py' --exclude-dir=venv --exclude-dir=.venv --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build . 2>/dev/null | grep -oE '[A-Za-z_][A-Za-z0-9_]*' | grep -v '^os$' | grep -v '^environ$' | grep -v '^getenv$' | grep -v '^get$' ;
# Python: config("XXX")
grep -rEoh 'config\(["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']' --include='*.py' --exclude-dir=venv --exclude-dir=.venv --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build . 2>/dev/null | grep -oE '["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']' | tr -d "\"'" ;
# Go: os.Getenv/os.LookupEnv
grep -rEoh '(os\.Getenv|os\.LookupEnv)\(["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']\)' --include='*.go' --exclude-dir=vendor --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build . 2>/dev/null | grep -oE '["'"'"'][A-Za-z_][A-Za-z0-9_]*["'"'"']' | tr -d "\"'" ;
# Go: viper
grep -rEoh 'viper\.(GetString|GetInt|GetBool|GetFloat64|GetDuration|Get|BindEnv|SetDefault)\(["'"'"'][A-Za-z_][A-Za-z0-9_.]*["'"'"']' --include='*.go' --exclude-dir=vendor --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build . 2>/dev/null | grep -oE '["'"'"'][A-Za-z_][A-Za-z0-9_.]*["'"'"']' | tr -d "\"'" ;
# Docker: ${VAR} in docker-compose/Dockerfile
grep -rEoh '\$\{[A-Za-z_][A-Za-z0-9_]*\}' --include='docker-compose*.yml' --include='docker-compose*.yaml' --include='Dockerfile*' --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build . 2>/dev/null | grep -oE '[A-Za-z_][A-Za-z0-9_]*'
) | sort -u
阶段 2:对比 .env.example 与 .env
# 提取 .env.example 中定义的变量名
ENV_EXAMPLE=".env.example"
ENV_FILE=".env"
if [ -f "$ENV_EXAMPLE" ]; then
EXAMPLE_VARS=$(grep -v '^#' "$ENV_EXAMPLE" | grep -v '^$' | cut -d'=' -f1 | sed 's/[[:space:]]//g' | sort -u)
else
echo "[WARN] .env.example 不存在,将在阶段 5 自动生成"
EXAMPLE_VARS=""
fi
if [ -f "$ENV_FILE" ]; then
ENV_VARS=$(grep -v '^#' "$ENV_FILE" | grep -v '^$' | cut -d'=' -f1 | sed 's/[[:space:]]//g' | sort -u)
else
echo "[WARN] .env 文件不存在"
ENV_VARS=""
fi
# 在 .env.example 中但不在 .env 中的变量(缺失)
# 确保输入非空且已排序,防止 comm 报错
if [ -n "$EXAMPLE_VARS" ] && [ -n "$ENV_VARS" ]; then
comm -23 <(echo "$EXAMPLE_VARS" | sort -u) <(echo "$ENV_VARS" | sort -u) 2>/dev/null || echo ""
elif [ -n "$EXAMPLE_VARS" ]; then
echo "$EXAMPLE_VARS"
fi
# 在代码中引用但不在 .env.example 中的变量(未文档化)
if [ -n "$CODE_VARS" ] && [ -n "$EXAMPLE_VARS" ]; then
comm -23 <(echo "$CODE_VARS" | sort -u) <(echo "$EXAMPLE_VARS" | sort -u) 2>/dev/null || echo ""
elif [ -n "$CODE_VARS" ]; then
echo "$CODE_VARS"
fi
阶段 2.5:Monorepo 多 .env 文件处理
# Monorepo 检测
IS_MONOREPO=false
if grep -q '"workspaces"' package.json 2>/dev/null || [ -f pnpm-workspace.yaml ] || [ -f lerna.json ]; then
IS_MONOREPO=true
echo "[INFO] 检测到 monorepo,扫描所有 workspace 的 .env 文件..."
ENV_FILES=$(find . -name '.env' -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/build/*')
ENV_EXAMPLE_FILES=$(find . -name '.env.example' -not -path '*/node_modules/*' -not -path '*/.git/*')
# 为每个 .env 文件分别执行阶段 2 的对比检查
for env_file in $ENV_FILES; do
env_dir=$(dirname "$env_file")
env_example="${env_dir}/.env.example"
echo ""
echo "── 检查: $env_file ──"
if [ -f "$env_example" ]; then
LOCAL_EXAMPLE_VARS=$(grep -v '^#' "$env_example" | grep -v '^$' | cut -d'=' -f1 | sed 's/[[:space:]]//g' | sort -u)
LOCAL_ENV_VARS=$(grep -v '^#' "$env_file" | grep -v '^$' | cut -d'=' -f1 | sed 's/[[:space:]]//g' | sort -u)
# 缺失变量
MISSING=$(comm -23 <(echo "$LOCAL_EXAMPLE_VARS") <(echo "$LOCAL_ENV_VARS") 2>/dev/null)
if [ -n "$MISSING" ]; then
echo "[MISSING] 在 .env.example 中定义但 .env 中缺失:"
echo "$MISSING" | sed 's/^/ /'
fi
else
echo "[WARN] $env_dir 缺少 .env.example"
fi
# 对每个 .env 文件也执行格式验证(阶段 4 逻辑)
done
else
# 非 monorepo,使用根目录 .env
ENV_FILES=".env"
fi
阶段 3:检测硬编码密钥/凭据
使用以下正则模式扫描代码中的硬编码敏感信息:
# 硬编码密钥检测正则模式表
# | 模式名称 | 正则表达式 |
# |-------------------|--------------------------------------------------------------|
# | API Key 赋值 | (api[_-]?key|apikey)\s*[:=]\s*["'][a-zA-Z0-9_\-]{16,}["'] |
# | Secret 赋值 | (secret|SECRET)\s*[:=]\s*["'][a-zA-Z0-9_\-]{8,}["'] |
# | Password 赋值 | (password|passwd|pwd)\s*[:=]\s*["'][^"']{4,}["'] |
# | Token 赋值 | (token|TOKEN)\s*[:=]\s*["'][a-zA-Z0-9_\-\.]{16,}["'] |
# | AWS Key | AKIA[0-9A-Z]{16} |
# | Private Key | -----BEGIN (RSA |EC )?PRIVATE KEY----- |
# | JWT(三段式) | eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+ |
# | 连接字符串 | (mongodb|postgres|mysql|redis):\/\/[^\s"']{10,} |
# | Bearer Token | Bearer\s+[a-zA-Z0-9_\-\.]{20,} |
# | GitHub Token | gh[pousr]_[a-zA-Z0-9]{36,} |
# | Slack Token | xox[baprs]-[a-zA-Z0-9\-]{10,} |
# | OpenAI Key | sk-proj-[a-zA-Z0-9]{20,} |
grep -rn \
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \
--include='*.py' --include='*.go' --include='*.json' --include='*.yaml' --include='*.yml' \
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=vendor --exclude-dir=__pycache__ \
--exclude-dir=dist --exclude-dir=build --exclude-dir=.next \
--exclude-dir=test --exclude-dir=tests --exclude-dir=__tests__ --exclude-dir=spec \
--exclude-dir=fixtures --exclude-dir=__mocks__ \
--exclude='*.lock' --exclude='package-lock.json' --exclude='bun.lockb' \
--exclude='*.test.*' --exclude='*.spec.*' --exclude='*.mock.*' \
-E 'AKIA[0-9A-Z]{16}|-----BEGIN (RSA |EC )?PRIVATE KEY-----|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|(mongodb|postgres|mysql|redis)://[^[:space:]"'"'"']{10,}|gh[pousr]_[a-zA-Z0-9]{36,}|xox[baprs]-[a-zA-Z0-9-]{10,}|sk-proj-[a-zA-Z0-9]{20,}' .
# 检测变量赋值中的硬编码
grep -rn \
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \
--include='*.py' --include='*.go' \
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=vendor --exclude-dir=__pycache__ \
--exclude-dir=dist --exclude-dir=build --exclude-dir=.next \
--exclude-dir=test --exclude-dir=tests --exclude-dir=__tests__ --exclude-dir=spec \
--exclude-dir=fixtures --exclude-dir=__mocks__ \
--exclude='*.lock' --exclude='package-lock.json' --exclude='bun.lockb' \
--exclude='*.test.*' --exclude='*.spec.*' --exclude='*.mock.*' \
-i -E "(api[_-]?key|secret|password|passwd|pwd|token)[[:space:]]*[:=][[:space:]]*[\"'][a-zA-Z0-9_-]{8,}[\"']" . \
2>/dev/null
# 检测环境变量信息泄露模式
# 1. console.log(process.env) — 整体打印环境变量
grep -rn \
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.cjs' \
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist --exclude-dir=build \
--exclude='*.test.*' --exclude='*.spec.*' \
-E 'console\.(log|info|debug|warn|error)\(process\.env\b' . 2>/dev/null
# 2. { ...process.env } — 对象展开泄露全部环境变量
grep -rn \
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.cjs' \
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist --exclude-dir=build \
--exclude='*.test.*' --exclude='*.spec.*' \
-E '\.\.\.\s*process\.env' . 2>/dev/null
# 3. export const KEY = process.env.XXX — 导出环境变量(可能被外部模块引用导致泄露)
grep -rn \
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.cjs' \
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist --exclude-dir=build \
--exclude='*.test.*' --exclude='*.spec.*' \
-E 'export\s+(const|let|var)\s+\w+\s*=\s*process\.env\.' . 2>/dev/null
阶段 4:验证变量格式
对 .env 文件中的变量值进行格式验证:
格式验证规则表
┌──────────────────┬─────────────────────────────────────┬──────────────────────┐
│ 变量名模式 │ 期望格式 │ 验证正则 │
├──────────────────┼─────────────────────────────────────┼──────────────────────┤
│ *_URL, *_URI │ 合法 URL │ ^https?://\S+$ │
│ *_PORT │ 1-65535 的整数 │ ^[0-9]+$ (范围检查) │
│ *_HOST │ 域名或 IP │ ^[\w.\-]+$ │
│ *_EMAIL │ 邮箱格式 │ ^[^@]+@[^@]+\.[^@]+$│
│ *_ENABLED, │ 布尔值 │ ^(true|false|0|1)$ │
│ *_DISABLED, │ │ │
│ IS_*, ENABLE_*, │ │ │
│ USE_*, HAS_* │ │ │
│ *_TIMEOUT, │ 正整数 │ ^[1-9][0-9]*$ │
│ *_INTERVAL, │ │ │
│ *_TTL, *_LIMIT │ │ │
│ *_PATH, *_DIR │ 路径格式(非空) │ ^[/~.][\S]+$ │
│ *_KEY, *_SECRET, │ 非占位符(不是 xxx、your-*、 │ 不匹配 │
│ *_TOKEN │ changeme、TODO 等) │ ^(xxx|your-.*| │
│ │ │ changeme|TODO| │
│ │ │ REPLACE_ME| │
│ │ │ placeholder)$ │
└──────────────────┴─────────────────────────────────────┴──────────────────────┘
# 格式验证脚本逻辑(伪代码,实际用 bash 实现)
while IFS='=' read -r key value; do
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
value=$(echo "$value" | sed 's/^["'\'']//' | sed 's/["'\'']$//')
# URL 验证(精确匹配协议+主机名格式)
if [[ "$key" =~ _(URL|URI)$ ]]; then
[[ ! "$value" =~ ^https?://[a-zA-Z0-9._-]+ ]] && echo "[FORMAT] $key: 期望 URL 格式,实际值: ${value:0:3}***"
fi
# 端口验证(先检查格式是否为纯数字,再检查范围)
if [[ "$key" =~ _PORT$ ]]; then
if [[ ! "$value" =~ ^[0-9]+$ ]]; then
echo "[FORMAT] $key: 期望数字端口号,实际值: ${value:0:3}***"
elif [ "$value" -lt 1 ] || [ "$value" -gt 65535 ]; then
echo "[FORMAT] $key: 期望 1-65535 端口号,实际值: ${value:0:3}***"
fi
fi
# 布尔值验证(使用 =~ 交替语法代替 || 避免 bash 兼容性问题)
if [[ "$key" =~ ^(IS_|ENABLE_|USE_|HAS_) ]] || [[ "$key" =~ _(ENABLED|DISABLED)$ ]]; then
[[ ! "$value" =~ ^(true|false|0|1|yes|no)$ ]] && echo "[FORMAT] $key: 期望布尔值,实际值: ${value:0:3}***"
fi
# 占位符检测
if [[ "$key" =~ _(KEY|SECRET|TOKEN|PASSWORD)$ ]]; then
if [[ "$value" =~ ^(xxx|your-|changeme|TODO|REPLACE_ME|placeholder|example|test123) ]]; then
echo "[PLACEHOLDER] $key: 疑似占位符未替换: ${value:0:3}***"
fi
fi
done < .env
阶段 5:自动生成 .env.example
如果项目中不存在 .env.example:
# 从代码扫描结果和现有 .env 合并生成 .env.example
if [ ! -f ".env.example" ]; then
echo "# Environment Variables Template" > .env.example
echo "# Generated by env-doctor on $(date +%Y-%m-%d)" >> .env.example
echo "# Copy this file to .env and fill in your values" >> .env.example
echo "" >> .env.example
# 按类别分组输出
# 1. 从代码中扫描到的变量
for var in $CODE_VARS; do
# 根据变量名猜测类别和默认值
case "$var" in
*_PORT) echo "$var=3000" ;;
*_HOST) echo "$var=localhost" ;;
*_URL|*_URI) echo "$var=https://example.com" ;;
*_ENABLED|*_DISABLED|IS_*|ENABLE_*|USE_*) echo "$var=false" ;;
*_KEY|*_SECRET|*_TOKEN|*_PASSWORD) echo "$var=your-$(echo "$var" | tr '[:upper:]' '[:lower:]')-here" ;;
*_ENV|NODE_ENV) echo "$var=development" ;;
*_TIMEOUT|*_TTL) echo "$var=30000" ;;
*_LIMIT) echo "$var=100" ;;
*) echo "$var=" ;;
esac
done >> .env.example
echo "[INFO] 已生成 .env.example,包含 $(wc -l < .env.example) 行"
fi
决策流程图
开始 env-doctor
│
▼
┌──────────────┐
│ 检测项目类型 │ ──→ JS/TS? Python? Go? 混合?
└──────┬───────┘
│
▼
┌──────────────┐
│ 扫描环境变量 │ ──→ 收集 CODE_VARS 列表
│ 引用 │
└──────┬───────┘
│
▼
┌──────────────┐ ┌─────────────────┐
│ .env.example │─否─→│ 阶段5: 自动生成 │
│ 存在? │ │ .env.example │
└──────┬───────┘ └────────┬────────┘
│是 │
▼ ▼
┌──────────────┐
│ 对比 .env 与 │ ──→ 报告缺失变量、未文档化变量
│ .env.example │
└──────┬───────┘
│
▼
┌──────────────┐
│ 检测硬编码 │ ──→ 报告发现的敏感信息(文件:行号)
│ 密钥/凭据 │
└──────┬───────┘
│
▼
┌──────────────┐
│ 格式验证 │ ──→ 报告格式不合规的变量
└──────┬───────┘
│
▼
┌──────────────┐
│ 输出报告 │
└──────────────┘
输出报告格式
========================================
env-doctor 环境变量健康检查报告
========================================
项目类型: [JS/TS | Python | Go | 混合]
扫描时间: YYYY-MM-DD HH:MM:SS
总耗时: X.XXs
扫描范围: N 个文件,M 个目录
排除目录: node_modules, .git, dist, build, vendor, __pycache__
Monorepo: 是 (N 个 workspace) / 否
── 1. 变量引用扫描 ──────────────────
代码中引用的环境变量: N 个
.env.example 中文档化的变量: M 个
变量覆盖率: X/M (代码使用的 / 文档化的)
[列出所有变量名]
── 2. .env 对比 ─────────────────────
[MISSING] VAR_NAME 在 .env.example 中定义但 .env 中缺失
[UNDOC] VAR_NAME 在代码中使用但未在 .env.example 中记录
[UNUSED] VAR_NAME 在 .env 中定义但代码中未使用
── 3. 硬编码密钥检测 ────────────────
[CRITICAL] src/config.ts:42 疑似硬编码 API Key
[CRITICAL] lib/db.py:15 疑似硬编码数据库连接字符串
[LEAK] src/app.ts:10 console.log(process.env) 信息泄露风险
[LEAK] src/config.ts:5 { ...process.env } 环境变量展开风险
[LEAK] src/env.ts:3 export const 导出环境变量
── 4. 格式验证 ──────────────────────
[FORMAT] DATABASE_URL: 期望 URL 格式
[PLACEHOLDER] API_SECRET: 疑似占位符未替换
── 5. 总结 ──────────────────────────
通过: X 项 | 警告: Y 项 | 严重: Z 项
扫描文件数: N | 扫描目录: M
变量覆盖率: XX%
========================================
质量检查清单
在报告完成前,逐项验证:
- 是否扫描了所有支持的文件类型(.ts/.js/.py/.go)
- 是否排除了 node_modules、vendor、.git、dist、build 等目录
- 是否检查了 docker-compose 和 Dockerfile 中的环境变量
- .env.example 与 .env 的对比结果是否准确
- 硬编码检测是否排除了测试文件中的 mock 值
- 格式验证是否覆盖了所有规则表中的模式
- 占位符检测是否识别了常见占位符模式
- 报告格式是否清晰、分类是否正确
- 如果生成了 .env.example,是否按类别分组且有注释
- 是否向用户报告了总结统计(通过/警告/严重)
注意事项
- 不要读取或显示
.env文件中的实际敏感值,只报告变量名 - 硬编码检测可能存在误报,标注
[疑似]让用户确认 .env.example中不应包含真实密钥值,只包含格式示例- 对于 monorepo 项目,递归检查子目录中的
.env文件