# Step 05: API Key 与环境变量

一句话导读:新增 utils/config.ts,从 .env / process.env 读 ANTHROPIC_API_KEY,启动时校验、日志里脱敏显示。看似只是读个变量,背后是一整套成熟 CLI 的配置管理思路。


# 一、这一步做了什么(What)

step05 在 step04 上加一个 utils/config.ts:用 dotenv 加载 .env,导出 getApiKey / hasApiKey / getMaskedApiKey / getBaseUrl / printEnvStatus。main() 一进来先 hasApiKey() 校验,没 Key 就打印设置指引并 exit(1);有 Key 则脱敏打印(前 12 后 4,中间星号)。功能主体还是 step04 的模拟 REPL,这一步只补齐「配置入口」。


# 二、面试官视角:读一个环境变量,为什么值得单开一个模块?(Why)

面试题:process.env.ANTHROPIC_API_KEY 一行就能拿到,为什么要包装成 config.ts 一整个模块?

因为「读 Key」只是表象,真正要解决的是三个配置管理的经典问题,散在各处读会全部踩坑:

  1. 来源多样:Key 可能来自系统环境变量、.env 文件、用户配置文件。到处 process.env.XXX 会导致「有的地方读得到、有的读不到」。
  2. 需要校验:Key 可能没设、格式不对(不以 sk-ant- 开头)。校验逻辑散落各处 = 重复代码 + 遗漏。
  3. 敏感信息:Key 绝不能明文进日志/报错。脱敏逻辑必须集中,否则总有一处忘了打码就泄露。

集中到一个模块后:

问题 散读 process.env 集中到 config.ts
来源切换 每处都要改 改一个函数
校验一致性 各处各写,易漏 hasApiKey() 一处定义
脱敏 总有地方忘打码 getMaskedApiKey() 强制统一

一句话:配置读取要收口。这是所有成熟 CLI 的标准做法——不是过度设计,是防坑。


# 三、原理:config.ts 的三个关键设计(How)

# 1. dotenv 加载:向上找 .env

const __dirname = dirname(fileURLToPath(import.meta.url));
const envPaths = [
  join(__dirname, '..', '.env'),        // step 目录内
  join(__dirname, '..', '..', '.env'),  // 项目根
];
for (const p of envPaths) {
  if (existsSync(p)) { config({ path: p }); break; }
}
1
2
3
4
5
6
7
8

为什么要向上找两级?因为 .env 可能放在每个 step 目录里,也可能放在整个项目根目录共享。这个「就近优先、逐级向上」的查找,正是配置文件解析的常见范式(.gitignore、tsconfig 都这么找)。注意:Claude Code 本身不依赖 dotenv,它直接读 process.env——dotenv 只是学习项目为了方便本地填 Key 而引入的。

# 2. 三级回退(真实 Claude Code 的读法)

系统环境变量是最顶层来源。真实版读 Key 走三级回退:

1. process.env.ANTHROPIC_API_KEY   → 最快,零配置
2. ~/.claude/settings.json          → 用户级配置
3. ~/.anthropic/config              → 全局配置
1
2
3

越上层越灵活(临时 export 就生效),越下层越持久。这条优先级链 环境变量 > 用户配置 > 全局配置 > 默认值,几乎是所有 CLI 工具的通用模式。

# 3. 脱敏显示

export function getMaskedApiKey(): string | null {
  const key = getApiKey();
  if (!key) return null;
  if (key.length <= 16) return '(key too short)';
  return `${key.slice(0, 12)}${'*'.repeat(key.length - 16)}${key.slice(-4)}`;
}
1
2
3
4
5
6

只露前 12、后 4,中间全星号。前缀 sk-ant-... 露出来能确认「是不是这把 Key」,后 4 位帮人工核对,中间的敏感段永不出现。校验用的 hasApiKey() 还额外检查 startsWith('sk-ant-')——格式校验前置,避免拿一把明显错的 Key 去打 API 白费一次网络往返。

# 启动流程

main() 启动
   ↓
hasApiKey()?  ── 否 → 打印设置指引 → exit(1)  (fail fast)
   ↓ 是
showSuccess(getMaskedApiKey())  → 脱敏打印
   ↓
printEnvStatus()  → 展示 BASE_URL 等
   ↓
进入 REPL
1
2
3
4
5
6
7
8
9

# 四、深入追问(面试常见 follow-up)

Q:为什么没 Key 要直接 exit(1),而不是先进 REPL、等真调 API 时再报错? A:这是 fail fast(快速失败) 原则。缺 Key 是「必然导致后续全部失败」的前置条件,越早暴露越好。如果放行进 REPL,用户输入半天、到第一次调 API 才报错,浪费时间还困惑。启动时校验 + 明确的设置指引(怎么复制 .env、怎么 export),把错误挡在最前面,用户体验最好。exit(1) 的非零退出码也让脚本/CI 能感知失败。

Q:脱敏为什么保留前缀和后 4 位,而不是全打码? A:全打码(****)虽然最安全,但失去了可核对性——用户没法判断「我是不是配错了 Key」。保留 sk-ant- 前缀确认类型、保留后 4 位供人工比对,是安全性和可用性的平衡。这是行业惯例(信用卡、AWS Key 都这么显示)。中间的高熵段才是真正的秘密,只要它不露,反推不出完整 Key。

Q:ANTHROPIC_BASE_URL 这种变量存在的意义是什么? A:为了可替换后端。默认打 api.anthropic.com,但企业自部署、走代理网关、或用兼容 API 的第三方服务时,需要改地址。把它做成环境变量(getBaseUrl() 默认官方地址、可被 env 覆盖),无需改代码就能切后端。真实版还认 HTTP_PROXY / HTTPS_PROXY、CLAUDE_CODE_HEADLESS、DEBUG 等一批变量,全走同一个集中读取层。


# 五、设计权衡

学习项目引入 dotenv 是便利性取舍——本地填 Key 方便,但真实 Claude Code 不带这个依赖(生产 CLI 不该假设有 .env,而是读系统环境和标准配置目录)。另外 hasApiKey() 只做前缀和长度校验,不做真实性验证——真假只有打一次 API 才知道。这里选择「便宜的本地校验挡住明显错误,真实校验留给第一次调用」,是成本合理的分层校验。


# 六、与真实源码的对照

我们的实现 Claude Code 源码
config.ts 读 env src/utils/env.ts + config.ts 集中读取
dotenv 加载 .env 无 dotenv,直读 process.env + 配置文件
单一 Key 来源 三级回退:env → 用户配置 → 全局配置
getMaskedApiKey 脱敏 日志/错误输出统一脱敏
getBaseUrl 可覆盖 ANTHROPIC_BASE_URL + 代理变量一整套

# 七、一句话总结

Step 05 = 配置读取收口 + fail fast + 脱敏:把「读 Key」从散落的 process.env 收进一个 config.ts,启动即校验、缺 Key 就带指引退出、日志里前 12 后 4 打星。读一个变量是小事,「怎么读得安全、读得统一、错得清楚」才是成熟 CLI 的功课。

# 下一步

cd step06 && npm start —— 把模拟的 callAI 换成真实 Anthropic SDK,让 Claude 第一次真正开口。