Skip to content

fix(env): 阻断 Claude 会话身份标记经 pm2/tmux 泄漏进 bot CLI(转写静默停写) - #625

Merged
deepcoldy merged 1 commit into
deepcoldy:masterfrom
xu4wang:fix/claude-session-marker-leak
Jul 28, 2026
Merged

fix(env): 阻断 Claude 会话身份标记经 pm2/tmux 泄漏进 bot CLI(转写静默停写)#625
deepcoldy merged 1 commit into
deepcoldy:masterfrom
xu4wang:fix/claude-session-marker-leak

Conversation

@xu4wang

@xu4wang xu4wang commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

问题

Claude Code 会给它的每个 Bash 子进程注入一组会话身份环境变量(CLAUDECODECLAUDE_CODE_CHILD_SESSIONCLAUDE_CODE_SESSION_IDCLAUDE_CODE_ENTRYPOINTCLAUDE_CODE_EXECPATHCLAUDE_PID)。从 bot 会话内部发起 pm2 delete+start / botmux restart 时,pm2 会把发起 shell 的环境快照进存档并注入所有受管 app——这些标记从此常驻 daemon。

后续链路(2026-07-27 实机事故,dev-beta 机器):

  1. 共享 tmux server 随最后一个 bmx-* 会话关闭自动退出;
  2. 下一条消息触发 spawn,worker 从带毒 daemon fork 出新 tmux server,标记进入 server 全局环境
  3. 此后所有 bot CLI 继承 CLAUDE_CODE_CHILD_SESSION=1,自认嵌套子会话,转写落盘静默关闭(状态栏黄条 "Transcript saving is off — inherited CLAUDE_CODE_CHILD_SESSION marker");
  4. botmux 的会话续命与切角色都依赖 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.ts boot / worker.ts boot),涵盖 stale dump.pm2 resurrect 路径;
  • CLAUDE_EFFORT 作为 boundary-only 键:继承穿透 pm2→daemon→worker 的值只可能是发起会话的 effort(会静默固化到所有 bot),在三边界清除;但进 pane unset / server scrub,保留 per-bot env(所有 backend 含 PTY)与 pane shell profile(tmux/zellij)两条合法配置通道;
  • per-bot env 拒绝显式配置身份标记(与 CLAUDECODE 同待遇);CLAUDE_EFFORT 仍可 per-bot 配置。

测试

test/child-env.test.ts        13 passed
test/tmux-env-isolation.test.ts  (真 tmux server 场景:带毒 global env → scrub → 存量 pane 不动、新 pane 干净)
test/per-bot-env.test.ts
共 35 passed;另跑 herdr-backend / tmux-backend-env 共 102 passed,零回归

codex 两轮定向 review:第一轮抓出 CLAUDE_EFFORT 泄漏残留(已修);第二轮确认泄漏链闭合、injectEnv 合并顺序正确、as const 消费方无破坏,无 payload。

🤖 Generated with Claude Code

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>
@xu4wang
xu4wang requested a review from deepcoldy as a code owner July 27, 2026 15:12

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

首审结论:✅ 无阻塞(Approve pending codex 复审 + 申晗拍板)

由 Claude 做首次 review。在隔离 worktree 钉住 head SHA b7a123c1 拉起,读全量改动 + 对拍当前 master + 本地 build/test(fork PR 无 CI)。未合码——按分工等 codex 复审、无申晗确认前不合。

这个 PR 在解决什么(白话)

Claude Code 会给它跑的每个子进程注一组"会话身份"环境变量:CLAUDECODECLAUDE_CODE_CHILD_SESSIONCLAUDE_CODE_SESSION_IDCLAUDE_CODE_ENTRYPOINTCLAUDE_CODE_EXECPATHCLAUDE_PID。其中最致命的是 CLAUDE_CODE_CHILD_SESSION=1:任何 claude CLI 看到它,就认为自己是"嵌套子会话",于是静默关掉转写落盘(transcript)。而 botmux 的会话续命(tmux 挂了 respawn、切角色)全靠 claude --resume <session-id>——转写没了,respawn 后上下文就悄无声息地全丢

