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