Development record
resolvePluginRoot'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.
resolvePluginRoot'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.
Frontmatter
| id | iss-170 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | major |
.abcd/work/issues/resolved/iss-170-resolvepluginroot-s-ancestor-walk.md
open on GitHub ↗ · commit history ↗
mentions3
- dcn-26080200000000022026-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.
- iss-171The PATH-install detector and installer assume one blessed layout: binTarget defaults to /usr/local/bin/abcd (sudo territory abcd cannot assume) and symlink detection only recognises that exact target, so a working ~/.local/bin/abcd symlink install — the field-standard single-user location used by uv/pipx/rustup-class tools — reports symlink.missing, a false gap; the detector is itself running as abcd from PATH while reporting abcd not installed. Letting ahoy install 'fix' it would write a symlink to <plugin-root>/abcd without validating the target exists — a dangling symlink shadowing the working install. Redesign per fix-the-detector and no-sudo: detection scans PATH for abcd, resolves symlinks (EvalSymlinks, same seam as iss-170), classifies dev-shim/owned/foreign; install defaults to ~/.local/bin (create if needed), adopts an existing owned install in place, does system-wide dirs only behind an explicit --bin-dir flag failing loudly when unwritable, never escalates privileges, refuses to create a symlink whose target does not exist; '~/.local/bin not on PATH' becomes its own loud gap with the printed one-line fix (script-first: print the export line, do not auto-patch shell profiles as the first rung).
- spc-35hook-binary-to-persistent-data-dir