Development record
2026-08-02 — iss-170: `resolvePluginRoot` now canonicalises the executable path with `resolvePath` (EvalSymlinks, falling back to `filepath.Abs`, then the original path, on error) before the ancestor walk, because `os.Executable` reports the path abcd was INVOKED through and `ahoy install` itself pins a PATH symlink at `/usr/local/bin/abcd` -> `<plugin-root>/abcd` — so the walk climbed the symlink's ancestors (`/usr/local/bin`, `/usr/local`, `/usr` — the loop's own guard excludes `/`, so it was never actually a candidate) and never saw `hooks/`, a permanent "plugin root not resolvable" gap on exactly the layout the installer writes, in a normal shell where neither `ABCD_PLUGIN_ROOT` nor `CLAUDE_PLUGIN_ROOT` is set (`CLAUDE_PLUGIN_ROOT` is set by the harness inside hook invocations, which is why every env-driven test — each of which sets it directly via `t.Setenv` — passed over the defect; this codebase has no independent evidence for whether any other context also sets it). The gap is platform-specific: `os.Executable` resolves through `/proc/self/exe` on Linux, already symlink-free, so the defect and this fix both bite only where the OS returns the invoked (unresolved) path — macOS's `execve`-argument implementation, per Go's `os/executable_darwin.go`. Reusing the existing `resolvePath` rather than calling `filepath.EvalSymlinks` inline keeps the fallback chain shared with the symlink-classification path; the `Abs` middle step is CWD-dependent in principle, but `os.Executable` returns an absolute path whenever it reports no error on both shipped platforms, so that step never fires here and no case that worked before can regress. Testing it required a seam: `os.Executable` is not overridable, so a package-level `var osExecutable = os.Executable` was added and the detector substitutes a fake returning the symlink path, with both env candidates blanked via `t.Setenv` so ONLY the fallback runs — watched fail (no plugin root) with the seam in place but the walk unresolved, and pass after.
2026-08-02 — iss-170: resolvePluginRoot now canonicalises the executable path with resolvePath (EvalSymlinks, falling back to filepath.Abs, then the original path, on error) before the ancestor walk, because os.Executable reports the path abcd was INVOKED through and ahoy install itself pins a PATH symlink at /usr/local/bin/abcd -> <plugin-root>/abcd — so the walk climbed the symlink's ancestors (/usr/local/bin, /usr/local, /usr — the loop's own guard excludes /, so it was never actually a candidate) and never saw hooks/, a permanent "plugin root not resolvable" gap on exactly the layout the installer writes, in a normal shell where neither ABCD_PLUGIN_ROOT nor CLAUDE_PLUGIN_ROOT is set (CLAUDE_PLUGIN_ROOT is set by the harness inside hook invocations, which is why every env-driven test — each of which sets it directly via t.Setenv — passed over the defect; this codebase has no independent evidence for whether any other context also sets it). The gap is platform-specific: os.Executable resolves through /proc/self/exe on Linux, already symlink-free, so the defect and this fix both bite only where the OS returns the invoked (unresolved) path — macOS's execve-argument implementation, per Go's os/executable_darwin.go. Reusing the existing resolvePath rather than calling filepath.EvalSymlinks inline keeps the fallback chain shared with the symlink-classification path; the Abs middle step is CWD-dependent in principle, but os.Executable returns an absolute path whenever it reports no error on both shipped platforms, so that step never fires here and no case that worked before can regress. Testing it required a seam: os.Executable is not overridable, so a package-level var osExecutable = os.Executable was added and the detector substitutes a fake returning the symlink path, with both env candidates blanked via t.Setenv so ONLY the fallback runs — watched fail (no plugin root) with the seam in place but the walk unresolved, and pass after.
Frontmatter
| id | dcn-2608020000000002 |
|---|---|
| type | decision |
| date | 2026-08-02 |
.abcd/work/decisions/dcn-2608020000000002-iss-170-resolvepluginroot-now.md
open on GitHub ↗ · commit history ↗
mentions1
- iss-170resolvePluginRoot's ancestor-walk fallback defeats the install layout ahoy itself creates: os.Executable() (internal/core/ahoy/store.go:92) is used without filepath.EvalSymlinks, so when abcd is invoked through the pinned PATH symlink the walk scans the symlink's ancestors (~/.local/bin, ~, /) and never finds hooks/ — a permanent phantom 'plugin root not resolvable' gap on exactly the layout ahoy install writes. Neither env var is set in a normal shell (CLAUDE_PLUGIN_ROOT exists only inside harness hook invocations), so the fallback is the path that matters. Fix is EvalSymlinks before the walk, plus a test invoking through a symlink watched failing first.