English summary — On Windows, the env block injected into ~/.bashrc contains
[ -f C:\Users\<user>\.teamai/env.sh ] && source C:\Users\<user>\.teamai/env.sh.
Because an unquoted \ is an escape character in POSIX shells, the word degrades to
C:UsersCarlos.teamai/env.sh, the [ -f ] test is false, && short-circuits and
source never runs — so no env variable is ever delivered. teamai doctor still
prints ✔ Env variables injected in shell profile because the check only greps for the
marker comment. The only visible symptom is downstream: MCP entries using ${VAR} get
skipped with unresolved variable(s).
Description
Windows 上 teamai pull(或 teamai init)注入到 shell profile 的 env 块,source 行用的是原生 Windows 路径(反斜杠):
# [teamai:env:start]
# DO NOT EDIT: This section is auto-managed by teamai
[ -f C:\Users\<user>\.teamai/env.sh ] && source C:\Users\<user>\.teamai/env.sh
# [teamai:env:end]
在没有引号的 POSIX shell 词里,\ 是转义符。上面这一行实际被解析成:
+ '[' -f C:UsersCarlos.teamai/env.sh ']'
exit=1
反斜杠被吞掉,得到一个不可能存在的路径 → [ -f ... ] 为假 → && 短路 → source 从未执行。于是 ~/.teamai/env.sh 里的变量永远进不了任何 shell。
而 .bashrc 里一个失败的 [ 测试既不报错也不中断,所以从用户视角完全静默:没有任何输出、没有非零退出影响会话。
更糟的是 teamai doctor 仍然显示通过:
✔ Env variables injected in shell profile
因为该检查只断言 profile 文本里出现过标记串 # [teamai:env:start],从不验证这个块是否真的能加载。于是失败被掩盖,唯一的可见症状跑到下游去了——MCP 定义里引用 ${VAR} 的服务被跳过,输出一句 unresolved variable(s),完全指不到 env.yaml 或 shell profile 头上。
Root cause
generateShellBlock() 把 path.join() 的结果(win32 → 反斜杠)原样插进 shell 脚本:
generateShellBlock(teamaiHome) {
const lines = [
TEAMAI_ENV_START,
"# DO NOT EDIT: This section is auto-managed by teamai",
`[ -f ${teamaiHome}/env.sh ] && source ${teamaiHome}/env.sh`,
TEAMAI_ENV_END
];
return lines.join("\n");
}
teamaiHome 来自 getDataHome() → getTeamaiHome(),即 path.join(home, ".teamai"),在 Windows 上必然是 C:\Users\<user>\.teamai。这里既没有换成分隔符、也没有加引号。
配套的 doctor 检查只做字符串包含判断:
{
name: "Env variables injected in shell profile",
check: async () => {
if (teamConfig?.sharing?.env?.injectShellProfile === false) return true;
if (!localConfig) return true;
const envYamlPath = path.join(localConfig.repo.localPath, "env", "env.yaml");
if (!await pathExists(envYamlPath)) return true;
const home = getUserHome();
// ...
return content?.includes(TEAMAI_ENV_START) ?? false;
},
fix: "Run `teamai pull` to inject env variables into shell profile"
}
整条链路里没有任何一步做「目标 shell 需要什么形式的路径」这个转换或校验。
Reproduction
- Windows 11 + Node.js +
teamai 0.24.0,团队仓库含 env/env.yaml 且至少有一个变量。
teamai pull → 上面的反斜杠块写入 ~/.bashrc。
- 用
sh -x 跑这个块:
$ sh -x ~/.bashrc # 或单独把该块存成文件后执行
+ '[' -f C:UsersCarlos.teamai/env.sh ']'
exit=1
- 确认变量从未到达 shell:
$ bash -lc 'echo "MY_VAR=[${MY_VAR}]"'
MY_VAR=[]
teamai doctor → 依旧 ✔ Env variables injected in shell profile。
对照:同一个块用 POSIX 形式写就能工作(这是我本机临时修正后的形态):
# [teamai:env:start]
# DO NOT EDIT: This section is auto-managed by teamai
[ -f "/c/Users/Carlos/.teamai/env.sh" ] && source "/c/Users/Carlos/.teamai/env.sh"
# [teamai:env:end]
$ bash -lc 'source ~/.bashrc >/dev/null 2>&1; echo "len=${#MY_VAR}"'
len=16
Environment
- OS: Windows 11 25H2 (10.0.26200)
- Node.js:
v22.22.2(v22.17.0 同样复现)
- teamai:
0.24.0
- Provider: GitHub
- AI tool(s): WorkBuddy / CodeBuddy —— 注意本缺陷与具体工具无关,只要 profile 是 POSIX shell(
~/.bashrc、~/.zshrc)就会中招
Suggested fix
两处独立的小改动,建议都做:
-
输出 shell 安全的路径。 在生成块的地方做转换,而不是改 path 的计算结果(同一个值还要用于真实文件系统访问)。推荐直接回避绝对路径,用 $HOME 并加引号:
[ -f "$HOME/.teamai/env.sh" ] && source "$HOME/.teamai/env.sh"
如果需要绝对路径(例如 dataHome 被自定义到别的盘),则应转成目标 shell 能懂的形式(/c/Users/... 这类 MSYS 风格)并始终加引号——注意 Program Files 这种带空格的路径只有加引号才安全。
-
让 doctor 验证行为而不是验证存在。 现在只要字符串在就算通过。建议在 shell 里真正 source 一次 profile(或至少求值那个 [ -f ... ] 测试)并断言其中一个变量确实解析成功,否则报 .bashrc 中的 env 块无法加载。
Logs
~/.bashrc 中被注入的块,以及它的求值结果
$ grep -A3 'teamai:env:start' ~/.bashrc
# [teamai:env:start]
# DO NOT EDIT: This section is auto-managed by teamai
[ -f C:\Users\Carlos\.teamai/env.sh ] && source C:\Users\Carlos\.teamai/env.sh
# [teamai:env:end]
$ sh -x <把上面的块存成文件后执行>
+ '[' -f C:UsersCarlos.teamai/env.sh ']'
exit=1
即:source 分支根本没被走到。
teamai doctor 的假通过
$ teamai doctor
✔ Env variables injected in shell profile
Description
Windows 上
teamai pull(或teamai init)注入到 shell profile 的 env 块,source行用的是原生 Windows 路径(反斜杠):在没有引号的 POSIX shell 词里,
\是转义符。上面这一行实际被解析成:反斜杠被吞掉,得到一个不可能存在的路径 →
[ -f ... ]为假 →&&短路 →source从未执行。于是~/.teamai/env.sh里的变量永远进不了任何 shell。而
.bashrc里一个失败的[测试既不报错也不中断,所以从用户视角完全静默:没有任何输出、没有非零退出影响会话。更糟的是
teamai doctor仍然显示通过:因为该检查只断言 profile 文本里出现过标记串
# [teamai:env:start],从不验证这个块是否真的能加载。于是失败被掩盖,唯一的可见症状跑到下游去了——MCP 定义里引用${VAR}的服务被跳过,输出一句unresolved variable(s),完全指不到 env.yaml 或 shell profile 头上。Root cause
generateShellBlock()把path.join()的结果(win32 → 反斜杠)原样插进 shell 脚本:teamaiHome来自getDataHome()→getTeamaiHome(),即path.join(home, ".teamai"),在 Windows 上必然是C:\Users\<user>\.teamai。这里既没有换成分隔符、也没有加引号。配套的
doctor检查只做字符串包含判断:整条链路里没有任何一步做「目标 shell 需要什么形式的路径」这个转换或校验。
Reproduction
teamai0.24.0,团队仓库含env/env.yaml且至少有一个变量。teamai pull→ 上面的反斜杠块写入~/.bashrc。sh -x跑这个块:teamai doctor→ 依旧✔ Env variables injected in shell profile。对照:同一个块用 POSIX 形式写就能工作(这是我本机临时修正后的形态):
Environment
v22.22.2(v22.17.0同样复现)0.24.0~/.bashrc、~/.zshrc)就会中招Suggested fix
两处独立的小改动,建议都做:
输出 shell 安全的路径。 在生成块的地方做转换,而不是改
path的计算结果(同一个值还要用于真实文件系统访问)。推荐直接回避绝对路径,用$HOME并加引号:如果需要绝对路径(例如
dataHome被自定义到别的盘),则应转成目标 shell 能懂的形式(/c/Users/...这类 MSYS 风格)并始终加引号——注意Program Files这种带空格的路径只有加引号才安全。让
doctor验证行为而不是验证存在。 现在只要字符串在就算通过。建议在 shell 里真正 source 一次 profile(或至少求值那个[ -f ... ]测试)并断言其中一个变量确实解析成功,否则报.bashrc 中的 env 块无法加载。Logs
~/.bashrc中被注入的块,以及它的求值结果即:
source分支根本没被走到。teamai doctor的假通过