装完终端才是开始:8 个 bug,每一个的第一反应都是错的

AI摘要
【知识分享】本文记录作者在安装开源终端Zap后修复8个bug的过程,涵盖图标白板、CLI检测、签名认证、证书信任及SSH测试失败等问题。文章详细分析了每个bug的表象与根因,强调排查时应对比正常与异常路径差异、警惕过滤器隐藏问题,并指出GUI应用不继承shell环境变量等macOS常见陷阱。作者最终修改33行代码,部分问题为上游缺陷,适合开发者参考。

上一篇写了我为了装一个终端(Zap),差点升级系统、差点抹掉一块有数据的 2TB 硬盘的全过程。
博客:装个终端而已:8GB 的 Xcode 下成了 82K 的 HTML,我差点抹掉一块有...
那篇的结尾是「跑起来了」。但跑起来只是开始。

接下来一天,我在这个刚装好的软件里挖出 8 个 bug,改了 12 个文件。其中大半不是我的环境问题,是所有人都会中招的上游 bug

这篇记的是这 8 个 bug。它们有个共同点:每一个的第一反应都是错的。

  • 图标白板 → 以为资源没打包(资源好好的)

  • 检测不到工具 → 以为扫描逻辑写错了(逻辑没错,是环境假设错了)

  • 反复要密码 → 以为是安全设置问题(是签名机制的必然结果)

  • 程序崩溃 → 以为是我刚改的代码(跟我完全无关)

  • 证书不受信任 → 以为证书没建成(建成了,是中间证书过期)

  • SSH 测试失败 → 以为是端口、以为是认证方式(连错两次,真相是没有 TTY)

如果每次都停在第一反应上,八个都修不掉。

最后那个我要单独说:它是三个独立缺陷叠在同一个症状下,而真正破局的不是我读代码,是一句”测试失败但连接可以打开”。这句话我永远不可能自己想到——我没有那台服务器,也没有点按钮的手。


Bug 1:空白图标 —— 一个只打 warning 就继续的构建脚本

装进 /Applications 之后,Dock 里是一张白纸。

第一反应是”图标没打包进去”。查了下,包里躺着一个完好的 Zap.icns,512×512,29 KB。资源在,那就是引用问题:

$ defaults read /Applications/Zap.app/Contents/Info.plist CFBundleIconFile
AppIcon

plist 指向 AppIcon,但包里根本没有叫这个名字的东西。

往上追到构建脚本 script/compile_icon

if ! -f “$BUNDLED_RESOURCES_DIR/Assets.car”; then
echo “Warning: actool did not produce Assets.car…” >&2
echo “(Note that XCode version <26 does not support .icon bundles…)” >&2
fi

然后不管三七二十一,照样改 plist

plutil -replace CFBundleIconFile -string “AppIcon” …

链路是这样的:

  1. 项目用了 macOS 新的 .icon 自适应图标格式

  2. 编译它需要 actool 产出 Assets.car

  3. .icon bundle 只有 Xcode 26+ 支持

    ,我装的是 16.4

  4. actool 没产出 Assets.car

  5. 脚本打了个 warning,然后继续把 plist 指向不存在的资源

修复就是让它在这种情况下别改 plist,保留原有的 .icns 引用:

if ! -f “$BUNDLED_RESOURCES_DIR/Assets.car”; then
echo “Warning: …” >&2
echo “Keeping the existing .icns icon reference in Info.plist.” >&2
exit 0
fi

教训:构建脚本里的 warning 往往就是 bug。 “警告后继续”这个模式很危险——它把一个本该失败的状态包装成了”成功但有点问题”,然后把损坏的产物交给下游。

这个 bug 影响所有用 Xcode < 26 构建的人,他们的图标应该全是空白的。


Bug 2:Codex 检测不到 —— GUI 应用不继承你的 shell PATH

Zap 有个”第三方 CLI 智能体”面板,会扫描本机装了哪些编码 agent。

我机器上装了 4 个,面板只显示 2 个:

Agent 路径 显示?
Claude ~/.local/bin/claude
Antigravity ~/.local/bin/agy
Codex ~/Library/pnpm/codex
Grok ~/.local/bin/grok ❌(另有原因,见 Bug 4 后)

Codex 明明在 PATH 里——我在终端敲 codex 是能跑的。

看扫描代码:

fn cli_agent_search_dirs() -> impl Iterator<Item = PathBuf> {
    let mut dirs = Vec::new();
    if let Some(path_var) = std::env::var_os("PATH") {
        dirs.extend(std::env::split_paths(&path_var));
    }
    extend_common_cli_dirs(&mut dirs);   // 一批硬编码目录
    dedupe_paths(dirs).into_iter()
}

