给 macOS 上的 Antigravity 和 Claude 增加 Proxy:用 Wrapper App 安全接管 Electron 出网
写在前面 这篇文章记录一种我已经在本机验证过的办法:不要直接修改原始应用包,也不要把真实代理地址写死到公开文章里,而是给 Electron 应用外面再包一层极小的 wrapper app。这样既能给 Antigravity、Claude 这类 macOS 桌面应用注入代理,又不会轻易破坏原始签名、升级链路和回滚路径。
很多人第一次遇到这个问题时,第一反应都是去改 /Applications/Claude.app 或 /Applications/Antigravity.app 里面的内容。短期看似乎可行,但长期通常会带来三个问题:
- 应用签名可能失效,后续启动、权限申请和升级都更脆弱。
- 一旦原应用自动更新,你手工改过的内容很容易被覆盖。
- 真实代理地址、内网 IP、鉴权信息如果直接写进应用包或公开文档,后果通常比“配置没生效”更严重。
我最后采用的是一个更稳的方案:
- 保留原始
Claude.app和Antigravity.app不动。 - 在
~/Applications下创建Claude (Proxy).app和Antigravity (Proxy).app。 - wrapper app 只做三件事:加载本地
proxy.env、导出大小写代理环境变量、用--proxy-server启动原始 Electron 应用。 - 如果应用内部还用了 Node/undici 的
fetch,再通过NODE_OPTIONS=--require=...注入一个极小的 bootstrap,把代理继续传到 Node 侧请求链路。
本文中的所有代理地址都用占位符表示,例如:
http://127.0.0.1:PORT
请把它替换成你自己的本地代理入口,不要把真实代理地址、内网 IP、用户名、密码、Token 或公司域名发布到公开文章、仓库或截图里。
1 为什么 wrapper app 比直接改原应用更安全
这类问题的本质不是“Claude 不支持代理”,而是:
- 你需要把 Chromium/Electron 层的代理参数传进去;
- 同时还要尽量照顾 Node 运行时里的
fetch/undici; - 但又不想破坏原始应用包。
而 wrapper app 恰好把这几个目标拆开了:
- 原始应用继续放在
/Applications,不改签名、不改资源、不改app.asar。 - 代理逻辑收敛到一个可读的 shell 脚本里,后续迁移到别的 Electron 应用也很容易。
- 代理地址放在用户目录下的
proxy.env,方便按机器、按用户、按环境切换。 - 回滚极其简单:删掉
~/Applications/<App> (Proxy).app和~/.<app>-proxy即可。
这个模式对下面几类应用都很适合:
- Claude
- Antigravity
- VS Code 及其各种 fork
- 其他可定位到主可执行文件路径的 Electron 桌面应用
2 整体结构长什么样
一套最小可用的结构通常只有下面几部分:
~/Applications/Claude (Proxy).app/
└── Contents/
├── Info.plist
└── MacOS/
└── launch
~/.claude-proxy/
├── proxy.env
└── bootstrap-fetch-proxy.cjs
其中:
Info.plist只是告诉 macOS 这是一个最小应用壳。launch是真正的入口,它负责导出环境变量并调用原始 app。proxy.env用来放私有代理配置。bootstrap-fetch-proxy.cjs用来补 Node/undici 一侧的代理。
3 先准备一份可复用的 Node fetch 代理 bootstrap
这个文件可以直接复用到 Claude、Antigravity 和其他 Electron 应用:
路径建议:
~/.claude-proxy/bootstrap-fetch-proxy.cjs
~/.antigravity-proxy/bootstrap-fetch-proxy.cjs
内容如下:
'use strict';
const path = require('path');
const { createRequire } = require('module');
function firstNonEmpty(...values) {
for (const value of values) {
if (typeof value === 'string' && value.trim() !== '') {
return value.trim();
}
}
}
function resolveAppRequire() {
const explicitPackageJson = firstNonEmpty(process.env.APP_PROXY_APP_PACKAGE_JSON);
const packageJsonPath =
explicitPackageJson ||
path.resolve(
path.dirname(process.execPath),
'..',
'Resources',
'app.asar',
'package.json'
);
return createRequire(packageJsonPath);
}
function resolveUndici() {
const explicitModule = firstNonEmpty(process.env.APP_PROXY_UNDICI_MODULE);
if (explicitModule) {
return require(explicitModule);
}
try {
return require('undici');
} catch (_) {
return resolveAppRequire()('undici');
}
}
function enableFetchProxy() {
const proxyUrl = firstNonEmpty(
process.env.APP_PROXY_URL,
process.env.HTTPS_PROXY,
process.env.HTTP_PROXY,
process.env.https_proxy,
process.env.http_proxy
);
if (!proxyUrl) {
return;
}
try {
const undici = resolveUndici();
if (
typeof undici.setGlobalDispatcher === 'function' &&
typeof undici.EnvHttpProxyAgent === 'function'
) {
undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent());
process.env.APP_FETCH_PROXY_ACTIVE = '1';
}
} catch (error) {
console.error(`[app-proxy] failed to enable fetch proxy: ${error.message}`);
}
}
enableFetchProxy();
这个 bootstrap 的关键点只有两个:
- 优先从环境变量里读代理,而不是把地址写死在代码里。
- 优先尝试
require('undici'),失败后再从目标应用自己的app.asar里解析。
4 Claude:完整可复用做法
先准备目录:
mkdir -p "$HOME/Applications/Claude (Proxy).app/Contents/MacOS"
mkdir -p "$HOME/.claude-proxy"
4.1 Info.plist
文件路径:
$HOME/Applications/Claude (Proxy).app/Contents/Info.plist
内容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>launch</string>
<key>CFBundleIdentifier</key>
<string>com.example.claude.proxy-wrapper</string>
<key>CFBundleName</key>
<string>Claude (Proxy)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
4.2 launch
文件路径:
$HOME/Applications/Claude (Proxy).app/Contents/MacOS/launch
内容:
#!/bin/bash
set -euo pipefail
CONFIG_DIR="${HOME}/.claude-proxy"
ENV_FILE="${CONFIG_DIR}/proxy.env"
BOOTSTRAP="${CONFIG_DIR}/bootstrap-fetch-proxy.cjs"
APP_BIN="/Applications/Claude.app/Contents/MacOS/Claude"
if [[ -f "${ENV_FILE}" ]]; then
set -a
# shellcheck disable=SC1090
source "${ENV_FILE}"
set +a
fi
PROXY_URL="${APP_PROXY_URL:-${HTTPS_PROXY:-${HTTP_PROXY:-http://127.0.0.1:PORT}}}"
NO_PROXY_VALUE="${NO_PROXY:-localhost,127.0.0.1,::1}"
PROXY_BYPASS_LIST="${APP_PROXY_BYPASS_LIST:-localhost;127.0.0.1;::1}"
export APP_PROXY_URL="${PROXY_URL}"
export HTTP_PROXY="${HTTP_PROXY:-${PROXY_URL}}"
export HTTPS_PROXY="${HTTPS_PROXY:-${PROXY_URL}}"
export ALL_PROXY="${ALL_PROXY:-${PROXY_URL}}"
export NO_PROXY="${NO_PROXY_VALUE}"
export http_proxy="${http_proxy:-${HTTP_PROXY}}"
export https_proxy="${https_proxy:-${HTTPS_PROXY}}"
export all_proxy="${all_proxy:-${ALL_PROXY}}"
export no_proxy="${no_proxy:-${NO_PROXY}}"
if [[ -f "${BOOTSTRAP}" ]]; then
if [[ -n "${NODE_OPTIONS:-}" ]]; then
export NODE_OPTIONS="--require=${BOOTSTRAP} ${NODE_OPTIONS}"
else
export NODE_OPTIONS="--require=${BOOTSTRAP}"
fi
fi
# Finder 可能在 Rosetta 环境里启动 wrapper。
# 在 Apple Silicon 机器上,优先按硬件能力判断,然后强制拉起 arm64 Claude。
if [[ "$(/usr/sbin/sysctl -in hw.optional.arm64 2>/dev/null || echo 0)" == "1" ]]; then
exec /usr/bin/arch -arm64 "${APP_BIN}" \
--proxy-server="${PROXY_URL}" \
--proxy-bypass-list="${PROXY_BYPASS_LIST}" \
"$@"
fi
exec "${APP_BIN}" \
--proxy-server="${PROXY_URL}" \
--proxy-bypass-list="${PROXY_BYPASS_LIST}" \
"$@"
别忘了加执行权限:
chmod +x "$HOME/Applications/Claude (Proxy).app/Contents/MacOS/launch"
4.3 proxy.env
文件路径:
$HOME/.claude-proxy/proxy.env
内容:
# 请替换成你自己的本地代理入口,不要写真实内网地址到公开仓库
APP_PROXY_URL="http://127.0.0.1:PORT"
# 可选:Chromium/Electron 的 bypass 列表
# APP_PROXY_BYPASS_LIST="localhost;127.0.0.1;::1"
4.4 bootstrap-fetch-proxy.cjs
把上一节的通用 bootstrap 复制到这里:
$HOME/.claude-proxy/bootstrap-fetch-proxy.cjs
5 Antigravity:做法完全一样,只改三个地方
如果你已经把 Claude 版做通了,Antigravity 基本只是把路径和名字替换掉:
mkdir -p "$HOME/Applications/Antigravity (Proxy).app/Contents/MacOS"
mkdir -p "$HOME/.antigravity-proxy"
Info.plist 只需要把名字改成 Antigravity (Proxy) 即可。
真正关键的是 launch 顶部这几个变量:
CONFIG_DIR="${HOME}/.antigravity-proxy"
ENV_FILE="${CONFIG_DIR}/proxy.env"
BOOTSTRAP="${CONFIG_DIR}/bootstrap-fetch-proxy.cjs"
APP_BIN="/Applications/Antigravity.app/Contents/MacOS/Electron"
如果你的 Antigravity 主程序路径不是上面这个值,就用实际路径替换它。
对应的 proxy.env 同样建议保持私有,只放在用户目录下:
APP_PROXY_URL="http://127.0.0.1:PORT"
6 如何确认代理真的生效了
最简单的验证方式不是只看图标能不能点开,而是看进程参数。
先退出原始应用,再启动 proxy 版应用,然后执行:
pgrep -lf '/Applications/Claude.app/Contents/MacOS/Claude'
pgrep -lf '/Applications/Antigravity.app/Contents/MacOS/Electron'
你应该能在输出里看到类似参数:
--proxy-server=http://127.0.0.1:PORT
--proxy-bypass-list=localhost;127.0.0.1;::1
如果你还想确认 Node 侧 bootstrap 是否被吃到,可以额外检查环境变量或应用日志里是否出现:
APP_FETCH_PROXY_ACTIVE=1
6.1 Apple Silicon 上如果出现 “Claude quit unexpectedly”
这个坑值得单独写出来,因为它表面看像“代理搞坏了 Claude”,但实际根因往往不是代理地址,而是wrapper 在 Finder/Rosetta 场景下把 Claude 以 x64 方式拉起了。
我这次实际排到的现象是:
- Claude 一打开就被 macOS 报 “Claude quit unexpectedly”;
- crash report 里能看到
translated: true和cpuType: "X86-64"; ~/Library/Logs/Claude/main.log里能看到arch: 'x64';- 同一台机器其实是 Apple Silicon,直接强制
arch -arm64启动后,应用就恢复正常。
真正容易写错的地方是这里:
- 很多 shell 脚本喜欢直接用
uname -m判断当前架构; - 但如果 wrapper 自己就是在 Rosetta 环境里起的,这里返回的可能是
x86_64; - 于是你会误判,接着把一个 universal 的 Claude 也以 x64 拉起来;
- 某些版本组合下,Claude 会继续运行,但更慢;另一些组合下会直接崩。
更稳的做法是:
- 用
sysctl -in hw.optional.arm64判断这台机器是否支持 arm64; - 只要结果是
1,就直接arch -arm64 "${APP_BIN}"; - 不要依赖 wrapper 当前进程的
uname -m结果。
如果你已经遇到这个问题,可以这样快速判断:
sysctl -in hw.optional.arm64
grep -n "Starting app" "$HOME/Library/Logs/Claude/main.log" | tail -n 2
如果第一条输出是 1,而日志里还是 arch: 'x64',那基本就可以确认是 wrapper 误把 Claude 以 Rosetta/x64 拉起来了。
修完后,你应该看到类似结果:
arch: 'arm64'
而且 Claude 下载或更新内部资源时,也会从 darwin-x64 切换为 darwin-arm64。
7 这套方法为什么适合 Claude、Antigravity,也适合类似应用
它本质上依赖的是 Electron 的两个稳定能力:
- Chromium 层支持
--proxy-server和--proxy-bypass-list - Node 侧支持
NODE_OPTIONS=--require=...
所以这个方法能不能迁移,关键只看三件事:
- 你能不能找到原始 app 的主可执行文件;
- 它是不是 Electron 或 Chromium 壳;
- 你能不能把私有代理配置从公开内容里隔离出去。
一旦这三点成立,Claude、Antigravity、VS Code 类应用其实都是同一个题。
8 常见坑
8.1 应用明明能打开,但其实还是原始实例
如果原始 Claude 或 Antigravity 已经在运行,再去打开 proxy wrapper,有些应用会把请求转发给已存在实例,而不是按新的参数重新起一个新进程。
所以正确顺序一般是:
- 先退出原始应用;
- 再打开
Claude (Proxy).app或Antigravity (Proxy).app; - 再看进程参数。
8.2 只配了 HTTP_PROXY,但应用内部某些请求还是不走代理
这是因为 Chromium 网络层和 Node/undici 并不是完全同一条链路。只配环境变量,未必能覆盖 Electron 主进程里的所有请求来源;只配 --proxy-server,也未必能覆盖 Node 侧 fetch。
所以更稳的做法是两层都做:
- 环境变量
- Chromium 启动参数
NODE_OPTIONSbootstrap
8.3 不要把真实代理地址写进文章和仓库
这一点值得单独再说一次。
很多教程在截图、脚本、提交记录里直接暴露:
- 真实代理主机名
- 内网 IP
- 代理用户名密码
- 公司域名
- VPN 出口结构
这是完全没必要的。
公开文章只需要使用:
http://127.0.0.1:PORT
私有环境里的真实值只放在:
~/.claude-proxy/proxy.env
~/.antigravity-proxy/proxy.env
9 一句话总结
如果你要给 macOS 上的 Claude、Antigravity 或其他 Electron 应用加代理,最稳的办法不是去改原始 .app,而是在它外面包一个最小 wrapper app,把代理配置、启动参数和 Node fetch bootstrap 都留在用户目录。
这样做的好处是:
- 对原始应用侵入最小;
- 迁移到其他 Electron 应用成本极低;
- 更适合团队沉淀成模板,避免每次重复造轮子;
- 也更容易避免把真实代理基础设施泄漏到公开文档里。