Development record
`ahoy install` can report success while leaving a PATH entry that a later plugin update silently breaks, and the condition that decides which happens is invisible to the operator.
ahoy install can report success while leaving a PATH entry that a later plugin update silently breaks, and the condition that decides which happens is invisible to the operator.
Observed adopting abcd in a managed repository. The install reported clean and install: pinned, and abcd worked. It also emitted a note: no verified release artefact was present in the persistent plugin data directory, so the entry was written as a SYMLINK into the versioned plugin cache directory rather than as an owned copy, and that directory is replaced when the plugin updates, at which point the entry dangles. The remedy given is to "re-run abcd ahoy install from a session whose hooks have provisioned the cache", which is a condition the operator has no way to check, create, or even observe from the outside.
The note is loud, which is right, and the failure is exactly the one this repository had already been bitten by from a different cause — a dangling user-bin entry shadowing everything, filed separately. So the tool knows this state is bad, warns about it, and installs into it anyway.
Needed: prefer fetching and verifying the release artefact at install time when the cache is cold, rather than degrading to a link that is known to break; or, if the fetch is deliberately out of scope for install, refuse the link form and say what to run first, since a warned-about install that dangles later is harder to diagnose than a refusal now. Either way, ahoy should notice on a later run that its own entry has become dangling and offer to repair it, which it currently cannot do because a dangling entry classifies as foreign.
Evidence (2026-09-23, folded in from iss-2609120447482506, closed as a
duplicate of this record): the same install shape observed on a second
machine during an ahoy install onboarding on 2026-09-12. The PATH entry was
written as a symlink to the plugin-root binary because the cache held no
verified artefact; immediately after a plugin update the link still pointed at
the superseded root while the live root had moved on, install_mode read
pinned, and bare ahoy reported zero gaps. Two asks it made, restated here so
they survive the close:
- Surface the pin as a gap while it still works, not only in the one-time
install note: an entry that is a symlink into a plugin root rather than an
owned copy is the state this record describes. The superseded half of that
ask is answered on main by 177a407d (iss-2609161805447092): a pin into an
older vintage now raises the required, resolvable
symlink.supersededgap. A pin into the CURRENT root on a cold cache still raises nothing; thesymlink.legacygap fires only once a verified cache is available. - Provision the cache during install, so the owned copy is written first time — this record's first "Needed" option.
Severity raised to major (2026-09-23). This record was filed minor;
iss-2609120447482506, closed as its duplicate, was major. The close folded that
record's unanswered half in here (a pin into the current root on a cold cache
raises no gap, and install does not provision the cache), so its severity comes
with it: closing a major as a duplicate of a minor would drop the finding below
the release-cut guard without anyone ruling it minor.
The release-cut guard still does not see it: the guard reads the records that
entered open/ since the anchor, and this record sat in open/ at v0.9.0, so
it counts as standing backlog, while iss-2609120447482506 entered after that
anchor. The raised severity keeps the grade honest; it does not put the finding
back in front of this cut's guard.
Grounds
- pursued: we expect a refusal naming a runnable command to be easier to act on than a warned-about link that dangles later, and the recorded path to be sufficient provenance for a dangling link because the record is written only by an install the operator ran and read under the hook's ownership guard; it is shown wrong if operators routinely lack a way to run the one-liner (no network, no curl) while their hooks can provision the cache, or if a recorded path is ever re-occupied by a dangling link abcd did not write
Frontmatter
| id | iss-2609100506263330 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | major |
.abcd/work/issues/resolved/iss-2609100506263330-ahoy-install-can-leave-a-path-entry-a.md
open on GitHub ↗ · commit history ↗
mentions4
- dcn-26092700000000032026-09-27 — iss-2609100506263330 takes the record's option (b): with no verified release artefact in the persistent plugin data directory, `ahoy install` writes no PATH entry and names the install one-liner as the command to run first, rather than writing a symlink into the plugin root that the next plugin update strands (lane implementer drainH, autonomous run A, orchestrator abcd-51). Option (a), fetching the artefact inside install, is not taken: install's documented meaning is local configuration, and adr-38 lets the network answer only a verb whose documented meaning is the fetch; the one fetch-and-verify primitive (`abcd update`'s) also imports ahoy, so reaching it from install would need a second downloader or an import inversion. A symlink into the plugin root that an earlier release wrote is left where it stands on a cold cache, raised as a required `symlink.legacy` gap whatever the cache holds, and recorded so the hooks accept it meanwhile. A dangling link `~/.abcd/path-entry` names (read through the same owned, not-group-or-other-writable guard the hook shims apply) is classified abcd's own and repaired by install or removed by uninstall with its record; an unrecorded dangling link keeps iss-2609100506256636's ruling — no provenance claimed, left untouched by detection and by any run with nothing to write in its place, cleared only when install writes the verified copy there.
- dcn-26092800000000052026-09-28 — iss-2609280932480608 is fixed rather than deferred (ruling by orchestrator abcd-9f, autonomous run A; lane fix2-drainH): iss-2609100506256636's rule, danglingness not provenance, applies past the one entry install acts on, so a gap-driven install removes every abcd-owned dangling `PATH` entry other than its target, with its record, and names each in a note. The removal waits for the target to be a working entry of abcd's own after the step, not merely for the run to have something to write: a cold-cache run that adopts the one-liner's copy writes nothing and still leaves `abcd` answering, while a run that leaves nothing working at the target keeps the dangling entry and names the command to run first. An unowned dangling link is never removed by this step. A dangling link runs nothing — the shell skips it — so the shadow note and the dangling gap no longer say it is what runs or that it shadows later entries. Correction to the 2026-09-27 entry above on iss-2609100506263330: an unrecorded dangling link is not cleared only when install writes the verified copy there, as that entry says, but whenever install writes an entry of its own there — the verified copy, or the `--dev` shim when the plugin binary it rebuilds beside exists; `clearDanglingEntry` clears it ahead of either write, and a run with nothing to write in its place still leaves it untouched.
- iss-2609120447482506PATH entry written as a symlink to the plugin-root binary (no verified artefact in the cache) is fragile by abcd's own note, yet bare 'ahoy' reports zero gaps afterwards. Immediately after a plugin update the symlink still pointed at the superseded root e3696dc while the live root was eda67ccc, and install_mode read 'pinned'. Detection should surface this as a gap (entry points at a non-current plugin root / not an owned copy) rather than only in a one-time install note; better still, provision the cache during install so the owned copy is written first time.
- iss-2609161805447092abcd update calls a pinned install foreign when its symlink points into a superseded plugin-cache version. On this machine ~/.local/bin/abcd is the pin abcd ahoy install wrote on 2026-09-07: a symlink into the plugin cache directory of the v0.7.1 vintage (e3696dc524e3). After the plugin moved on, go run ./cmd/abcd update v0.9.0 --yes refused with 'the entry at ~/.local/bin/abcd is not something abcd owns, and abcd never clobbers a binary it does not own' and offered 'remove or rename the occupant' as the remedy. The link target is a published release binary (its digest is the darwin-arm64 line of v0.7.1's checksums.txt) inside abcd's own cache, so nothing about it is foreign: the classifier in internal/core/ahoy/update_target.go only admits a symlink into the CURRENT plugin root as owned, and a link into an older vintage falls through to foreign before the digest check the verb's help promises ever runs. The refusal's detail and remedy are both wrong for this shape; the right remedy is abcd ahoy install, which the owned-dangling case already names, and the honest detail is that the pin points at a superseded vintage. Found while finishing the v0.9.0 deploy on 2026-09-16.