Skip to content

fix(claude-goal-mode): bake a launcher that actually runs Python into the Claude adapter - #6229

Merged
huangruiteng merged 2 commits into
loopx-project:mainfrom
JasonBuildAI:codex/claude-installer-python-launcher-m2p9
Oct 11, 2026
Merged

huangruiteng merged 2 commits into
loopx-project:mainfrom
JasonBuildAI:codex/claude-installer-python-launcher-m2p9

Conversation

@JasonBuildAI

@JasonBuildAI JasonBuildAI commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Goal And Delivered Outcome

  • Outcome basis / optional anchor: follow-up to the merged fix(claude-goal-mode): pin the Claude Code goal-mode entry stdio to UTF-8 #6166, whose slice boundary named this residual: "install.py / hooks.json bake a bare python3 command, which on Windows can resolve to the 0-byte WindowsApps alias".
  • Goal/source and gap: the Claude Code adapter installer baked the shutil.which("python3") PATH lookup into the generated PreToolUse hook, statusline and /loopx command without ever running it. On a Windows host where that name resolves to the 0-byte WindowsApps App Execution Alias, every baked command never starts Python - it exits non-zero (9009 here) or raises OSError depending on the host, writing nothing either way: the optional --harden should_run gate fails OPEN (Claude Code sees a decision-less, non-zero hook exit and runs the tool anyway), the statusline renders nothing, and /loopx never runs. The installer now bakes only an interpreter it has confirmed can launch Python.
  • Observable before -> after, with the validation row that proves it: on the affected host the exact installed PreToolUse command never reached the gate at the base commit (non-zero exit, empty stdout); at this revision it exits 0 with its {} no-op decision (real_entrypoint row).
  • Issue/task and intended base: self-contained reproduced defect, so no separate issue (per CONTRIBUTING); intended base main. Related to fix(claude-goal-mode): pin the Claude Code goal-mode entry stdio to UTF-8 #6166.

Author Declaration

  • Written by: OpenAI Codex agent - drafted with AI assistance at the requester's direction, under human-set constraints and review.

Implemented against

Criterion (spec clause) Disposition Symbol / path Test or command
Baked interpreter must be able to launch Python implemented _launches_python / _python_cmd (loopx/claude_goal_mode/scripts/install.py) pytest tests/test_claude_goal_install_python_launcher.py
Preserve the python3 -> python -> sys.executable preference where python3 runs implemented _python_cmd test_python_cmd_keeps_the_python3_preference_when_it_runs
The installed hook command actually runs implemented hardening_block / add_hardening test_installed_gate_command_actually_runs_python
  • Self-check before submission: ran the new tests before/after, the stdio regression suite, installer smokes, architecture guards, ruff, and the diff-scoped canary premerge; every failing smoke in that run fails identically at the base commit on this host. An independent review was then performed on revision 66d3ea43e; its findings are addressed in 5673f99e9 (preference-order and non-zero-exit coverage, probe timeout, host-agnostic wording). Deliberately left out: quoting for interpreter paths that contain spaces and the static plugin manifests (see Scope).

Scope And Continuation

  • Completed scope and remaining work: complete within this scope - the three installer-baked artifacts (hook, statusline, /loopx command) can no longer receive a launcher that fails to start Python.
  • Slice boundary / successor: two named residuals, neither a regression and neither used by a documented install path today:
    • an interpreter path containing spaces is still interpolated unquoted into the hook/statusline command strings and the allowed-tools rule, so a confirmed-runnable interpreter in a spaced directory reproduces the same silent-dead-command symptom; that quoting / permission-pattern concern needs its own change and validation;
    • the static plugin manifests (.claude-plugin/plugin.json, hooks/hooks.json, settings.example.json, commands/loopx.md) still bake a bare python3; the README documents only install.py and there is no marketplace.json, so no documented install path reaches them.

Validation

  • Tested revision: 5673f99
  • Run state: finished
  • Input classes: synthetic