泄漏路径(2026-07-27 dev-beta 实机事故):

  1. 从某个 bot 会话内部跑了 pm2 restart / botmux restart
  2. pm2 会把"发起这条命令的 shell 的环境"快照进它管理的所有 app(含 daemon);
  3. 共享 tmux server 随最后一个 bmx-* 会话关闭而自动退出;
  4. 下一条消息触发 spawn,worker 从带毒的 daemon fork 出一个新 tmux server,这些标记就进了 server 的全局环境表
  5. 此后全机所有 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 redactpane wrapper unset(tmux/zellij 共用的 SHELL_WRAPPER_SCRIPT)、tmux client strip
    • TMUX_SERVER_GLOBAL_SCRUB_KEYS → daemon 启动时的 scrubTmuxServerGlobalEnv自愈一个已经带毒、仍在运行的 tmux server,不必等它自然消亡;
  • 新增 scrubClaudeSessionMarkerEnv(),在与 scrubSessionCliHomeEnv 完全相同的三个进程边界调用(cli.tspm2Env / index-daemon.ts boot / worker.ts boot),覆盖 stale dump.pm2 resurrect 路径;
  • CLAUDE_EFFORT 作为 boundary-only 键:在三边界清除(继承穿透 pm2→daemon→worker 的值只可能是发起会话自己的 effort,会静默固化到所有 bot),但进 pane unset / server scrub——保留两条合法配置通道:per-bot env(所有 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_MODELCLAUDE_CODE_MAX_CONTEXT_TOKENSCLAUDE_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 部署各需申晗另行授权。

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex 复审结论:✅ Approve(无阻塞)

独立复核了 PR head b7a123c1 的完整 diff、Claude Code 实际判定逻辑、环境构造调用链、各后端/会话类型影响,以及与远端当前 master=fdb105a8 的三方合成树。结论与首审一致:修复必要,边界选择正确,未发现需要改代码的 blocker。

关键正确性

  1. 根因与修复点吻合。 本机 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 边界主动清除。
  2. 规则源与三层防线闭合。 六个身份键集中在 CLAUDE_SESSION_MARKER_ENV_KEYS,同时驱动 direct/PTY redactChildEnv、tmux client strip、tmux/zellij pane wrapper unset,并进入 server-global scrub;不存在只修 pm2、却让存量 tmux server 或直连后端继续带毒的缺口。
  3. 三个进程边界覆盖 stale dump。 pm2Env、daemon boot、worker boot 都调用 scrub;即便绕过 botmux CLI、由 raw pm2/dump.pm2 恢复,也会在真正 fork worker/CLI 前再次清洗。
  4. CLAUDE_EFFORT 的层级拆分正确。 只在进程边界去掉 ambient 泄漏,不进入 child redact / pane unset / server scrub;per-bot env 在 redact 后注入,tmux/zellij shell profile 也仍可生效。身份键则被 per-bot 配置拒绝。
  5. 没有误扫 CLAUDE_CODE_* 前缀。 gateway/provider 的 CLAUDE_CODE_SUBAGENT_MODELCLAUDE_CODE_MAX_CONTEXT_TOKENSCLAUDE_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 部署,继续等待申晗明确拍板。

@deepcoldy
deepcoldy merged commit 4131c0c into deepcoldy:master Jul 28, 2026
@deepcoldy

Copy link
Copy Markdown
Owner

已合码 ✅ (admin-merge,申晗授权)

合并 commit:4131c0c7,master b200c277 → 4131c0c7

合码前复核(master 已前移)

我首审时的 trial-merge base 是 fdb105a8;合码前 master 已前移到 b200c277(期间并入 #623 / #622 / #627 / #605,其中 #605 也改了 worker.ts 的 authPaths / codex-redirect 逻辑)。按规矩对当前 master 重新验证:

  • 隔离 worktree 钉 head b7a123c1,trial-merge 当前 master b200c2770 冲突,PR 逻辑完整保留(三边界 scrubClaudeSessionMarkerEnv 调用 2/2/2、CLAUDE_SESSION_MARKER_ENV_KEYS 6 处引用均在);
  • 我的 worker.ts boot 顶部清洗(line 287)fix(sandbox): 重定向 Codex 不再暴露宿主 ~/.codex + 修复 10 个非 hermetic 测试 #605sandbox authPaths 改动(line 6767+) 位置正交,无语义冲突;
  • 合成树 pnpm build 通过;
  • env + backend 定向套件 137/137 passed(child-env / per-bot-env / tmux-env-isolation / tmux-backend-env / herdr-backend);
  • 合码时 PR 仍 APPROVED、head 仍 b7a123c1(无人在我 review 之后推新 commit)。

fork PR 无 CI,故走 --admin 绕过分支保护的 CI 门。

状态

  • 发版(tag)live 部署 尚未执行,各需单独授权。

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.

2 participants