它读了 PATH。那为什么没找到?

$ launchctl getenv PATH
(空)

关键在这里:从 Launchpad / Dock 点开的 App,继承的是 launchd 的环境,不是你 shell 的环境。

你在 .zshrc 里写的 export PATH=...PNPM_HOME=...,对 GUI 启动的应用完全不可见。它只能拿到系统默认 PATH。

所以扫描实际上只依赖那批硬编码目录:

/opt/homebrew/bin /usr/local/bin ~/.cargo/bin ~/.bun/bin
~/.local/bin ~/.nvm/versions/node/*/bin

~/Library/pnpm(pnpm 全局 bin 的 macOS 默认位置)不在里面。

补上就好:

dirs.extend([
home.join(“.cargo/bin”),
home.join(“.bun/bin”),
home.join(“.local/bin”),
// pnpm 全局 bin 的默认位置(macOS 与 Linux)。GUI 启动的应用不继承 shell
// 的 PATH,所以 PNPM_HOME 对扫描不可见,这里按默认路径补上。
home.join(“Library/pnpm”),
home.join(“.local/share/pnpm”),
]);

这是 macOS 上的经典陷阱,任何”我在终端能跑,但 App 里说找不到”的问题,第一个要怀疑的就是它。

同样影响所有用 pnpm 装 CLI 工具的人。


Bug 3:每次重编译都要输钥匙串密码 —— ad-hoc 签名的代价

重新构建、替换、启动,弹框:要求输入登录钥匙串密码。

先查它想访问什么:

$ security find-generic-password -s “dev.zap.Zap”
“acct”=”AgentProviderSecrets”
“svce”=”dev.zap.Zap”

是存 AI 供应商 API Key 的地方。

原因是签名变了。

macOS 钥匙串的访问控制绑定在创建该条目的 App 的代码签名上。而我这个是 ad-hoc 签名(Signature=adhoc,机器上 0 个开发者证书)——ad-hoc 签名是二进制内容的哈希

也就是说:每改一行代码重新编译,签名就变了,在钥匙串眼里就是一个全新的、陌生的 App 在试图读别人的密钥。

点”始终允许”只对当前这个二进制有效,下次重编又白搭。

彻底的解法是搞一个稳定的签名身份——免费的 Apple Development 证书就够(今天为了装 Xcode 刚接受的开发者协议正好能用上)。而且项目脚本本来就在找它:

SIGNING_CERT=”$(security find-identity -p codesigning -v | grep “Apple Development” | …)”
codesign –sign “${SIGNING_CERT:–}” …

${SIGNING_CERT:--} 那个 :-- 是”没找到就用 -(ad-hoc)”。证书建好后脚本自动切过去,不用改代码。


Bug 4:codex 起不来 —— 时间戳破的案

Codex 终于被检测到了,点开却崩了:

Error: spawn …/vendor/aarch64-apple-darwin/codex/codex ENOENT

第一反应是我刚加的 pnpm 检测有问题。但 ENOENT 说的是那个二进制不存在,跟检测无关。

$ ls -la …/vendor/aarch64-apple-darwin/codex/
total 0
drwxr-xr-x@ 2 xiao1024 staff 64 14 Jul 04:01 .

空目录。 整个 codex 包只有 3.9 MB,而正常带 Rust 二进制的包应该是几十 MB。

破案的是时间戳:

路径 修改时间
包的其余部分 4 May 22:29(安装时)
codex/ 空目录 14 Jul 04:01

安装是 5 月,二进制是 7 月被删的。 不是装的时候就没成功,是装好之后被什么东西清掉了——大概率是清理工具或杀毒软件误删(macOS 上未签名的 Rust 二进制经常中招)。

重装解决:

pnpm remove -g @openai/codex && pnpm add -g @openai/codex

教训:ls -la 的时间戳能告诉你”什么时候坏的”,这经常比”哪里坏了”更快指向元凶。


Bug 5:证书建好了,但系统说不受信任

Bug 3 的解法是”建一个免费的 Apple Development 证书”。在 Xcode 里点几下就建好了,然后:

$ security find-identity -p codesigning -v
0 valid identities found

一个有效身份都没有。

第一反应是证书没建成功。但去掉 -v(只看有效的)过滤器再查:

$ security find-identity
1) 8D9765DA… “Apple Development: peterzh1024@gmail.com (3AVVCC3T9K)”
(CSSMERR_TP_NOT_TRUSTED)

证书在,但不受信任。 这是两个完全不同的问题。

代码签名证书是一条链:

Apple Root CA

WWDR 中间证书

Apple Development: 你的邮箱

我的证书本身没问题(有效期 2026-07-29 → 2027-07-29),断的是中间那一环

查了下钥匙串里的 WWDR:

subject = … CN=Apple Worldwide Developer Relations Certification Authority
notBefore = Feb 7 2013
notAfter = Feb 7 2023 ← 过期三年多了

而新签发的开发证书,签发者是OU=G3 标识的另一张 WWDR

issuer = CN=Apple Worldwide Developer Relations Certification Authority, OU=G3

名字几乎一样,但不是同一张证书。 Apple 在 2020 年换发了新的中间证书,2013 年那张在 2023 年 2 月过期。

怎么确认是不是同一张?看密钥标识符:

开发证书的 Authority Key Identifier(我的签发者是谁)

09:FE:C0:15:90:F9:AF:64:0A:92:12:B9:26:28:63:0C:97:EC:A7:B2

下载的 G3 中间证书的 Subject Key Identifier(我是谁)

09:FE:C0:15:90:F9:AF:64:0A:92:12:B9:26:28:63:0C:97:EC:A7:B2

一模一样,就是它。

从 Apple 官方 CA 页面下载并导入:

curl -sSLO www.apple.com/certificateauthority...
security add-certificates -k ~/Library/Keychains/login.keychain-db AppleWWDRCAG3.cer

再查:

$ security find-identity -p codesigning -v
1) 8D9765DA… “Apple Development: peterzh1024@gmail.com (3AVVCC3T9K)”
1 valid identities found

重新构建后签名链完整了:

Authority=Apple Development: peterzh1024@gmail.com (3AVVCC3T9K)
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA

TeamIdentifier=63RZ5YUHBY # 之前是 “not set”

从此重新编译不再弹钥匙串密码。

这个坑很隐蔽的地方在于:那张过期的 WWDR 平时完全不碍事。 你不建新证书就永远不会暴露。而网上绝大多数教程只说”去 Xcode 建个证书”,不会提 2013 版中间证书过期这回事——因为写教程的人机器上早就更新过了。

教训:-v 这类”只显示有效项”的过滤器会把问题藏起来。 当一个东西”查不到”时,先去掉过滤器确认它是”不存在”还是”存在但状态不对”——这两者的排查方向完全相反。


加 Grok Build:穷尽匹配的红利

除了修 bug,我还给它加了 xAI 的 Grok Build TUIgrok --help 第一行就是这个名字)。

项目内置了 14 个 CLI agent,没有 Grok:

Claude / Gemini / Codex / Amp / Droid / OpenCode / Copilot
Pi / Auggie / Cursor / Goose / DeepSeek / Antigravity / Omp

有两条路:

方案 A(不改代码):配置里有个正则映射表,可以把任意命令映射到”通用 CLI Agent”。5 分钟搞定,但显示名是通用的 “CLI Agent”,没有专属图标。

方案 B(改代码):加一个枚举变体,成为一等公民。

我选了 B。而这里有个值得说的工程细节——项目的 AGENTS.md 有一条强制规范:

match 语句禁止使用 _ 通配(除非确实需要),保持穷尽匹配。

平时看这条会觉得啰嗦。但当你要加一个枚举变体时,它的价值就出来了:

编译器会带着你走一遍所有需要改的地方。

我只是在枚举里加了一行 Grok,,然后 cargo check 就精确地报出了 6 处遗漏:命令前缀、显示名、图标、技能提供方、品牌色、遥测映射,外加 3 个分组 match。一处都不会漏,一处都不用猜。

如果这些 match 都写了 _ => ...,新变体会静默落进默认分支,然后在运行时以各种诡异的方式表现出来。

最终改动:

app/src/server/telemetry.rs | 1 +
app/src/terminal/cli_agent.rs | 20 +++++++++++++++
app/src/terminal/cli_agent_sessions/listener/mod.rs | 1 +
…/cli_agent_sessions/plugin_manager/mod.rs | 1 +
app/src/terminal/cli_agent_tests.rs | 1 +
app/src/terminal/view/use_agent_footer/mod.rs | 1 +
crates/warp_core/src/ui/icons.rs | 2 ++
script/compile_icon | 7 ++++++-
8 files changed, 33 insertions(+), 1 deletion(-)

外加一个新的 grok.svgcargo check 0 错误,检测测试 28/28 通过。

一个顺手发现的坑

Grok CLI 装了一个叫 agent 的符号链接:

~/.grok/bin/agent -> ../downloads/grok-0.2.112-macos-aarch64

而 Zap 里:

CLIAgent::CursorCli => “agent”,

agent 启动 Grok,会被识别成 Cursor。grok 调用就没事。这类命令名撞车在 CLI 工具越来越多的今天大概会越来越常见。



Bug 6、7、8:同一个症状下的三层 bug

这是全场最难的一个,值得单独讲——因为三个独立缺陷叠在一起,症状一模一样,我连错两次才摸到真相。

症状

SSH 管理器里,密码模式连非 22 端口的服务器,点「测试连接」永远失败:

root@xx.xx.xx.119: Permission denied (password).

日志显示 17 分钟内试了 10 次。

我的第一次误判:端口

第一反应是端口没传给 ssh。查代码,果然找到一个确凿的缺陷

// on_connect 和 on_test_connection 里各有一份
let port: u16 = port_str.trim().parse().unwrap_or(22); // ← 解析失败静默变 22

而且输入框里那个 22set_placeholder_text灰色提示,不是真实内容——框子空着时 parse() 失败,于是测试连的是 22 端口,却把该端口的认证失败报成你填的那个端口的失败。

这是真 bug,我修了。但它不是元凶。

我的第二次误判:认证方式

接着发现测试时强行禁掉了 keyboard-interactive:

“-o”, “PreferredAuthentications=password”,
“-o”, “KbdInteractiveAuthentication=no”,

很多服务器(尤其走 PAM 的)只提供 keyboard-interactive,不提供 password 客户端把唯一可用的方法禁掉,服务器当然拒。

这也是真 bug。原作者禁它是为了避免 pam_faildelay 累积顶满超时——为了防超时,把一整类合法配置判死了。

修的时候我直接把那两行拆了。结果被一句话纠正:

「兼容他说的超时问题吧」

对。我那个改法能跑,但是错的工程决策——原作者的顾虑是真实的。改成两阶段:先走原来的快路径,只在被明确拒绝时才放开 kbd-interactive 重试一次。常规服务器耗时零变化。

这不是信息层面的纠正,是品味层面的。 我的第一版方案更差。

修完再测——还是失败。

破局:一句我永远不可能自己知道的话

「测试是错误的 连接 加密码又登录了」

同一台机器、同一个端口、同一个密码,双击打开终端手动输密码能登录成功,只有测试按钮失败。

这句话把范围从”整条 SSH 链路”瞬间锁死到”测试路径与连接路径之间的差异”。我没有他的服务器、没有他的手、看不到他屏幕上发生了什么——这个观察不可能来自读代码。

两条路径的差异只有一个:

路径 密码怎么送
实际连接 真 PTY,用户手动输入 / 注入器写入
测试连接 pipe stdin

真相:TTY

$ ssh -V
OpenSSH_9.9p2

$ ps -o tty= -p <Zap 的 PID>
?? # 没有控制终端

OpenSSH 的 read_passphrase() 默认从 /dev/tty 读密码,不是 stdin。

从 Dock / Finder 启动的 App 没有控制终端,/dev/tty 打不开,ssh 拿不到密码,于是以”无密码”发起认证 → 服务器返回 Permission denied (password)

而代码里的注释,恰好断言了相反的事:

//! 非 Windows:ssh 在 pipe stdin 模式下能正常从 stdin 读密码

这个假设在开发时是成立的。 从终端跑 cargo run,进程继承了 shell 的 tty,测试一切正常。打包成 .app 从 Dock 启动,tty 消失了,功能就死了。

有意思的是,Windows 分支早就遇到过同类问题并解决了——Win32-OpenSSH 无控制台时会打印 GetConsoleMode on STD_INPUT_HANDLE failed 挂死,所以那边用了 SSH_ASKPASS:写一个临时脚本,ssh 派生它、把 stdout 当密码读,完全绕过 stdin 和 tty。

修复就是把它变成跨平台。Unix 侧写一个 #!/bin/sh\nexec cat "$FILE" 的脚本(用 exec cat 而不是 echo,密码含 $、引号、&|<> 都能原样输出不被 shell 二次解析),权限收到 0700,密码文件 0600,RAII 守卫保证 ssh 退出后立即删除。

改完,日志里那条 Permission denied 彻底消失。

这一个 bug 教了三件事

① 同一个症状可以有多个独立原因。 三个缺陷都会导致 Permission denied (password),一模一样。修掉前两个之后症状不变,很容易误判成”没修对”,实际是”还有第三个”。

② 开发环境和生产环境的隐性差异最要命。 有 tty 和没 tty 的区别,在 cargo run 下永远暴露不出来。这类 bug 只在打包、安装、从图标启动之后才现身——而那通常已经是你以为”做完了”的时候。

③ 有些信息只能来自用手用软件的人。 我能在几秒内读完 769 行代码并记住第 3249 行写了什么。但”测试失败可连接成功”这个观察,只能来自真的去点了那两个按钮的人。


复盘:这 8 个 bug 的共同点

把它们排在一起看:

表面现象 实际原因 要看的层级
图标是白板 plist 指向不存在的资源 构建脚本
检测不到 Codex GUI 应用不继承 shell PATH 进程环境模型
老要输密码 ad-hoc 签名每次构建都变 代码签名机制
Codex 崩溃 二进制被第三方删了 文件时间戳
证书不受信任 中间证书 2023 年就过期了 证书链
SSH 测试失败(三层) 端口静默降级 / kbd-int 被禁 / GUI 无 TTY 进程有没有控制终端

注意最后一行有三个原因。 前两个都是真缺陷、都修了,症状却纹丝不动——因为还有第三个。这是最容易让人放弃的情况:你明明改对了东西,现象却没变。

真正有用的动作只有一个:不要相信表象,往下多看一层。

而”往下看一层”具体是什么,其实很具体——就是几条命令:

现象 别猜,去看
图标白板 defaults read Info.plist —— 它到底指向哪
检测不到 launchctl getenv PATH —— App 到底拿到什么环境
要密码 security find-generic-password —— 它到底要访问什么
崩溃 ls -la —— 时间戳告诉你什么时候坏的
查不到 去掉 -v 等过滤器 —— 是”不存在”还是”存在但无效”
同一功能有的路径好有的坏 对比两条路径的差异,而不是盯着坏的那条查

这六条,比六次「我觉得可能是」有用得多。

最后一条是 SSH 那个 bug 教的,也是最贵的。我盯着”测试为什么失败”查了两轮,都在错误的方向上。真正有效的问题是“能用的那条路径和不能用的那条,差在哪”——问出这个问题,答案立刻收敛到 pipe stdin 与 PTY 的区别,进而到 TTY。

盯着坏的地方看,容易越看越深、越钻越偏。对比好的和坏的,差异会自己浮出来。

最后一条我想单独强调:过滤器会藏问题。

security find-identity -p codesigning -v 返回”0 个”,很容易理解成”证书没建成”,然后你就去重建证书——重建一百次也没用,因为问题根本不在那儿。去掉 -v 才看得到真相:证书在,但 CSSMERR_TP_NOT_TRUSTED

“没有结果”和”有结果但被过滤掉了”,排查方向完全相反。 任何时候查不到东西,先确认自己是不是在看一个被过滤过的视图。


最后

5 个 bug 里,3 个是上游问题(图标、pnpm 检测,以及顺带发现的命令名撞车),其中 2 个值得提 PR。代码改动加起来 33 行。

另外 2 个是环境问题(ad-hoc 签名、过期的中间证书),改不了代码,但每个用 macOS 编译自己软件的人迟早都会撞上

装一个开源软件,跟”参与一个开源项目”之间的距离,可能比想象中短很多。它不需要你先读懂整个架构——它只需要你在被某个具体的东西硌到时,多往下看一层。

我改的这 33 行,没有一行需要理解那 67 个 crate 是怎么组织的。它们全是”这里明显不对”的直接反应。

上一篇:《装个终端而已:8GB 的 Xcode 下成了 82K 的 HTML,我差点抹掉一块有数据的 2TB 硬盘》

本人项目地址:github.com/xaiwind/warp-zap-ailap 基于warp-zap 做了些改动,mac本地实测可用。以上文章由claude code 根据工作记录生成 ,本人审核负责。如果文章对你有一点帮助,欢迎点赞、评论、收藏、关注!我是想风,@xaiwind 一个关注出海与 AI 的创作者。 本文首发想风技术文档

本作品采用《CC 协议》,转载必须注明作者和本文链接
唯有坚持,滴水穿石----will
zhaocrazy
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
编程AI 出海 @ 数字游民
文章
87
粉丝
29
喜欢
69
收藏
152
排名:502
访问:1.7 万
私信
所有博文
社区赞助商