Check kind Result Public-safe evidence / limitation
regression_parity passed tests/test_claude_goal_install_python_launcher.py: 8 failed against the base revision (copied into the base worktree as an untracked file), 8 passed at this revision.
unit passed pytest tests/test_claude_goal_install_python_launcher.py tests/test_claude_goal_mode_stdio_utf8.py -> 13 passed; with tests/architecture/test_source_session_registry_denial.py -> 15 passed.
real_entrypoint passed On a Windows host where shutil.which("python3") is the 0-byte WindowsApps alias: ran install.py --scope project --harden --skip-mcp, then executed the installed PreToolUse command through a shell with an event on stdin. Base: non-zero exit, empty stdout. This revision: rc 0, stdout {}.
static passed ruff check tests/test_claude_goal_install_python_launcher.py loopx/claude_goal_mode/scripts/install.py.
integration passed python examples/claude-install-no-system-mutation-smoke.py; pytest tests/architecture/test_project_registry_io_census.py tests/architecture/test_top_level_module_budget.py tests/architecture/test_control_plane_import_boundaries.py -> passed.
integration failed pytest tests/test_claude_goal_release_qualification.py -> 14 passed, 3 failed; the three real_* tests abort on this host's Node probe (requires Node 22.22.3) and fail identically at the base commit.
manual failed loopx canary premerge --from-git-diff --git-diff-base upstream/main -> diff checks, changed-file compile, and public/private boundary passed; the selected install-family smokes (install-local-smoke, install-local-overwrite-smoke, codex-cli-packaged-install-smoke, loopx-update-smoke, semantic-vocabulary-drift-smoke, claude-install-optin-smoke, two node-probe smokes) fail identically at the base commit on this host (no symlink privilege, older Node, missing npm deps).
  • Coverage and gaps: the changed paths are covered by the new tests - a simulated dead alias (portable to any host) plus the really installed command - and both rejection branches of the probe (launch failure and non-zero exit). CI has no Windows runner, so the simulated tests are what guards the regression there. Untested: an actual App Execution Alias cannot be created in CI without host privileges; the spaced-path and static-manifest residuals above.

Frontend / Visual Evidence

  • UI impact: none

Type of Change

  • Bug fix

LoopX Area

  • Host or runtime integration

Technical Direction

  • Direction / acceptance reference, when applicable: N/A

Shared-authority RFC fixture impact

  • N/A

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths.
  • I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.
  • I kept the change scoped to the linked issue/task.
  • I completed the visual evidence section for UI changes, or marked UI impact none.
  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

… the Claude adapter

The Claude Code adapter baked a bare `python3` PATH lookup into the PreToolUse
hook, the statusline and the `/loopx` command. On Windows that name can resolve
to the 0-byte `WindowsApps` App Execution Alias, which exits 9009 writing
nothing, so the optional `--harden` gate silently failed OPEN, the statusline
rendered nothing, and `/loopx` never ran. PR loopx-project#6166 named this residual.

`_python_cmd()` now bakes only an interpreter it has confirmed can launch
Python: the preference order (`python3` -> `python` -> `sys.executable`) and
POSIX behavior are unchanged.

Verified on a zh-CN Windows host where `shutil.which("python3")` is that alias
(0 bytes, rc 9009): the exact installed hook command now exits 0 with its `{}`
no-op decision, where the base commit exited 9009 with empty stdout. The new
tests fail 4/4 before the change and pass after.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
JasonBuildAI added a commit to JasonBuildAI/loopx that referenced this pull request Oct 11, 2026
…ro exits

Follow-up to the independent review of this PR: the tests now pin the
`python3` -> `python` preference and both rejection branches of the interpreter
probe (non-zero exit and launch failure), and the probe bounds a candidate
launch with a timeout so a hanging launcher falls through instead of stalling
the installer. Docstrings no longer assume a single host shape - the Windows
alias exits 9009 on some hosts and raises OSError on others.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer: model_agent | gpt-6.1-sol | OpenAI | runtime_reported | xhigh

精确 head:6229@5673f99e92a3b778dce9aa447d0334e6ce0dafef;不可变原始基线:43643a5e5dd25a05382c232824ebeb806ce45e25。独立审查当前完整两文件 diff 和实际生成入口;未查询或等待 CI。

动机

明确安装 Claude goal-mode 并使用 hook、状态栏或 /loopx 的使用者。

当 PATH 的 python3 是非零退出或无法启动的 alias 时,旧安装器仍写入它,三个入口无结果,工具 gate 无法执行;当前跳过失效候选,生成的三个入口能够运行。

实际 project 安装后执行生成命令:失效候选下的九个调用从无输出、非零退出恢复;armed Read 返回 allow、受阻 Write 返回 deny,状态栏和 slash status 可读回原 Goal。

本次只修安装器生成命令的 launcher 选择;不改变安装 opt-in、armed、工具权限或命令文字,不宣称完成路径空格引用、静态 plugin manifests、原生 Windows 或 live Claude 验证。

这里的 armed 指项目已由现有 .claude/loop.md 标记启动 Goal 模式;hook 是执行工具前的原有资格检查。修复让这项检查真正启动,不赋予新的工具权限。

改动思路

Python launcher 探测属于现有 Claude adapter 安装器的 provider IO,工具资格和权限继续由原 Goal gate 决定。

在同一安装器验证候选并保留 python3→python→当前解释器顺序,覆盖三个实际生成入口;不扩大到静态 manifest 或新的控制面状态。

shutil.which 只能找到可执行文件,不能证明它能启动 Python。新 helper 用受限时长的实际 -c 调用验证;非零退出、启动错误或超时就检查下一候选。最终 sys.executable 是正在运行安装器的解释器。生成命令继续交给原脚本、原项目范围和原类型化决策 owner,用户可以重新执行原有显式安装来恢复入口。

已检查附近 _has_mcp 和独立 venv provisioning:它们验证 SDK 模块并承担依赖安装权限,不能替代普通 standalone script 的 launcher 选择。此次共享一个小 probe,保留既有 _python_cmd 为三个生成入口的唯一选择 seam,属于与当前修复一致的有界整理;无须新 provider 框架或并行控制面规则。

具体改动

独立规格是实现前已有的 Claude goal-mode README。已合并 #6166 修复直接入口的 UTF-8,并明确把 Windows launcher 残余列为范围外;本次没有把作者新测试当规格。

spec_ref: loopx/claude_goal_mode/README.md

spec_revision: 43643a5e5dd25a05382c232824ebeb806ce45e25

使用规格实际标题:Install (opt-in) implemented:正常安装仍不自动启用 adapter,显式安装保留 project/user 范围。Optional hardening (--harden) implemented:只有显式 harden 写 hook/状态栏,保留已有 permission deny 和无关配置。Use implemented:实际生成的 /loopx status 能执行并读回所选 Goal。新 launcher safeguard 服务这些既有入口,不创建新的激活设置。

完整两文件 +161/-2:安装器新增21行、测试新增140行;无新 flag、schema、持久化事实、prompt 或视觉布局。

关键代码讲解

  • _launches_python:以实际进程运行 [py, "-c", ""],10秒 timeout,只有退出0才接受;OSError/SubprocessError 都拒绝。
  • _python_cmd:依次验证 python3、python,再回退当前解释器;健康 python3 的优先级保留。
  • hardening_block:用同一选择构建原 hook/状态栏;command_md 同样使用它生成 slash 命令及 allowed-tools 路径。后续资格判断与权限代码未变。

同一个独立 harness 在原始基线和 head 各产生30项真实观察:隔离 project 安装、dry-run、非 harden/harden、健康候选、真实非零退出 executable 和缺失候选。实际执行生成的 shell 命令,而不是只检查 builder 字符串;其中9项失效候选入口从 rc9/空输出恢复为 rc0/原资格或状态结果。固定“安装后入口可执行”oracle 在原始基线退出1、当前 head 退出0。

原有配置保留、dry-run 不写文件、非 harden 不安装 hook 均核验。实际 armed Read 返回 allow;刻意不提供 Node 的隔离环境中,受 quota-health 阻止的 Write 返回 deny,状态栏和 slash 返回该原有 blocked 状态。未 armed 和同一父目录中新建的其他项目仍无 gate 结果,没有 FORCE 或 mock 生成授权答案。另以真实进程验证10秒超时拒绝、不可执行格式的 OSError 拒绝和两个候选均失败时的 fallback。

对主干的风险

当前 launcher 修复没有阻塞发现。57项相关 launcher/stdio/release/架构测试、Ruff、5项 direct 检查以及首次 native canary 的16项选定检查中14项通过;完整 semantic smoke 通过。以下失败保留,不能把原 canary 写成全绿。

首次 install-local-smoke.py 和 codex-cli-packaged-install-smoke.py 失败。独立冷源码原始基线/head 对照,均缺少生成的 Chat bundle;合成 archive 中 fallback 又缺 dashboard build 源目录。错误发生在正常安装显式跳过 Claude adapter 的路径,未执行本次 _python_cmd。不是只比较两个失败计数:核验了同一脚本、缺失 manifest 和后续目录错误的完整因果链。

执行原有 scripts/chat_bundle.py build --install 补齐源码构建前提后,两项原脚本在 base/head 都成功完成安装、promotion 和既有 readback。该配对使用 LOOPX_USAGE_PING=0 隔离一次性 fixture 的后台遥测,没有改代码、测试断言或门槛。另一条此前默认遥测开启的诊断已成功完成安装/readback,但临时 home 清理报 OSError66(directory not empty);根据生命周期和 opt-out 对照推测为后台写入竞争,具体 writer 未验证,未宣称修复默认遥测清理。这个 fixture/runtime 观察与 launcher 正确性分开保留,原全局 gate 的红灯仍需其 owner 处理。

语义与 CI 对齐

没有新共享状态词汇、持久化契约或第二个决策源;semantic_alignment=not_applicable 指当前局部 host IO helper,不免除源码 advisory/全树检查。候选探测由机器执行,不能称为可忽略建议。默认安装、hardening 开关、项目 armed 和实际工具权限边界保留。agent 消费的命令正文没有改写义务;仅替换启动解释器。

未测试原生 Windows App Execution Alias 或 live Claude。解释器路径含空格的既有引用问题、静态 plugin manifests 仍在范围外。空 -c 退出0是这个探测的假设,不能证明任意伪装 wrapper 的身份。未执行用户活跃 Goal 写入、模型调用或额外权限授予。没有 first-screen 改动;没有宣称 mock 或合成项目代表实际模型理解。

我的整体评价

APPROVE,goal_achieved 仅指此次生成入口的 launcher 修复;long_horizon=improved、user_experience=improved:重复调用不再永久卡在失效候选,原 scope/拒绝/读回结果真正可达,修正后重新安装可恢复。完整 diff 的规模与问题匹配,附近共享 probe 的有界整理已应用,权限/配置 owner 保留。当前代码判断与全局打包 gate、维护者 merge 授权分别成立;本评审不自合并或 bypass。

Motivation

Users explicitly installing Claude goal-mode need its pre-tool hook, statusline and /loopx to execute. The original installer could bake a PATH python3 alias that exits nonzero or cannot start, leaving all three generated entries silent and the existing tool gate unavailable. The current head rejects that candidate. Nine actual dead-candidate entry calls recover to the original decision or Goal readback. An armed project uses the existing .claude/loop.md marker; the hook checks existing eligibility, rather than granting new permission. Path quoting, static manifests, native Windows and live Claude qualification remain outside this repair.

Approach

