1. Headless 模式跑 Claude Code,先被 401 卡住
在脚本里跑claude -p "..." --output-format json --max-turns 3,本来是挺顺的一条流水线:CI 日志灌进去,结构化结果吐出来,再喂给下游脚本处理。但很多人在新环境第一次跑 headless 请求时,常撞上一个很尴尬的报错:HTTP 401 Unauthorized。更让人迷惑的是,请求 URL 显示多了一个/v1,而 Base URL 本身明明没有写/v1。
这个现象的本质,是 Base URL 的拼写位置和工具的补全逻辑冲突。Claude Code 的 Headless 模式在发起请求时,会根据环境变量里的ANTHROPIC_BASE_URL拼出完整 API 路径。TaoToken 这类统一接入通道要求你只填根地址https://taotoken.net/api,结果若有人手滑写成https://taotoken.net/api/v1,工具又会自动补一个/v1,最终请求打到/api/v1/v1/...,鉴权直接失败,401 就这么来的。先去 TaoToken 官网 创建一把 API Key,再回来把 Base URL 填对,问题就能解开。
这条排障思路,不仅要解决 401,还要让 Headless 模式在脚本里真正可用。原文里有一整套 headless 任务设计方法——issue 分类、changelog 生成、CI 失败分析、文档链接检查——它们都依赖同一个前提:能稳定跑通一次claude -p。所以下面先解决 Base URL 的问题,再回到原文场景里看怎么配置、怎么验证。
2. 先拿 Key:TaoToken 模型广场与 API Keys
回到准备材料这一步。要在脚本里接 TaoToken,你需要的不是官网首页,而是自己的 API Key。打开 TaoToken,注册并进入控制台。左侧的 API Keys 页面点击创建,命名建议按任务来,比如ci-issue-triage、gitlab-changelog,这样后续在用量列表里能一眼看出是哪个 headless 任务花的 token。
Key 创建好后,先别急着填进配置。去模型广场看一眼当前支持的模型 ID,以及接口地址的写法。模型 ID 不要凭记忆填,不同时间段模型列表会更新,以当天页面上显示的为准。这里要分清两个完全不同的地址:浏览器里访问官网落地页,用带 UTM 的完整链接;填进 Claude Code 的 Base URL,则必须是接口地址https://taotoken.net/api,末尾没有/v1,也不要追加任何 UTM 参数。官网地址和接口地址混用,是紧接着最容易踩的坑。
原文在讲 headless 架构时提到,输入可以来自环境变量、stdin 管道或文件。这里的 Key 与 Base URL,本质上也属于“输入环境变量”——准备阶段把这两样拿稳,后面的claude -p命令才能把环境变量转成一次真正的调用。建议把 Key 存在 shell 的私密环境变量文件里,而不要散落在项目仓库;具体配置示例见第 4 节。
3. 401 根因:Base URL 多出 /v1 的路径拼接
排障时,最重要的是先看实际请求打到哪个 URL。Claude Code 的命令行参数-p本身不提供 base_url 参数,它读取的是环境变量ANTHROPIC_BASE_URL。而很多人在从官方文档迁移时,习惯于把这一类变量填成https://api.anthropic.com这种带一级路径的样式;换到兼容通道也会下意识地填https://taotoken.net/api/v1,并认为“加个 /v1 才像官方”。
麻烦就在这里。Claude Code 不会帮你判断 Base URL 里是否已经有版本号,它只负责把基础地址、模型路径和 Key 拼成一个请求。如果基础地址多写一段/v1,某些构建版本会自动补/v1,于是真实路径变成/api/v1/v1/messages;即便没有自动补全,服务端按新版路径解析/v1/...也可能因为路径作用域不一致而拒绝 Key,返回 401。
3.1 三种常见的错误填法
| 填法 | 实际请求路径 | 结果 |
|---|---|---|
https://taotoken.net/api | .../api/v1/messages | 正确 |
https://taotoken.net/api/v1 | .../api/v1/v1/messages | 路径重复,401 |
https://taotoken.net/api/ | .../api//v1/... | 双斜杠或鉴权失败 |
https://taotoken.net/?utm_source=... | 带查询参数,无法作为 API 地址 | 请求无效 |
第 4 行值得单独说。带 UTM 的完整官网地址是https://taotoken.net/?utm_source=taotoken_aicg_blog_end,那是给人点进去注册、看模型广场用的,不是给程序当 Base URL。把查询参数塞进请求地址,轻则解析失败,重则让鉴权中间件直接拒绝。“给人用的官网地址”和“给程序用的接口地址”分开记,排障能少花一半时间。
3.2 正确填法只有一个
正确的 Base URL 只有一种:https://taotoken.net/api,末尾不加/v1,也不能带 UTM。填进环境变量后,Claude Code 会把它解析为.../api/v1/messages,这个路径就是兼容通道的真实入口。多出来的/v1谁添的、为什么添,都不重要;重要的是你交出去的地址永远保持干净。
4. 在 Claude Code 的 settings.json 和 env 里指到 TaoToken
配置 Claude Code 有两条常见路径:全局配置文件~/.claude/settings.json,以及当前 shell 里的环境变量。两条路都能让 headless 模式读到底层 API 参数。
4.1 settings.json 中设置 env 块
在~/.claude/settings.json里加一个env块。Claude Code 启动时会读取这块配置,把它合并到当前进程的环境变量中。示例:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 模型广场为准" } }注意两点。第一,ANTHROPIC_AUTH_TOKEN的值必须是你刚创建的 API Key,把占位符YOUR_API_KEY替换成实际字符,不要在 Key 外面加双引号,也不要留空格。第二,ANTHROPIC_MODEL不要照抄旧文章的模型 ID,Claude 模型列表会更新,去模型广场看当天列表,把对应模型 ID 填进去。模型 ID 填错一般会报 404,而不是 401,但两者都会打断 headless 脚本。
4.2 当前 shell 临时注入环境变量
如果不打算改全局配置,或在 CI 里每个 job 单独传参,可以用环境变量注入。claude -p本身没有--base-url参数,环境变量是唯一注入方式。在 Linux/macOS 的 shell 里:
export ANTHROPIC_BASE_URL=https://taotoken.net/api export ANTHROPIC_AUTH_TOKEN=YOUR_API_KEY export ANTHROPIC_MODEL=<模型ID以模型广场为准> claude -p "..." --output-format json --max-turns 3等号两边不要加空格。若是在 Windows PowerShell 下跑,则是:
$env:ANTHROPIC_BASE_URL="https://taotoken.net/api" $env:ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY" $env:ANTHROPIC_MODEL="<模型ID以模型广场为准>"把这三行写进脚本头,再执行claude -p,请求就会统一从 TaoToken 的通道走。脚本里如果还有别的环境变量(比如GITHUB_TOKEN),照旧保留,互不干扰。有一点容易被忽略:有些 CI runner 预设了ANTHROPIC_BASE_URL指向官方地址,你在脚本里 export 前要先清空或覆盖,否则旧值会和新值打架。执行前可以打印一次环境变量,确认没有残留/v1或旧域名。
5. 把原文的 issue 分类命令改造成可运行版本
原文给了好几个 headless 场景,这里以 issue 分类为例,把完整命令串起来。任务目标是:读取一条 GitHub issue,输出结构化 JSON,类别限定为 bug/feature/docs/question/infrastructure,并且禁止写文件和执行命令。这些要求对应原文强调的“只读任务”。
5.1 最小可运行的 issue 分类命令
先准备两个环境变量,把 issue 标题和正文放在变量里,再调用claude -p:
export ISSUE_TITLE="登录接口在并发请求时偶发 401" export ISSUE_BODY="复现步骤:连续调用 /auth/token 20 次,约 5 次返回 401,服务端日志未见明显异常" claude -p "Classify this GitHub issue into one of: bug, feature, docs, question, infrastructure. Issue title: $ISSUE_TITLE Issue body: $ISSUE_BODY Output ONLY a JSON object: {\"category\": \"...\", \"confidence\": 0.0-1.0, \"reasoning\": \"...\"}" \ --output-format json \ --max-turns 1 \ --disallowedTools "Edit,Write,Bash"--max-turns 1的意思是模型只能回答一轮,不能额外调用工具去翻仓库。--disallowedTools "Edit,Write,Bash"把所有写操作和 shell 执行都掐掉,保证脚本即使跑在 CI runner 上,也不会改动任何文件或连接外部生产环境。这一行配置,就是把 headless 任务限定在“只读”的关键。
5.2 校验 JSON 输出
把输出捕获到变量里再接 jq 校验。jq empty只检查 JSON 语法,不管字段是否齐全;“分类结果”这一步要验证字段真实存在:
RESULT=$(claude -p "Classify this GitHub issue into one of: bug, feature, docs, question, infrastructure. Issue title: $ISSUE_TITLE Issue body: $ISSUE_BODY Output ONLY a JSON object: {\"category\": \"...\", \"confidence\": 0.0-1.0, \"reasoning\": \"...\"}" \ --output-format json \ --max-turns 1 \ --disallowedTools "Edit,Write,Bash") echo "$RESULT" | jq empty 2>/dev/null if [ $? -ne 0 ]; then echo "ERROR: 输出不是合法 JSON" exit 1 fi echo "$RESULT" | jq -e '.category and .confidence' >/dev/null 2>&1 if [ $? -eq 0 ]; then echo "分类结果:$(echo "$RESULT" | jq -r '.category')" echo "置信度: $(echo "$RESULT" | jq -r '.confidence')" else echo "ERROR: 缺少必要字段" exit 1 fi这段脚本的好处是:Base URL 写错时,$RESULT通常是空字符串或错误详情,jq empty会直接暴露格式异常;模型 ID 写错时,错误信息会出现在 stderr,也能很快定位。把这段校验代码加到 CI 的 step 里,headless 任务就不再是“跑完才知道结果”的黑盒。
5.3 原文章节里的其他命令怎么接
原文里还有 changelog 生成、CI 失败分析、文档链接检查三个示例,接入方式完全相同:只要环境变量里已有ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_MODEL,其余命令继续沿用原文的参数设计。比如批量 changelog 生成:
COMMITS=$(git log --oneline v1.2.0..HEAD) claude -p "Generate a changelog from these commits. Group by: breaking, features, fixes, other. Commits: $COMMITS Output markdown with no extra commentary." \ --output-format text \ --max-turns 2 \ --allowedTools "Read,Grep"--allowedTools "Read,Grep"允许模型读package.json或源文件来理解变更含义,但写不了任何文件。--max-turns 2给模型一轮读文件、一轮输出的空间,既限制成本,也避免它在无人监督时绕进多余操作。需要提醒的是,这些命令跑在 CI runner 上,本质上仍是 AI 辅助生成和解释,最终的编译运行、SQL 执行、文件提交都应在本地或专门的执行环境由人触发,不要让claude -p直接执行部署类命令。
6. 从 401 到 200:验证一次 headless 调用,顺手对用量
配置改完,跑一条最简单的 prompt 验证:
claude -p "说一句话说明你已经准备好"如果输出正常返回,说明环境变量、Key、模型 ID 三样都对上了。如果依然报 401,按下面顺序查:
- Base URL 是否多写 /v1。确认
ANTHROPIC_BASE_URL的值是https://taotoken.net/api,没有尾部/v1、没有尾部斜杠、没有 UTM。 - Key 是否拼错或抄多。
YOUR_API_KEY应替换成控制台里的完整字符,注意别把空行或引号带进去。 - 模型 ID 是否被旧教程带偏。去 TaoToken 的模型广场按当前列表填,而不是照抄本文或历史文章的截图。
- CI 里是否有旧环境变量残留。在脚本里执行
env | grep ANTHROPIC,把旧的ANTHROPIC_BASE_URL清掉,再重新 export。
还有一种隐蔽情况:你在settings.json里写对了,但 shell 里之前export过一个错误的临时变量,临时值会覆盖配置文件值,导致看起来明明改对了还是 401。遇到这种情况,先unset ANTHROPIC_BASE_URL再跑一次。
跑通之后,去控制台对一下这次调用。配置保存后,建议先去 TaoToken 模型对话 里用同一把 Key 发一条测试消息,确认模型 ID 和 Base URL 没填错。若要长期跑 headless 批处理,可以打开 Coding Plan 看套餐额度是否够用;创建新的子 Key 统一走 控制台 API Keys。Claude Code 环境变量对照表在 接入文档 里也有,和本文第 4 节完全对得上。