> 环境里一个没清掉的变量,会把订阅制 CLI 静默切到按量计费

Research Tools Beginner 2026-10-12 04:23 2026-10-12
#cli #environment #billing #automation #cron #tooling

一个每天跑的脚本要 shell out 去调一个 CLI。那个 CLI 既支持订阅登录, 也支持 API key,而环境变量的优先级高于订阅登录 —— 所以 shell 里(或 .bashrc 里,或某次调试后忘记 unset 的) 一个 ANTHROPIC_API_KEY,会让这个每天跑的任务走计费通道, 而输出完全一样。

结论先行

一个每天跑的脚本要 shell out 去调一个 CLI。那个 CLI 既支持订阅登录, 也支持 API key,而环境变量的优先级高于订阅登录 —— 所以 shell 里(或 .bashrc 里,或某次调试后忘记 unset 的) 一个 ANTHROPIC_API_KEY,会让这个每天跑的任务走计费通道, 而输出完全一样。

修法是调用时把这一类变量从子进程环境里剔掉,而不是依赖调用者的环境干净:

_AUTH_OVERRIDES = (
    "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL",
    "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX",
)
env = {k: v for k, v in os.environ.items() if k not in _AUTH_OVERRIDES}
subprocess.run(cmd, env=env, ...)

为什么

这是一个没有任何可见症状的配置漂移:

  • 输出不变(同一个模型、同一个回答格式)。
  • 退出码不变。
  • 日志不变。
  • 唯一的差别出现在一张账单上,而账单的延迟是以天或月计的。

而优先级的方向让它单向危险:环境变量覆盖已登录的订阅, 反过来不会。也就是说"环境里多一个变量"的后果是切走, "环境里少一个变量"的后果是用订阅 —— 默认行为是安全的那一侧, 而污染的方向是不安全的那一侧。

长期运行的自动化任务(cron、systemd timer、CI、守护进程) 最容易中,因为它们的环境是继承来的、而且没人每天去看一眼。 这和 cron 的 PATH 太瘦是一对镜像问题:那边是继承的环境少了东西, 这边是继承的环境多了东西,两者都是"手动跑正常" (见 优雅降级设计好了,却没人被告知——退化必须在产物上留痕)。

怎么用

凡是会改变"调用谁/怎么付费/连到哪"的环境变量, 在 shell out 时显式构造子进程环境,不要直接传 os.environ。 白名单比黑名单更彻底,但黑名单在实践中够用而且改动小 —— 关键是这个决定要写在调用点,和一条注释一起:

# Env vars that would redirect the CLI away from the subscription login.

这类变量的清单值得单独维护一份。 除了 key 本身, 还有 base URL(改成代理或镜像)、云厂商开关(切到 Bedrock / Vertex)、 以及 profile / region 一类。只剔掉 *_API_KEY 会漏掉后面这些。

在自动化任务里把"用的是哪条通道"打进日志。 如果 CLI 有办法报告当前认证方式,调用一次记一行。 这是唯一能让这个失效在账单之前被看到的办法 —— 没有可观测症状的失效,必须被显式地观测。

同类的变量还有一堆,都值得在长跑任务里清掉或固定: http_proxy / HTTPS_PROXY、PYTHONPATH、CUDA_VISIBLE_DEVICES、 LANG / LC_ALL(影响排序和数字格式)、TZ(影响日期分界)。 它们的共性是:会改变结果但不会改变"是否成功"。

出处

~/Engineering/worklog-printer/digest.py:16-19(模块 docstring 里对这个 陷阱的说明)与 :35-39, 166(_AUTH_OVERRIDES 与构造子进程环境那行)。