systemd 服务代理配置导致 Claude 403 错误

问题描述

cc-connect(飞书消息桥接到 Claude Code CLI 的 Go 服务)通过 systemd user service 运行时,所有飞书消息触发的 Claude API 调用均返回 403 错误:

Failed to authenticate. API Error: 403
{"error":{"type":"forbidden","message":"Request not allowed"}}

cc-connect 日志表现为 response_len=101, tools=0,即 Claude 返回了极短的错误响应且没有调用任何工具。

误导性现象:错误信息是 “Request not allowed”(403 Forbidden),很容易误以为是认证/授权问题(token 过期、账户限制、并发会话超限等),但实际上根本不是。

原因分析

根因:systemd 服务不继承用户 shell 的环境变量。

在中国网络环境下,访问 api.anthropic.com 需要代理。用户 shell 中通常已配置了 http_proxy/https_proxy/all_proxy,终端直接运行 Claude Code 一切正常。但 systemd user service 的进程环境是隔离的,不会读取 .bashrc/.zshrc 中的 export。

Claude CLI 在无法连接 API 时,不会报 “connection refused” 或 “timeout”,而是生成一个 model: <synthetic> 的本地错误响应,内容为 403 Forbidden。这进一步增加了误导性——看起来像是服务端拒绝了请求,实际上请求根本没到达服务端。

时间线巧合:问题出现时恰逢 Claude Code 从 2.1.73 自动更新到 2.1.74,更容易误判为版本更新引入的 breaking change。

解决方案

在 systemd service 文件中显式添加代理环境变量:

# ~/.config/systemd/user/cc-connect.service
[Service]
# ... 其他配置 ...
Environment=http_proxy=http://127.0.0.1:7890
Environment=https_proxy=http://127.0.0.1:7890
Environment=all_proxy=socks5://127.0.0.1:7890

然后重载并重启:

systemctl --user daemon-reload
systemctl --user restart cc-connect

诊断方法:env -i 二分法

最终定位问题的关键技巧是用 env -i 模拟 systemd 的最小环境:

# 1. 先用最小环境复现问题
env -i HOME=$HOME PATH=/usr/local/bin:/usr/bin:/usr/sbin \
  claude --output-format stream-json --input-format stream-json
 
# 2. 逐步添加环境变量,直到问题消失
env -i HOME=$HOME PATH=... \
  http_proxy=http://127.0.0.1:7890 \
  https_proxy=http://127.0.0.1:7890 \
  all_proxy=socks5://127.0.0.1:7890 \
  claude --output-format stream-json --input-format stream-json

添加代理变量后问题立即消失,确认根因。

注意事项

  • systemd 服务永远不要假设环境变量存在PATH、代理、LANGDISPLAY 等都需要显式声明
  • Claude CLI 的 <synthetic> 响应会伪装成 API 错误:遇到 403 时先验证网络连通性,再排查认证
  • 不要被时间巧合误导:版本更新 + 出错 ≠ 版本引入 bug,可能只是环境变化的巧合
  • 走过的弯路:检查 token 有效期、清除会话文件、检查并发限制、strace 抓包——这些在网络不通的情况下都不会有收获,应该优先排除最基本的连通性问题

相关链接