中文 English

给 macOS 上的 Antigravity 和 Claude 增加 Proxy:用 Wrapper App 安全接管 Electron 出网

发布时间: 2026-03-25
macOS Electron 代理 Claude Antigravity 自动化

写在前面 这篇文章记录一种我已经在本机验证过的办法:不要直接修改原始应用包,也不要把真实代理地址写死到公开文章里,而是给 Electron 应用外面再包一层极小的 wrapper app。这样既能给 Antigravity、Claude 这类 macOS 桌面应用注入代理,又不会轻易破坏原始签名、升级链路和回滚路径。

很多人第一次遇到这个问题时,第一反应都是去改 /Applications/Claude.app/Applications/Antigravity.app 里面的内容。短期看似乎可行,但长期通常会带来三个问题:

  1. 应用签名可能失效,后续启动、权限申请和升级都更脆弱。
  2. 一旦原应用自动更新,你手工改过的内容很容易被覆盖。
  3. 真实代理地址、内网 IP、鉴权信息如果直接写进应用包或公开文档,后果通常比“配置没生效”更严重。

我最后采用的是一个更稳的方案:

  1. 保留原始 Claude.appAntigravity.app 不动。
  2. ~/Applications 下创建 Claude (Proxy).appAntigravity (Proxy).app
  3. wrapper app 只做三件事:加载本地 proxy.env、导出大小写代理环境变量、用 --proxy-server 启动原始 Electron 应用。
  4. 如果应用内部还用了 Node/undici 的 fetch,再通过 NODE_OPTIONS=--require=... 注入一个极小的 bootstrap,把代理继续传到 Node 侧请求链路。

本文中的所有代理地址都用占位符表示,例如:

http://127.0.0.1:PORT

请把它替换成你自己的本地代理入口,不要把真实代理地址、内网 IP、用户名、密码、Token 或公司域名发布到公开文章、仓库或截图里

1 为什么 wrapper app 比直接改原应用更安全

这类问题的本质不是“Claude 不支持代理”,而是:

  1. 你需要把 Chromium/Electron 层的代理参数传进去;
  2. 同时还要尽量照顾 Node 运行时里的 fetch/undici
  3. 但又不想破坏原始应用包。

而 wrapper app 恰好把这几个目标拆开了:

  1. 原始应用继续放在 /Applications,不改签名、不改资源、不改 app.asar
  2. 代理逻辑收敛到一个可读的 shell 脚本里,后续迁移到别的 Electron 应用也很容易。
  3. 代理地址放在用户目录下的 proxy.env,方便按机器、按用户、按环境切换。
  4. 回滚极其简单:删掉 ~/Applications/<App> (Proxy).app~/.<app>-proxy 即可。

这个模式对下面几类应用都很适合:

  1. Claude
  2. Antigravity
  3. VS Code 及其各种 fork
  4. 其他可定位到主可执行文件路径的 Electron 桌面应用

2 整体结构长什么样

一套最小可用的结构通常只有下面几部分:

~/Applications/Claude (Proxy).app/
└── Contents/
    ├── Info.plist
    └── MacOS/
        └── launch

~/.claude-proxy/
├── proxy.env
└── bootstrap-fetch-proxy.cjs

其中:

  1. Info.plist 只是告诉 macOS 这是一个最小应用壳。
  2. launch 是真正的入口,它负责导出环境变量并调用原始 app。
  3. proxy.env 用来放私有代理配置。
  4. 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 的关键点只有两个:

  1. 优先从环境变量里读代理,而不是把地址写死在代码里。
  2. 优先尝试 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 方式拉起了

我这次实际排到的现象是:

  1. Claude 一打开就被 macOS 报 “Claude quit unexpectedly”;
  2. crash report 里能看到 translated: truecpuType: "X86-64"
  3. ~/Library/Logs/Claude/main.log 里能看到 arch: 'x64'
  4. 同一台机器其实是 Apple Silicon,直接强制 arch -arm64 启动后,应用就恢复正常。

真正容易写错的地方是这里:

  1. 很多 shell 脚本喜欢直接用 uname -m 判断当前架构;
  2. 但如果 wrapper 自己就是在 Rosetta 环境里起的,这里返回的可能是 x86_64
  3. 于是你会误判,接着把一个 universal 的 Claude 也以 x64 拉起来;
  4. 某些版本组合下,Claude 会继续运行,但更慢;另一些组合下会直接崩。

更稳的做法是:

  1. sysctl -in hw.optional.arm64 判断这台机器是否支持 arm64;
  2. 只要结果是 1,就直接 arch -arm64 "${APP_BIN}"
  3. 不要依赖 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 的两个稳定能力:

  1. Chromium 层支持 --proxy-server--proxy-bypass-list
  2. Node 侧支持 NODE_OPTIONS=--require=...

所以这个方法能不能迁移,关键只看三件事:

  1. 你能不能找到原始 app 的主可执行文件;
  2. 它是不是 Electron 或 Chromium 壳;
  3. 你能不能把私有代理配置从公开内容里隔离出去。

一旦这三点成立,Claude、Antigravity、VS Code 类应用其实都是同一个题。

8 常见坑

8.1 应用明明能打开,但其实还是原始实例

如果原始 Claude 或 Antigravity 已经在运行,再去打开 proxy wrapper,有些应用会把请求转发给已存在实例,而不是按新的参数重新起一个新进程。

所以正确顺序一般是:

  1. 先退出原始应用;
  2. 再打开 Claude (Proxy).appAntigravity (Proxy).app
  3. 再看进程参数。

8.2 只配了 HTTP_PROXY,但应用内部某些请求还是不走代理

这是因为 Chromium 网络层和 Node/undici 并不是完全同一条链路。只配环境变量,未必能覆盖 Electron 主进程里的所有请求来源;只配 --proxy-server,也未必能覆盖 Node 侧 fetch

所以更稳的做法是两层都做:

  1. 环境变量
  2. Chromium 启动参数
  3. NODE_OPTIONS bootstrap

8.3 不要把真实代理地址写进文章和仓库

这一点值得单独再说一次。

很多教程在截图、脚本、提交记录里直接暴露:

  1. 真实代理主机名
  2. 内网 IP
  3. 代理用户名密码
  4. 公司域名
  5. 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 都留在用户目录

这样做的好处是:

  1. 对原始应用侵入最小;
  2. 迁移到其他 Electron 应用成本极低;
  3. 更适合团队沉淀成模板,避免每次重复造轮子;
  4. 也更容易避免把真实代理基础设施泄漏到公开文档里。