> 环境里一个没清掉的变量,会把订阅制 CLI 静默切到按量计费
一个每天跑的脚本要 shell out 去调一个 CLI。那个 CLI 既支持订阅登录, 也支持 API key,而环境变量的优先级高于订阅登录 —— 所以 shell 里(或 .bashrc 里,或某次调试后忘记 unset 的) 一个 ANTHROPIC_API_KEY,会让这个每天跑的任务走计费通道, 而输出完全一样。
// TABLE_OF_CONTENTS
结论先行¶
一个每天跑的脚本要 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 与构造子进程环境那行)。