Skip to content

fix(hooks): run CodeBuddy's Windows hooks through cmd.exe - #637

Merged
jeff-r2026 merged 5 commits into
Tencent:mainfrom
zdGrande:fix/codebuddy-ide-windows-hooks
Sep 21, 2026
Merged

jeff-r2026 merged 5 commits into
Tencent:mainfrom
zdGrande:fix/codebuddy-ide-windows-hooks

Conversation

@zdGrande

Copy link
Copy Markdown
Contributor

问题

CodeBuddy 在 Windows 上一向跑不起来 teamai 的 hook。原因有两个,彼此独立。

1. 被当成"这个环境没有 shell"直接跳过

skipToolsWithoutShell() 的门禁只问一件事:existsSync('/bin/sh')。而 Windows 版 CodeBuddy 的安装树里没有任何 POSIX shell(没有 sh.exe/bash.exe),于是门禁判定"该环境无法执行 hook 命令",把 codebuddy 整个从注入名单里摘掉、只留一条 warn,其它工具则不受影响。

但这个问题问错了对象。CodeBuddy 的 hook 运行器是把 command 字符串交给 child_process.spawn(command, [], { shell: true })(genie 的 HookExecutorImpl),Windows 上这条路径走的是 %ComSpec% —— 也就是操作系统一定提供的 cmd.exe。新增的 resolveCodebuddyShell() 正是回答这个问题;POSIX 平台继续保留 /bin/sh 的保守判定。

2. 渲染出来的命令是 POSIX 形态,cmd.exe 读不懂

即便放行,命令串也仍是 POSIX 形态:

PATH="$HOME/.teamai/bin:$PATH" teamai hook-dispatch session-start --tool codebuddy 2>/dev/null || true

cmd.exe 没有 VAR=value command 前缀、没有 /dev/null、也没有 || true,这条串在第一个 token 就会失败。而失败比"静默不生效"更糟:CodeBuddy 会把非 0 的 UserPromptSubmit 判定为 allowed: false直接挡掉用户提问。因此改为 cmd 形态,并逐字复刻 POSIX 的 fail-open 语义:

set "PATH=%USERPROFILE%\.teamai\bin;%PATH%" && teamai hook-dispatch session-start --tool codebuddy 2>nul || exit /b 0

toolUsesCmdShell() 负责在两种形态间切换,范围刻意收得很窄:win32 && codebuddy。WorkBuddy 在 Windows 上继续用 POSIX 形态 —— 它的 hook 运行器是自带 PortableGit 里的 MSYS sh,这一点 resolveWorkbuddyShell() 早已确立 —— POSIX 平台则完全不受影响。

项目作用域的团队 hook 门禁按同样的原则渲染,理由也相同:一条 POSIX 的 if [ "$PWD" = … ] 挡在 cmd 命令前面就是语法错误,会把门禁和载荷一起打死,于是那台机器上团队的遥测注入与上报从来没执行过。两种门禁在 reconcile 侧都能被识别,所以机器在两种形态之间来回切换,也不会留下没有东西能清理的条目。

3. cmd.exe 需要一个 teamai.cmd 才解析得到

PATH="<bin>:$PATH" teamai … 对 POSIX shell 成立,是因为 teamai 本身是一个无扩展名的 sh 脚本。cmd.exe 按 PATHEXT(.COM;.EXE;.BAT;.CMD)解析命令名,永远不会执行无扩展名文件 —— 所以只把 ~/.teamai/bin 前置到 PATH 没有任何作用。ensureTeamaiWrapper() 现在会在 Windows 上于原有 shim 旁边多写一份 teamai.cmd

@echo off
"<node>" "<entry>" %*

两份 wrapper 都使用 node 与 CLI 入口的绝对路径,因此都不依赖 hook 子进程的 PATH。node 的解析顺序是 WorkBuddy 自带运行时 → CodeBuddy 自带运行时 → process.argv[0],而最后这个就是当前这个函数自己正在运行的解释器,所以它不可能缺失。该文件在每次 init / pull / hooks inject 都会幂等重写,删掉后下一次开会话即可自愈。

测试

