Skip to content

fix: find a harness on PATH the way the shell does - #20

Merged
bojieli merged 4 commits into
mainfrom
fix-relative-path-harness-lookup
Aug 22, 2026
Merged

fix: find a harness on PATH the way the shell does#20
bojieli merged 4 commits into
mainfrom
fix-relative-path-harness-lookup

Conversation

@bojieli

@bojieli bojieli commented Aug 22, 2026

Copy link
Copy Markdown
Owner

The bug

Every harness launcher resolves its binary with exec.LookPath and checks err != nil. Since Go 1.19, a binary found through a relative PATH entry comes back with the resolved path and an error satisfying exec.ErrDot, so that check rejects it. reach then prints:

reach: grok is not installed or not in PATH

about a binary the operator can launch by name in the very same shell. That message is false about the one thing it asserts, and it sends someone off reinstalling a harness that was never missing.

The obvious repair does not work either, which is what makes this worth fixing rather than documenting. LookPath stops at the first match, so appending an absolute entry leaves the earlier relative one matching first and failing identically:

PATH=...:../some/bin                      -> ErrDot, "not installed"
PATH=...:../some/bin:/abs/some/bin        -> ErrDot, "not installed"   (still!)

Hit live while testing #12: grok ran fine from zsh, and reach grok insisted it was not installed, through two rounds of PATH edits.

The fix

lookHarnessPath keeps exec.LookPath and drops only the ErrDot refusal. A relative PATH entry is an ordinary thing for a person to keep, and bash runs what it finds there without comment, so reach does too.

Two things are deliberate:

  • The lookup stays exec.LookPath, not a hand-rolled PATH walk. platform_windows.go already documents why: on Windows executability is PATHEXT, not a file mode, and re-implementing that is how a search quietly stops finding .cmd wrappers.
  • The result is resolved to an absolute path, while the working directory is still the one the lookup assumed. reach hands this path to exec and to the seam probe; a path left relative names a different file after any chdir, and "which binary is this, really" is exactly the question those two callers cannot be vague about.

Applied to every harness call site — claude, codex, goose, gemini, crush, kimi, and doctor — and duplicated into harnessprobe for the reason cache.go duplicates the REACH_HOME rule: a probe that resolved a binary differently from the launcher would verify a seam for one binary and hand the operator another.

Tests

Three, covering the reported behaviour rather than the implementation: a relative entry resolves to an absolute path; a relative entry sitting ahead of an absolute one still resolves instead of refusing; and a genuinely absent binary still errors, because the refusal being wrong for relative entries does not make "not installed" wrong in general.

go build, go vet and go test ./... all pass.

Not in scope

internal/fileops/helper.go looks up the go toolchain the same way. It is left alone here: that path is a build-time convenience with a clear failure message, not the user-facing lookup this fixes. Worth a follow-up if you want the policy uniform.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PuwaXDJut6CorENbW1KHPk

bojieli and others added 2 commits August 22, 2026 13:58
Since Go 1.19 exec.LookPath returns a binary found through a relative PATH
entry together with an error, so a caller testing err != nil refuses to run
it. Every harness launcher tested exactly that and reported "not installed
or not in PATH" for a binary the operator could launch by name in the same
shell.

The obvious repair did not work either: LookPath stops at the first match,
so appending an absolute entry left the earlier relative one matching first
and failing identically.

A relative PATH entry is an ordinary thing to keep and bash runs it without
comment, so reach now does too. The lookup stays exec.LookPath rather than a
hand-rolled PATH walk, because on Windows executability is PATHEXT and not a
file mode; only the refusal is dropped. The result is resolved to an absolute
path while the working directory is still the one the lookup assumed, since
this path is handed to exec and to the seam probe and a relative one names a
different file after any chdir.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuwaXDJut6CorENbW1KHPk
nilerr caught the reflex: filepath.Abs only fails when the working directory
cannot be determined, and returning the unresolved relative path with a nil
error there hands the caller exactly the ambiguity this function exists to
remove. There is nothing to resolve a relative entry against in that state,
so it is an error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuwaXDJut6CorENbW1KHPk
@bojieli bojieli mentioned this pull request Aug 22, 2026
4 tasks
bojieli and others added 2 commits August 22, 2026 14:20
grok.go arrived with main after this branch was cut, still calling
exec.LookPath directly. Left alone it would be the one harness that still
refused a binary found through a relative PATH entry — which is the exact
case that surfaced this bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuwaXDJut6CorENbW1KHPk
@bojieli
bojieli merged commit aa2155a into main Aug 22, 2026
22 of 23 checks passed
@bojieli bojieli mentioned this pull request Aug 22, 2026
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