Skip to content

Make defaults.binaries.* take precedence in spawned terminals (CROW-487)#489

Merged
dgershman merged 1 commit into
mainfrom
feature/crow-487-binaries-path-precedence
Jun 11, 2026
Merged

Make defaults.binaries.* take precedence in spawned terminals (CROW-487)#489
dgershman merged 1 commit into
mainfrom
feature/crow-487-binaries-path-precedence

Conversation

@dgershman

Copy link
Copy Markdown
Collaborator

Closes #487

Summary

  • Scaffolder: build a per-devroot symlink farm at ${devRoot}/.claude/bin/<name> from every defaults.binaries entry whose target is executable. Loop is idempotent, reaps stale symlinks when keys are removed, and skips non-executable / nonexistent targets so a misconfigured path never shadows a working PATH install.
  • TmuxBackend: configure(crowBinDir:) threads the bin dir through; registerTerminal exports CROW_BIN_DIR and seeds the spawned window's PATH with the bin dir in front via tmux new-window -e.
  • Shell wrapper: re-prepends $CROW_BIN_DIR to PATH after sourcing the user's .zshrc / .bashrc so a user export PATH=… can't shadow the symlink farm. Fish / unknown-shell branches inherit the already-seeded PATH from the wrapper's outer scope.

Effect

agent shell> which corveil
/Users/you/devroot/.claude/bin/corveil
agent shell> corveil --version          # the binary you configured in Settings → General

Embedded skills like /query-corveil (shipped inside the corveil binary itself) keep using bare corveil invocations — Crow makes those resolve to the user-configured binary without touching the skill content. Generic in the map: codex, cursor, and any future tools share the same mechanism via #484's existing defaults.binaries schema.

Test plan

  • swift test --filter "ScaffolderBinarySymlinkTests" — 6 tests covering executable targets, non-executable skip, nonexistent skip, stale reaping, non-symlink files left alone, reconfiguration re-points.
  • swift test --filter "TmuxBackendCrowBinDirTests"configure(crowBinDir:) propagation + default empty.
  • Manual: set defaults.binaries.corveil in Settings to an executable; open a Claude Code session; run which corveil → reports ${devRoot}/.claude/bin/corveil; corveil --version matches the configured binary; /query-corveil invokes that binary.
  • Manual: unset defaults.binaries.corveil; relaunch; symlink removed; bare corveil falls back to PATH (or unresolved if not installed).
  • Manual: set defaults.binaries.corveil to a nonexistent path; relaunch; one-line warning in [Scaffolder] log; no broken symlink created.
  • Manual: also set defaults.binaries.codex / defaults.binaries.cursor; both symlinks materialize.

Related

🐦‍⬛ Generated with Crow via Claude Code

…d them first (CROW-487)

Embedded skills like /query-corveil call bare `corveil`, but Crow's
configured path was only consulted at Scaffolder time — once inside an
agent terminal the resolution went straight through PATH. This bridges
the gap without touching the skill content:

- Scaffolder materializes a symlink per defaults.binaries.<name> entry
  at {devRoot}/.claude/bin/<name>. Loop is idempotent and reaps stale
  links when keys are removed or targets become non-executable, so a
  broken pointer can never shadow a working PATH install.
- TmuxBackend.configure(crowBinDir:) threads the bin dir through; every
  registerTerminal call seeds the spawned window with PATH prefixed by
  the bin dir and exports CROW_BIN_DIR for the shell wrapper.
- crow-shell-wrapper.sh re-prepends $CROW_BIN_DIR to PATH AFTER sourcing
  user rc, so a user `export PATH=…` in .zshrc can't shadow the symlink
  farm. The wrapper's outer scope already inherits the seeded PATH for
  fish / unknown-shell branches that don't source rc.

Generic in the map — codex / cursor / future tools share the same
mechanism via #484's existing schema, no per-tool wiring needed.
@dgershman dgershman requested a review from dhilgaertner as a code owner June 11, 2026 21:27
@dgershman dgershman added the crow:merge Crow auto-merge on green label Jun 11, 2026

@dhilgaertner dhilgaertner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code & Security Review

Critical Issues

None.

Security Review

Strengths:

  • Symlink reaper uses attributesOfItem(atPath:) (does not traverse the final link), so only Crow-owned symlinks are removed — a hand-dropped real file in .claude/bin is preserved (covered by nonSymlinkFilesInBinDirAreLeftAlone).
  • Misconfigured / non-executable / nonexistent targets are skipped rather than turned into broken links, so a bad config can never shadow a working PATH install — it just falls through to PATH. This is the right failure mode.
  • No secrets, no injection surface: targets are validated with isExecutableFile and linked verbatim; CROW_BIN_DIR is exported through tmux -e, not interpolated into a shell string.

Code Quality

  • Precedence is layered correctly: Swift seeds PATH=crowBinDir:resolvedPATH via tmux new-window -e (covers fish / non-rc shells and wrapper-bypassing processes), and the wrapper re-prepends $CROW_BIN_DIR after sourcing user rc — the only insertion point that survives a user export PATH=… in .zshrc. The front-of-PATH case guard prevents unbounded growth on shell re-exec.
  • Idempotent: removeItem + createSymbolicLink gives ln -sf semantics; reconfiguration re-points and stale keys are reaped (both tested).
  • Best-effort error handling (log + swallow) honors the documented contract that this step never fails an otherwise-successful scaffold. All three scaffold(...) call sites pass binaryOverrides consistently; the [:] default keeps the new parameter safe.
  • Strong test coverage: 6 Scaffolder symlink tests (executable / non-executable / nonexistent / stale-reap / non-link-preservation / re-point) + 2 TmuxBackend propagation tests.

Green (consider only — non-blocking):

  • isExecutableFile resolves relative to Crow's CWD while createSymbolicLink(withDestinationPath:) makes the link relative to the bin dir. A relative defaults.binaries value could in theory pass the check yet link to a broken target. In practice values come from the CROW-482 absolute-path picker, and a relative path would almost certainly fail isExecutableFile and be skipped — so no broken link materializes.
  • A configured key colliding with a pre-existing real (non-symlink) file in .claude/bin will replace that file (the no-clobber guarantee only applies to the reap path). Harmless in practice for a Crow-managed dir.

Note

swift build/swift test could not be run in this review checkout — Frameworks/GhosttyKit.xcframework ships no binary artifact here (environment limitation, not a PR defect). Review is based on full static reading; the PR reports both suites pass locally.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Approve — driven by [0 Red, 0 Yellow, 2 Green] findings.


🐦‍⬛ Reviewed by Crow via Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crow:merge Crow auto-merge on green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make defaults.binaries.corveil take precedence in spawned terminals (symlink + PATH prepend) so /query-corveil and friends use the configured binary

2 participants