Launcher readiness belongs to the existing adapter installer. A bounded actual -c process probe rejects nonzero exit, startup failure and timeout; selection keeps python3, then python, then the interpreter already executing the installer. All three generated entries share the existing selection owner. SDK import checks and venv provisioning have different dependency/authority semantics and cannot substitute for ordinary script readiness. The small shared probe is a proportionate related refactor; no new provider framework, state owner or activation setting is needed. Re-running the existing explicit installation restores generated entries.

Concrete changes

The independent pre-change specification is the immutable README revision above. Its actual Install (opt-in), Optional hardening (--harden) and Use headings are implemented: ordinary LoopX installation leaves the adapter off; explicit scope and separate hardening preserve existing settings and permission denies; the generated status command actually reads its scoped Goal. Merged6166 had explicitly deferred the launcher residual. New author tests do not define acceptance.

The complete two-file diff is +161/-2:21 installer lines and140 test lines. The three linked exact-head symbols show the10-second process probe, unchanged healthy-candidate preference/current-interpreter fallback, and consumption by hook/statusline; command_md also consumes the same selector. No flag, schema, persistent fact, prompt or visual layout is added.

An identical independent harness makes30 original-base/head observations through actual scoped installation and generated shell commands. Nine dead-executable calls move from rc9/empty to rc0/scoped results. The fixed executable-entry oracle fails on the original base and passes on this head. Healthy/missing candidates, dry-run no-write, non-harden isolation, existing settings/permission preservation, armed Read allow, unavailable-quota Write deny and unarmed/new other-project no-op are exercised. No FORCE or mocked postcondition supplies the decisions. Separate real process probes reject a10-second timeout and an executable-format OSError, then fall back when both candidates fail.

Risks to main

No current launcher blocker remains.57 relevant launcher/stdio/release/architecture tests, Ruff,5 direct checks and14 of16 initially selected native checks pass, including the full semantic smoke. The original native canary remains red: both install packaging fixtures fail before Claude adapter execution on a missing generated Chat bundle, with synthetic-archive fallback missing dashboard build source. The same complete causal signatures reproduce on the immutable original base and exact head, rather than being inferred from equal failure counts.

After the documented Chat source build, both unchanged packaging scripts complete install/promotion/readback on base and head with LOOPX_USAGE_PING=0 to isolate disposable-fixture background activity. No source, assertion or hard limit changed. An earlier prepared diagnostic with default telemetry enabled completed installation/readback but failed temp-home cleanup with OSError66. Background-write competition is an inference from lifecycle and opt-out contrast; the exact writer is unverified. This does not certify default-telemetry cleanup as fixed or the original global gate as green. The packaging fixture/runtime owner retains that separate concern.

No shared state vocabulary, authority or persisted contract changed; semantic alignment is not applicable to this local IO helper, while advisory/full-tree checks were still executed. Default installation, explicit hardening and original armed/scope/permission rules retain their contracts. The probe is enforced, and downstream agent instructions retain their obligations. CI was not fetched or waited for. Native Windows aliases, live Claude, interpreter paths containing spaces, static plugin manifests and arbitrary deceptive zero-exit wrappers are not qualified by this evidence.

Overall assessment

APPROVE for the current exact head. This closes the bounded generated-entry launcher repair and improves repeated use and recovery while retaining the original scoped policy owner. The shared probe is proportionate and the related future-facing pass was applied at the existing selection seam. Independent code approval, the disclosed global packaging gate and maintainer integration authority remain separate; no merge or bypass is granted.

English verdict: APPROVE — 5673f99. Full current diff and actual generated-entry/recovery counterfactuals verified. Original unrelated cold packaging failures and earlier cleanup diagnostic retained; prepared paired checks pass. Native Windows/live Claude and default-telemetry cleanup remain unqualified; CI not consulted.

@huangruiteng
huangruiteng merged commit 7a0a49d into loopx-project:main Oct 11, 2026
1 of 4 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.

3 participants