四个既有测试套件补齐了 Windows 用例,全部通过 spy 驱动 process.platform,因此在 ubuntu CI 上是真正执行、而不是被跳过:

  • hooks-shell-check.test.ts —— win32 下 codebuddy 不再被跳过(不再出现 "no shell is available" 警告),且产出的命令是 cmd 形态;POSIX 门禁保持原样。
  • hooks-wrapper.test.ts —— win32 下会在 POSIX shim 旁边写出 teamai.cmd(含解析到的 node 与入口),其它平台不写。
  • hooks-reconcile-scope.test.ts —— 由任一渲染器写出的项目门禁都能被识别并清理。
  • hooks-golden.test.ts —— codebuddy 在非 Windows 下的输出仍与跨平台 golden fixture 逐字节一致;仅在 win32 跳过,因为该 fixture 是 POSIX 基线,Windows 形态由 hooks-shell-check.test.ts 另钉。

tsc --noEmitnpm run build 与上述四个套件全绿(53 passed,1 skipped)。

平台影响

  • Windows + CodeBuddy:hook 现在能执行。
  • Windows + WorkBuddy:不变(MSYS sh、POSIX 命令、单份 wrapper)。
  • POSIX(任意工具):不变 —— 所有分支都在 process.platform === 'win32' 之后。
  • package.json 未改动。

验证

在安装树内不含任何 POSIX shell 的 Windows + CodeBuddy CN 4.9.8 上验证:注入不再报警、~/.codebuddy/settings.json 为 cmd 形态、%USERPROFILE%\.teamai\bin\teamai.cmd 被创建(删除后于下一次开会话时自动重建)、hook-dispatch 端到端可运行。

Issue

Fixes #579 —— 「windows codebuddy跳过hook」:Windows 下 init 直接把该工具跳过,即上面的原因 (1);而原因 (2) 会在 (1) 被解除后立刻显形。

bundled-runtime: CodeBuddy provides cmd.exe on Windows, so the /bin/sh shell gate is a false negative there. POSIX keeps the old check.

builtin-hooks: render cmd-syntax commands for codebuddy on win32 and write a teamai.cmd shim, so the PATHEXT lookup can resolve the CLI.

hooks: render the project gate in the host shell's syntax and recognise both renderings, so a platform switch cannot leave dead duplicates.

tests: win32 cases with a spied platform, so ubuntu CI covers them.
@jeff-r2026 jeff-r2026 self-assigned this Sep 18, 2026
@zdGrande
zdGrande force-pushed the fix/codebuddy-ide-windows-hooks branch from f362ada to 7d1cc4b Compare September 18, 2026 09:41
@jeff-r2026 jeff-r2026 assigned jeff-r2026 and unassigned jeff-r2026 Sep 18, 2026
@github-actions

Copy link
Copy Markdown

发现

  • [P1] 项目门禁不匹配时会返回非零并阻断提问src/hooks.ts:304 使用 findstr ... && (command);当 CodeBuddy 在目标项目之外运行时,findstr 返回 1,整个 hook 也返回 1。对于 UserPromptSubmit,CodeBuddy 会将其解释为 allowed: false,导致其他项目中的所有提问被阻止。门禁不匹配必须返回 0,同时仅保留载荷自身的退出状态。
  • [P2] wrapper 写入位置与 hook 搜索位置可能不一致src/builtin-hooks.ts:245 固定从 %USERPROFILE%\.teamai\bin 查找,但 ensureTeamaiWrapper() 使用 getUserHome(),其优先级是 HOMEUSERPROFILEos.homedir()。Windows 上若设置了不同的 HOME(Git Bash、自定义环境等)或缺少 USERPROFILEteamai.cmd 会写到一个目录、hook 却搜索另一个目录,最终静默失效。命令应使用与 getUserHome() 相同的已解析路径。

测试说明

  • PR 描述包含类型检查、构建、单测清单以及真实 Windows + CodeBuddy 的端到端验证记录,满足测试计划和 e2e 记录要求;此项不构成阻塞。

@zdGrande
zdGrande force-pushed the fix/codebuddy-ide-windows-hooks branch from 6100d6a to 7d1cc4b Compare September 20, 2026 03:21
@github-actions

Copy link
Copy Markdown

审查发现

  • [P1 阻断] src/builtin-hooks.ts:245 将 wrapper 路径硬编码为 %USERPROFILE%\.teamai\bin,但 ensureTeamaiWrapper() 通过 getUserHome() 写入,而后者优先使用 HOME。Windows 用户设置自定义 HOME 时,teamai.cmd 会写到另一目录,CodeBuddy 仍找不到 teamai,hook 只会静默失效。应使用同一 home 解析结果生成并正确转义路径。
  • [P1 阻断] src/hooks.ts:290 直接把 %CD% 和项目路径拼进 cmd 命令,没有处理 %&!、括号等合法路径字符。比如当前目录包含 & 时,变量展开后会被 cmd 当作命令分隔符,导致门禁失效甚至执行路径中的文本。当前测试只断言字符串形态,没有在 cmd.exe 中覆盖特殊路径。

