Skip to content

fix(claude): give spawned CLI a working PATH; prefer configured binary#70

Merged
sam-powers merged 2 commits into
mainfrom
fix/claude-cli-resolution-path
Jul 10, 2026
Merged

fix(claude): give spawned CLI a working PATH; prefer configured binary#70
sam-powers merged 2 commits into
mainfrom
fix/claude-cli-resolution-path

Conversation

@sam-powers

Copy link
Copy Markdown
Owner

What & why

When Quill runs as a packaged macOS .app launched from Finder, launchd hands it a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) that lacks Node. Since claude is a #!/usr/bin/env node script, the spawned process died with env: node: No such file or directory@claude replies and full-document review silently failed for installed users. (It never reproduced under tauri dev from a terminal, which is why it went unnoticed.)

This ports three related backend behavior specs (docs/to-improve/01–03) into the public repo, adapted to this repo's conventions rather than pasted from the fork.

Changes (src-tauri/src/lib.rs)

  • Give the spawned CLI a working PATH. New pure, unit-tested build_child_path assembles the child's PATH — the resolved binary's own dir first (its sibling node lives there for nvm/Homebrew layouts), then the login-shell PATH, the inherited PATH, and well-known fallbacks (nvm newest-first, ~/.local/bin, Homebrew, system dirs) — de-duplicated preserving first occurrence. Wired into spawn_claude_resume via .env("PATH", …) (alongside the existing .env_remove("ANTHROPIC_API_KEY"), untouched).
  • Prefer the user's configured binary. Reordered resolve_claude_binary: whichlogin shell → hardcoded dir scan last. Previously the scan ran before the shell and could pick a stale global install that emits a deprecated request shape and 400s ("thinking.type.enabled" is not supported…). The scan is now a last-resort fallback, never a preemption.
  • Use an interactive login shell. -ilc instead of -lc, because toolchain PATH lines (nvm/fnm/Homebrew/Volta) commonly live in .zshrc/.bashrc, which a non-interactive login shell doesn't source. Binary resolution takes the last non-empty stdout line (banner-proof); the child-PATH read prints $PATH behind a unique sentinel so rc/profile banners can't clobber it. Shell reads are factored into login_shell_stdout so the two callers don't duplicate the invocation.

Testing

  • cargo test — 39 pass, including four new build_child_path tests: includes the binary's own dir; claude dir comes first; de-dups preserving first occurrence; well-known dirs present when login + inherited PATH are both absent (the packaged-.app worst case).
  • cargo clippy -- -D warnings and cargo fmt --check — clean.
  • Frontend suite (255 tests) green via pre-commit hook.
  • Manual, out of automated scope: the real .app-from-Finder spawn path can't be reconstructed in cargo test — verify by building the .app, launching from Finder, and confirming an @claude reply streams instead of env: node: No such file or directory.

Notes

  • No frontend routes affected (Rust-backend-only), so browser testing is not applicable.
  • Does not touch the user's stale claude install — only the preference order.
  • First of a multi-group port of docs/to-improve/ (this PR = Group 1, specs 01–03). The full spec set is vendored here for provenance; Groups 2–5 follow in separate PRs.

🤖 Generated with Claude Code

sam-powers and others added 2 commits July 8, 2026 23:50
A packaged .app launched from Finder inherits launchd's minimal PATH,
which lacks Node — and `claude` is a `#!/usr/bin/env node` script, so the
spawned process died with `env: node: No such file or directory`. Three
related backend fixes in resolve_claude_binary / spawn_claude_resume:

- Set the child's PATH via a new pure, unit-tested `build_child_path`:
  the resolved binary's own dir first, then the login-shell PATH, the
  inherited PATH, and well-known fallbacks (nvm/Homebrew/system),
  de-duplicated preserving first occurrence.
- Reorder resolution so the user's configured CLI wins: which → login
  shell → hardcoded dir scan last. The scan could previously preempt the
  shell and pick a stale global install that 400s on a deprecated shape.
- Read the shell with an interactive login shell (`-ilc`, not `-lc`) so
  toolchain PATH lines in .zshrc/.bashrc are sourced; read the child PATH
  behind a sentinel so rc/profile banners can't clobber it.

Automated verification of the .app-from-Finder path is manual (the
packaged launchd env can't be reconstructed in cargo test); the pure
PATH builder is covered by four unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESi6dnK3Wc1jYRpZN5qfZA
Records the Group 1 implementation plan and vendors the docs/to-improve/
behavior specs (01–12) that drive this multi-group port, so the porting
provenance lives in the repo alongside the changes it produces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESi6dnK3Wc1jYRpZN5qfZA
@sam-powers
sam-powers merged commit 8dd0430 into main Jul 10, 2026
3 checks passed
@sam-powers
sam-powers deleted the fix/claude-cli-resolution-path branch July 10, 2026 12:42
@sam-powers sam-powers mentioned this pull request Jul 10, 2026
sam-powers added a commit that referenced this pull request Jul 10, 2026
Patch release bundling the fixes from PRs #70#74:

- @claude CLI resolution: working PATH + prefer configured binary (#70)
- desktop: menu stale-closure, window-close capability, deep-link
  window surfacing (#71)
- retry failed @claude replies in place (#72)
- re-run cancelled @claude replies; remove broken new-session (#73)
- surface @claude in composer placeholders; widen reading measure (#74)

Also removes the stray docs/to-improve and docs/plans files that landed
via #70 — those are downstream porting notes, not public-repo content.


Claude-Session: https://claude.ai/code/session_01ESi6dnK3Wc1jYRpZN5qfZA

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant