fix(env): 阻断 Claude 会话身份标记经 pm2/tmux 泄漏进 bot CLI(转写静默停写) - #625
Conversation
CLAUDE_CODE_CHILD_SESSION 等标记是 Claude Code 注入其 Bash 子进程的会话身份, 从 bot 会话里发起的 pm2 delete+start 会把它们快照进 pm2 存档;共享 tmux server 随最后一个会话关闭自动退出后,下一次 spawn 从带毒 daemon fork 出新 server, 标记进入 server 全局环境——此后所有 bot CLI 自认嵌套子会话,转写落盘静默关闭, --resume 连续性(挂掉重启/切角色 respawn)全部失效。2026-07-27 实机事故。 修复(单一规则源 child-env.ts): - 新增 CLAUDE_SESSION_MARKER_ENV_KEYS 并入 REDACTED_CHILD_ENV_KEYS: 贯通 pty redact、pane wrapper unset、tmux client strip 三层 - 并入 TMUX_SERVER_GLOBAL_SCRUB_KEYS:daemon 启动即自愈已带毒的运行中 server - scrubClaudeSessionMarkerEnv 在 pm2Env/daemon boot/worker boot 三边界清洗 - per-bot env 拒绝显式配置身份标记(CLAUDE_EFFORT 仍可配,行为旋钮非身份) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deepcoldy
left a comment
There was a problem hiding this comment.
首审结论:✅ 无阻塞(Approve pending codex 复审 + 申晗拍板)
由 Claude 做首次 review。在隔离 worktree 钉住 head SHA b7a123c1 拉起,读全量改动 + 对拍当前 master + 本地 build/test(fork PR 无 CI)。未合码——按分工等 codex 复审、无申晗确认前不合。
这个 PR 在解决什么(白话)
Claude Code 会给它跑的每个子进程注一组"会话身份"环境变量:CLAUDECODE、CLAUDE_CODE_CHILD_SESSION、CLAUDE_CODE_SESSION_ID、CLAUDE_CODE_ENTRYPOINT、CLAUDE_CODE_EXECPATH、CLAUDE_PID。其中最致命的是 CLAUDE_CODE_CHILD_SESSION=1:任何 claude CLI 看到它,就认为自己是"嵌套子会话",于是静默关掉转写落盘(transcript)。而 botmux 的会话续命(tmux 挂了 respawn、切角色)全靠 claude --resume <session-id>——转写没了,respawn 后上下文就悄无声息地全丢。
泄漏路径(2026-07-27 dev-beta 实机事故):
- 从某个 bot 会话内部跑了
pm2 restart/botmux restart; - pm2 会把"发起这条命令的 shell 的环境"快照进它管理的所有 app(含 daemon);
- 共享 tmux server 随最后一个 bmx-* 会话关闭而自动退出;
- 下一条消息触发 spawn,worker 从带毒的 daemon fork 出一个新 tmux server,这些标记就进了 server 的全局环境表;
- 此后全机所有 bot 的 CLI 都继承
CLAUDE_CODE_CHILD_SESSION=1,集体停写转写。
现有防线只挡了 6 个标记里的 1 个(CLAUDECODE),其余 5 个全部穿透。
改动逻辑
单一规则源 src/utils/child-env.ts:
- 新增
CLAUDE_SESSION_MARKER_ENV_KEYS(6 个标记),并入两处既有集合:REDACTED_CHILD_ENV_KEYS→ 自动贯通已有的三层防线:pty redact、pane wrapperunset(tmux/zellij 共用的SHELL_WRAPPER_SCRIPT)、tmux client strip;TMUX_SERVER_GLOBAL_SCRUB_KEYS→ daemon 启动时的scrubTmuxServerGlobalEnv会自愈一个已经带毒、仍在运行的 tmux server,不必等它自然消亡;
- 新增
scrubClaudeSessionMarkerEnv(),在与scrubSessionCliHomeEnv完全相同的三个进程边界调用(cli.ts的pm2Env/index-daemon.tsboot /worker.tsboot),覆盖 staledump.pm2resurrect 路径; CLAUDE_EFFORT作为 boundary-only 键:在三边界清除(继承穿透 pm2→daemon→worker 的值只可能是发起会话自己的 effort,会静默固化到所有 bot),但不进 pane unset / server scrub——保留两条合法配置通道:per-botenv(所有 backend 含 PTY,在 redact 之后注入)与 pane shell profile(tmux/zellij);- per-bot
env拒绝显式配置这 6 个身份标记(与CLAUDECODE同待遇),CLAUDE_EFFORT仍可 per-bot 配置。
我验证了什么
① 用我自己的 live Claude env 对拍标记列表 — 我此刻就跑在 Claude Code 里,env | grep CLAUDE 出来的会话身份标记正好是这 6 个(含 CLAUDE_CODE_CHILD_SESSION=1)+ CLAUDE_EFFORT。列表真实、完整、无缺漏。
② narrow 6-key 显式枚举是本 PR 的关键正确决策 — daemon 日志证明:真实 bot 会经 per-bot env 合法注入 CLAUDE_CODE_SUBAGENT_MODEL、CLAUDE_CODE_MAX_CONTEXT_TOKENS、CLAUDE_CODE_ATTRIBUTION_HEADER(gateway 部署,来自 launch alias)。如果偷懒用 CLAUDE_CODE_* 前缀一把扫,就会误伤这些行为旋钮。runtime 实测确认:6 个身份标记 reserved=true,而这些 gateway 旋钮 + CLAUDE_EFFORT reserved=false(仍可配)。
③ 三层防线 + server scrub 确实按描述消费这些集合 — 读源码逐条核对:PANE_ENV_UNSET_KEYS ← REDACTED_CHILD_ENV_KEYS,且 wrapper 里 unset 跑在 per-bot /usr/bin/env 注入之前,所以 per-bot 的 CLAUDE_EFFORT 能存活。zellij 复用同一 SHELL_WRAPPER_SCRIPT 路径。
④ server-global scrub 打默认 socket(可能是用户自己的 tmux server)属于既有行为 — 这些标记与已被 scrub 的 CLAUDECODE 同类(会话身份,在全局表里 stale-by-definition),且刻意排除 user-wide GITHUB_TOKEN,与既有边界一致。非阻塞。
⑤ 无隐藏 copy-point、无 botmux 代码读取这些标记(只写/注释)。
⑥ 合当前 master 干净 — master 已从 PR base b30e8949 前移到 fdb105a8(并入 #611/#588/#621,同改 worker.ts/cli.ts/daemon.ts)。真 merge commit 试合 0 冲突,合成树 build 绿,PR 逻辑 grep 确认存活。
测试(隔离 worktree,pnpm build 绿)
PR 用例(child-env / per-bot-env / tmux-env-isolation) 35/35 passed
env+backend 套件(+ tmux-backend-env / herdr-backend) 137/137 passed
↑ PR head 与「PR+当前 master 合成树」两处都过
全量:干净 master 与合成树 失败集完全一致 = 10 failed
(scheduler 2 / v3-distillation 6 / schedule-card 1 / fs-policy-bwrap 1)
= 本机 time/API-creds/bwrap baseline,与 env-scrub 无关
(minimalClaudeEnvironment 用 AWS/Bedrock allowlist,不碰任何被 scrub 的标记)
→ 零回归
残留
- @codex 复审(cc ou_17f09bc94fb29ee8e7c66416f42d5804);
- 合码 / 发版 / live 部署各需申晗另行授权。
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
Codex 复审结论:✅ Approve(无阻塞)
独立复核了 PR head b7a123c1 的完整 diff、Claude Code 实际判定逻辑、环境构造调用链、各后端/会话类型影响,以及与远端当前 master=fdb105a8 的三方合成树。结论与首审一致:修复必要,边界选择正确,未发现需要改代码的 blocker。
关键正确性
- 根因与修复点吻合。 本机 Claude Code
2.1.220二进制中,转写持久化确实读取CLAUDE_CODE_CHILD_SESSION,并包含Transcript saving is off — inherited CLAUDE_CODE_CHILD_SESSION marker的告警路径;botmux 的 pane wrapper 又会清掉TMUX,不能依赖 Claude 自己通过 tmux global env 识别并忽略 stale marker。因此必须在 botmux 边界主动清除。 - 规则源与三层防线闭合。 六个身份键集中在
CLAUDE_SESSION_MARKER_ENV_KEYS,同时驱动 direct/PTYredactChildEnv、tmux client strip、tmux/zellij pane wrapper unset,并进入 server-global scrub;不存在只修 pm2、却让存量 tmux server 或直连后端继续带毒的缺口。 - 三个进程边界覆盖 stale dump。
pm2Env、daemon boot、worker boot 都调用 scrub;即便绕过 botmux CLI、由 raw pm2/dump.pm2 恢复,也会在真正 fork worker/CLI 前再次清洗。 CLAUDE_EFFORT的层级拆分正确。 只在进程边界去掉 ambient 泄漏,不进入 child redact / pane unset / server scrub;per-botenv在 redact 后注入,tmux/zellij shell profile 也仍可生效。身份键则被 per-bot 配置拒绝。- 没有误扫
CLAUDE_CODE_*前缀。 gateway/provider 的CLAUDE_CODE_SUBAGENT_MODEL、CLAUDE_CODE_MAX_CONTEXT_TOKENS、CLAUDE_CODE_ATTRIBUTION_HEADER等合法旋钮不受影响;这是公共层改动中很重要的兼容性边界。
横向影响复核
- CLI: 公共 child-env 路径会影响全部 CLI,但只删除 Claude 会话身份键;其它 20+ CLI 的 provider/proxy/feature env 不受影响。
- 后端: PTY 走 direct redact;tmux/tmux-pipe 走 client strip + wrapper + server scrub;zellij 复用同一 wrapper;Herdr/Riff 从已清洁的 worker/child env 构造。均闭合。
- 会话类型: fresh spawn、tmux server 重生、daemon/worker restore、role-switch/respawn 都受益;adopt/reattach 的已运行 pane 不会被 retroactively 改写,但 server scrub 会保证后续新 pane 干净,符合现有安全语义。
- 平台:
delete env[key]的进程边界和 PTY redaction跨平台;tmux/zellij 自愈只作用于对应 Unix 后端,没有引入新的路径/shell/编码假设。 - sandbox: 外层 bwrap/Seatbelt 仍从清洁的
childEnv启动;per-bot 注入顺序未被改变。
独立验证
在 PR head + 远端当前 master 的真实三方合成树(0 冲突)上运行:
pnpm build
PASS
pnpm exec vitest run --project unit \
test/child-env.test.ts \
test/per-bot-env.test.ts \
test/tmux-env-isolation.test.ts \
test/tmux-backend-env.test.ts \
test/herdr-backend.test.ts
5 files passed, 137/137 tests passed
git diff --check 通过;合成树相对当前 master 仍严格是本 PR 的 8 个文件、195+/10-,没有合并时丢逻辑。
非阻塞维护提醒:身份键采用显式枚举是对的,后续 Claude Code 若新增会影响 fresh session 的身份标记,应更新该集合,不应改成前缀清扫。
Approve 仅代表代码复审通过;未执行合码、发版或 live 部署,继续等待申晗明确拍板。
已合码 ✅ (admin-merge,申晗授权)合并 commit: 合码前复核(master 已前移)我首审时的 trial-merge base 是
fork PR 无 CI,故走 状态
|
问题
Claude Code 会给它的每个 Bash 子进程注入一组会话身份环境变量(
CLAUDECODE、CLAUDE_CODE_CHILD_SESSION、CLAUDE_CODE_SESSION_ID、CLAUDE_CODE_ENTRYPOINT、CLAUDE_CODE_EXECPATH、CLAUDE_PID)。从 bot 会话内部发起pm2 delete+start/botmux restart时,pm2 会把发起 shell 的环境快照进存档并注入所有受管 app——这些标记从此常驻 daemon。后续链路(2026-07-27 实机事故,dev-beta 机器):
CLAUDE_CODE_CHILD_SESSION=1,自认嵌套子会话,转写落盘静默关闭(状态栏黄条 "Transcript saving is off — inherited CLAUDE_CODE_CHILD_SESSION marker");claude --resume <session-id>,转写缺失意味着 respawn 后上下文静默丢光。现有防线只覆盖了
CLAUDECODE一个键,其余标记全部穿透。修复(单一规则源
child-env.ts)CLAUDE_SESSION_MARKER_ENV_KEYS(6 个身份标记):REDACTED_CHILD_ENV_KEYS→ 自动贯通 pty redact、pane wrapper unset、tmux client strip 三层既有防线;TMUX_SERVER_GLOBAL_SCRUB_KEYS→ daemon 启动时的scrubTmuxServerGlobalEnv会自愈已带毒的运行中 server(无需等 server 重生);scrubClaudeSessionMarkerEnv(),在与scrubSessionCliHomeEnv相同的三个进程边界调用(cli.ts pm2Env/index-daemon.tsboot /worker.tsboot),涵盖 stale dump.pm2 resurrect 路径;CLAUDE_EFFORT作为 boundary-only 键:继承穿透 pm2→daemon→worker 的值只可能是发起会话的 effort(会静默固化到所有 bot),在三边界清除;但不进 pane unset / server scrub,保留 per-botenv(所有 backend 含 PTY)与 pane shell profile(tmux/zellij)两条合法配置通道;CLAUDECODE同待遇);CLAUDE_EFFORT仍可 per-bot 配置。测试
codex 两轮定向 review:第一轮抓出
CLAUDE_EFFORT泄漏残留(已修);第二轮确认泄漏链闭合、injectEnv 合并顺序正确、as const消费方无破坏,无 payload。🤖 Generated with Claude Code