测试说明

  • PR 描述包含单元测试计划、构建结果及 Windows + CodeBuddy 的真实 CLI 端到端记录,满足测试说明要求。

林志达 added 2 commits September 20, 2026 11:25
hooks: the cmd gate `${gate} && (payload)` let a gate miss inherit findstr's
exit status — non-zero is surfaced as a hook error, and exit 2 blocks
UserPromptSubmit. Use `& if not errorlevel 1 (payload) else exit /b 0`: a miss
exits 0, a match still passes the payload's own status through.

builtin-hooks: the cmd wrapper searched `%USERPROFILE%\.teamai\bin` while the
writer uses getUserHome(), so HOME/USERPROFILE could diverge and `teamai` was
never found. Embed the resolved bin dir; drop TEAMAI_BIN_DIR_WIN.

tests: update the cmd-gate and codebuddy PATH assertions.
@github-actions

Copy link
Copy Markdown

审查结论

  • [P1 阻断] src/hooks.ts:291%CD% 未转义地拼入 cmd.exe 命令。项目或子目录名包含 &|() 等 Windows 合法字符时,门禁会被拆成多个命令,可能导致 hook 失效、UserPromptSubmit 被阻断,甚至执行目录名注入的命令。应使用不会重新解析路径元字符的比较方式,并补充特殊字符路径的真实 cmd 验证。
  • [P2 非阻断] src/builtin-hooks.ts:252src/builtin-hooks.ts:146 将 home、Node 和入口绝对路径直接写入批处理;路径中的 % 会被 cmd 当作环境变量展开,导致 wrapper 无法找到。需要对批处理中的 % 做转义,并覆盖包含 % 的路径测试。

PR 描述包含单元测试计划、构建检查以及 Windows + CodeBuddy 真实 CLI 端到端记录,测试说明本身充分。

@jeff-r2026

Copy link
Copy Markdown
Collaborator

Please resolve the P1 finding and conflicts.

@github-actions

Copy link
Copy Markdown

审查发现

  • [P1 阻断] src/hooks.ts:292%CD% 未转义地拼入 cmd.exe 命令。Windows 目录名允许 &()% 等字符;变量展开后这些字符会被重新解释为控制符。例如位于 C:\src\x&whoami& 时,每次 hook 都可能执行目录名中的命令。需要使用不重新解析路径元字符的目录判断方式,并增加含 &/% 路径的真实 Windows 测试。
  • [P1 阻断] src/bundled-runtime.ts:151 改变了 Windows CodeBuddy 的公开行为,但未同步受影响文档。docs/windows-hooks.md:13docs/windows-hooks.zh-CN.md:11 仍明确声称 CodeBuddy 会被无条件跳过,并继续推荐 Git Bash/WSL 绕行方案。AGENTS.md 要求行为变化同步所有受影响的双语文档。

PR 描述包含构建、测试计划及真实 Windows + CodeBuddy 端到端验证记录,测试说明本身充分。

Cmd shell metacharacters in project roots break the team-hook
project gate; escape the root literal and match with findstr.
Sync tests and docs.
@github-actions

Copy link
Copy Markdown

审查发现

  • [P1 阻断] src/hooks.ts:296 使用 ^ 转义 %,但 cmd.exe 的百分号变量展开发生在 caret 转义之前。因此项目路径如 C:\repo\a%TEMP%b 会被环境变量替换,导致项目门禁匹配错误;若变量值含 cmd 元字符,还可能形成命令注入。新增的真实 cmd 测试包含此案例,但在 Ubuntu 测试中被跳过。请改用不会把项目路径直接插入 cmd 命令的方案,或采用经过真实 cmd.exe 验证的百分号处理方式。

PR 描述包含构建、类型检查、单测以及 Windows + CodeBuddy 真实 CLI 端到端验证记录,测试说明本身充分。

@jeff-r2026
jeff-r2026 merged commit 04b6de2 into Tencent:main Sep 21, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

windows codebuddy跳过hook

2 participants