fix(install): refuse to clobber a Codex shim owned by a different install (#553) - #742
Merged
Merged
Conversation
…tall (#553) install.sh --cmd <other-name> unconditionally overwrote the shared ~/.agents/bin/codex shim, repointing every Codex launch on the machine into the new install's drivers/storage with no warning -- printed as a routine "refreshed" no-op. Recurred twice with real installs affected. codex-shim-install.sh now records and checks ownership: a new shim_owner_script_dir reads back which skill dir a shim was generated for, and `install` refuses to repoint a shim owned by a DIFFERENT install unless AGMSG_CODEX_SHIM_FORCE=1 is set, naming the current owner either way. `status` gains a second "owner:" line for the same reason. install.sh's two call sites split by how much authority they carry: --update explicitly names (or resolves) one specific, already-registered install, so it forces the reclaim -- the documented recovery path. A fresh --cmd install gives no such signal and does not force; it now surfaces (rather than silently swallows) a refusal, naming the AGMSG_CODEX_SHIM_FORCE incantation to claim the shim explicitly if that's actually wanted. Also fixes a real, measured regression the ownership check's own status line would otherwise have introduced: install.sh piped `status` straight into `grep -q '^installed:'` under `pipefail`. A second output line let `grep -q` close the pipe (it stops reading the instant it matches) while `status` was still writing that second line, so the write's SIGPIPE flipped the whole pipeline's exit status even though grep DID match. Fixed by capturing status into a variable before grepping it in both call sites. Positive control performed by hand before writing the fix: installed a production copy, then a second, differently-named copy, and confirmed byte-for-byte that the shim's recorded owner switched to the second install. Both new bats tests reproduce this same RED against the unfixed code (confirmed via git stash) before passing GREEN with the fix.
…e of forcing (review) Two issues raised in review, both against codex-shim-install.sh: 1. shim_owner_script_dir extracted the owner via `eval` on a line read back from the shim file. is_agmsg_shim's authenticity check is a grep for one marker string -- it proves nothing about the rest of a file that lives at a local, single-user, normally-writable-by-that-user path and could have been hand-edited afterward. A crafted file carrying the marker plus a line shaped like the real export (e.g. with a command substitution as its "value") turned a read-only `status` call into arbitrary code execution (raised independently by two reviewers). Fixed by dropping eval entirely. `install` now writes a dedicated, never-executed comment line (`# agmsg-shim-owner: <dir>`) alongside the real, executable export line the shim needs at runtime, and shim_owner_script_dir reads only that comment via plain text extraction. No interpretation of file content as code, regardless of what the file contains. 2. The refusal message named the AGMSG_CODEX_SHIM_FORCE=1 escape hatch but not what using it actually does. Confirmed against tonight's own recurring pattern (a printed remedy that doesn't say what state it leaves you in isn't a remedy yet): now states explicitly that forcing repoints every Codex launch through the shim from the current owner to this install. install.sh's fresh-install call site no longer discards the shim's own stderr on refusal, so this full message actually reaches the user during a normal install instead of being silently swallowed. Forced reproduction for the eval removal: reverted shim_owner_script_dir to the eval-based version, confirmed the new "tampered shim cannot execute code" test fails (RED) -- the sentinel file it plants gets created via the tampered export line -- then restored the fix and confirmed the sentinel is never created (GREEN), for both `status` and `install`.
…cing (review) Two issues raised in review, both against the ownership guard added earlier: 1. An existing agmsg shim written before this PR shipped has no `# agmsg-shim-owner:` line at all. shim_owner_script_dir returns empty for it, and the install guard required owner to be non-empty before refusing -- so a shim with unknown, unrecorded ownership was treated as "unowned, safe to take" and silently overwritten by any new, differently-named install. That is #553's own bug, recurring at exactly the migration moment it matters most: the first time a second install runs an installer carrying this fix against a production shim that predates it. Fixed by treating "is an agmsg shim (marker matches) but records no owner" as "legacy owner, unknown" rather than "unowned" -- it now fails closed the same as a foreign-owned shim, distinguishing the two only in the message shown (named owner vs. "predates ownership tracking"). AGMSG_CODEX_SHIM_FORCE=1 still claims it either way. Also, non-blocking but folded in: the owner comment now stores the %q-quoted path (matching the executable export line's own quoting) rather than the raw path, and every reader compares/displays the quoted form -- a path containing a literal newline could otherwise have forged a second, fake comment line of its own. 2. install.sh's --update block forced the shim reclaim unconditionally, including for a bare `--update` with no `--cmd`. That resolves its target by scanning for a single existing install rather than the caller naming one, and on this base (integration/remote) that scan does not fail closed when more than one install is present -- the fail-closed fix for that (#599) is PR #659, still pending against main and not merged here. Forcing unconditionally meant whichever install a glob happened to resolve to could steal the shim from another install the caller never named at all, compounding #599 with a #553-shaped consequence. Fixed by capturing whether --cmd was passed explicitly (before CMD_NAME gets defaulted or resolved to anything else) and forcing only then. Bare `--update` still refreshes a shim this same install already owns (the common single-install case, unaffected), but no longer reaches past one it doesn't. Forced reproduction for both: reverted each fix in turn, confirmed the matching test fails (RED) against the reverted code -- a crafted legacy shim gets silently claimed in the first case, an unrelated install's shim gets silently stolen by an ambiguously-resolved bare --update in the second -- then restored the fix and confirmed both pass (GREEN).
… (review) The previous two fixes -- failing closed on an owner-unknown (legacy, pre- this-feature) shim, and no longer forcing bare `--update` -- are each correct alone but combined to block the single-install upgrade path they were never meant to touch. Nearly every real machine's existing shim predates ownership tracking and has no owner comment; a routine `install.sh --update` with no `--cmd` (how a single-install user normally upgrades) could no longer refresh it at all, leaving a stale shim (possibly still pointing at a pre-move path) in place indefinitely. Adds agmsg_only_one_install: true iff exactly one `.agmsg`-marked directory exists under ~/.agents/skills, derived directly from this machine's actual state rather than assumed from whether #599's fail-closed multi-install handling (PR #659) is merged wherever this runs -- which it may not be. An owner-unknown shim is now claimable without --cmd/--force specifically when this holds: with only one agmsg install anywhere, nothing else could have written the shim, so there is no one to take it from. With two or more installs present the check returns false and the existing fail-closed behavior is unchanged. Forced reproduction: reverted the allowance, confirmed the new "bare --update migrates this machine's own pre-#553 shim" test fails (RED) -- the legacy shim is left stale, still pointing at its old path -- then restored the fix and confirmed it migrates to the new owner-tracked format (GREEN).
…all (review) Non-blocking review note: agmsg_only_one_install's ~/.agents/skills scan answers the same question #659 (still open against main, unmerged) will independently need for its own bare-`--update` candidate enumeration -- the third time tonight two implementations have answered the same question separately (after #722 -> #733, #741's config validation). Splits the scan into agmsg_install_candidates (lists candidate install dirs, one per line) with agmsg_only_one_install now just counting its output. This doesn't unify the two implementations -- #659 doesn't exist on this base to point at yet -- but gives it something to call instead of re-scanning ~/.agents/skills a second time once it lands on a shared base with this file. Behavior unchanged; tests unchanged and still 73/73.
…directly (review)
Review: "behavior unchanged" after splitting agmsg_only_one_install into a
listing half (agmsg_install_candidates) and a counting half is a claim,
not something 73 pre-existing, unrelated tests already prove -- none of
them exercise the split's own boundary. In particular, agmsg_only_one_
install now counts lines from agmsg_install_candidates' output, so a
candidate name containing a space is a real risk this split introduced:
if the listing ever word-split instead of emitting one line per
directory, a single real install would silently miscount as more than
one and a routine upgrade would wrongly fail closed.
Adds three direct unit tests (source the script, call the functions):
zero installs, exactly one install with a space in its name, and two
installs alongside an unmarked sibling directory that must not be
counted. All three source codex-shim-install.sh in a subshell and read
its output through a '[end]' marker rather than parsing raw text, which
uncovered its own bug while writing these: a GNU sed '1,/re/' range does
not close on line 1 even when line 1 matches, so the zero-installs case
(where '[end]' IS line 1) silently kept reading past it. Replaced with
an awk '/end/{exit}{print}' extraction, which is correct regardless of
where the marker falls.
76/76 pass (test_codex_shim.bats + test_install.bats).
…ists (review) Review (independently verified against the actual code): agmsg_only_one_ install decided "only one install" by counting REAL .agmsg markers on disk, but install.sh's fresh --cmd path checks/refreshes the Codex shim before it touches this install's own marker. So during a fresh, second, differently-named install, this install's own marker genuinely does not exist yet at the moment the check runs -- the count then only sees the FIRST install's marker, concludes "only one install exists anywhere", and lets the second install silently claim a legacy (owner-unknown) shim without --force. That is #553's own bug, reappearing specifically for the single-legacy-shim case the previous round's fix was meant to keep working. Fixed by counting OTHER installs relative to self (SCRIPT_DIR's own skill root, recovered by stripping the fixed .../scripts/drivers/types/ codex suffix install.sh always lays this script out under) rather than a plain total. Self counts as a candidate whether or not its own marker is written yet -- but only when self's recovered skill root structurally sits under ~/.agents/skills; otherwise (e.g. running this script directly against a repo checkout, as several of this file's own tests do) self isn't trusted as an implicit member, and the check falls back to the previous exact-one-real-marker behavior. Forced reproduction: added "a second, differently-named FRESH install does NOT silently claim a pre-existing legacy shim" (test_install.bats) and confirmed it fails (RED) against the unfixed self_root-unaware version -- the second install's shim step reads the first install's lone marker, agmsg_only_one_install returns true, and the shim silently repoints. Restored the fix, confirmed GREEN. Also adds direct unit coverage for agmsg_only_one_install's boundary (test_codex_shim.bats): self invisible with zero/one other real install, self already registered with a space in its name, and an unmarked sibling correctly excluded -- each constructs a realistic $0 via bash -c's argv0 so SCRIPT_DIR resolves the same way it does in a real invocation. Also fixes the enforced-assertions gate this round's own additions pushed 18 over baseline (616): non-last `[[ ]]`/`! cmd` checks in bats tests can't fail the suite on macOS bash 3.2 (#670) -- converted to `grep -qF`/`refute` (both plain commands, always enforced) or left as `[ ]`, which was never flagged. No behavior change; back to exactly 616. 78/78 pass (test_codex_shim.bats + test_install.bats).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
install.sh --cmd <other-name>unconditionally overwrote the shared~/.agents/bin/codexshim, repointing every Codex launch on the machine (through the shim) into the new install's drivers/storage — silently, printed as a routine "refreshed Codex monitor shim" no-op. Recurred twice with real installs affected (issue comment has the second occurrence's measured blast radius).codex-shim-install.shnow records and checks ownership:shim_owner_script_dirreads back which skill dir a shim was generated for.installrefuses to repoint a shim owned by a different install (or one whose ownership isn't recorded at all — see the legacy-shim fix below) unlessAGMSG_CODEX_SHIM_FORCE=1is set, naming the current owner either way (never a bare "no").statusgains a secondowner:line for the same reason.install.sh's two call sites split by how much authority they carry:--update --cmd <name>explicitly names one specific, already-registered install — that's the documented recovery path (re-run it against the production name to repoint the shim back), so it forces the reclaim. (Bare--updatewith no--cmddoes not force — see the scoping fix below.)--cmdinstall gives no such signal and does not force. It now surfaces (rather than silently swallows) a refusal, naming the exactAGMSG_CODEX_SHIM_FORCE=1 ... installcommand to run if reclaiming is actually wanted.Also fixed: a real regression the ownership check's own output would have introduced
install.shpipedcodex-shim-install.sh statusstraight intogrep -q '^installed:'under this script'sset -o pipefail. Adding the secondowner:line broke that:grep -qstops reading (and closes its end of the pipe) the instant it matches the first line, andstatus's ownechoof the second line then hits a reader that's already gone — SIGPIPE, a nonzero exit for that pipeline stage — whichpipefailreports as the whole pipeline failing even though grep did match. Measured directly while testing this PR, not theoretical: the shim-refresh step silently stopped firing on--updateoncestatusgrew a second line.Fixed by capturing
status's output into a variable before grepping it, in bothinstall.shcall sites. A one-linestatus(as it was before this PR) never hits this, so the hazard was specifically introduced by this PR's ownowner:line — worth calling out since it would otherwise have shipped as a silent, unrelated-looking breakage of the existing--updateshim-refresh path.Fixed in review:
evalon shim-derived content was a code-execution pathRaised independently by two reviewers:
shim_owner_script_dirextracted the recorded owner viaevalon a line read back from the shim file.is_agmsg_shim's authenticity check is agrepfor one marker string — it proves nothing about the rest of a file at a local, normally-writable-by-that-user path that could have been hand-edited afterward. A file carrying the marker plus a line shaped like the realexport ...=line, with a command substitution as its "value", turned a read-onlystatuscall into arbitrary code execution.Fixed by dropping
evalentirely.installnow writes a dedicated, never-executed comment line (# agmsg-shim-owner: <dir>) alongside the real, executableexportline the shim still needs at runtime for its own purposes, andshim_owner_script_dirreads only that comment via plain text extraction (sed) — no code path interprets file content as shell, regardless of what the file contains. Forced reproduction: reverted to theevalversion, confirmed the new "tampered shim cannot execute code" test fails (RED) — the sentinel file it plants via a crafted command substitution actually gets created — then restored the fix and confirmed the sentinel is never created (GREEN) for bothstatusandinstall.Also strengthened per review: the refusal message named
AGMSG_CODEX_SHIM_FORCE=1but not what using it does. It now states explicitly that forcing repoints every Codex launch through the shim from the current owner to this install — andinstall.sh's fresh-install call site no longer discards the shim's own stderr on refusal, so this full message actually reaches the user during a normal run instead of being silently swallowed.Fixed in review: an existing (pre-this-PR) shim looked "unowned" and got silently clobbered anyway
Raised in review: an agmsg shim written before this PR shipped has no
# agmsg-shim-owner:line at all —shim_owner_script_dirreturns empty for it, and the install guard requiredownerto be non-empty before refusing. So a shim with genuinely unknown, unrecorded ownership was treated as "unowned, safe to take" and silently overwritten by a new, differently-named install. That is #553's own bug, recurring at exactly the migration moment it matters most: the first time a second install runs an installer carrying this fix, against a production shim that predates it — i.e. most real machines, the first time they hit this.Fixed by treating "is an agmsg shim (marker matches) but records no owner" as legacy owner, unknown rather than unowned — it now fails closed the same as a foreign-owned shim, differing only in the message shown (a named owner vs. "predates ownership tracking (#553)").
AGMSG_CODEX_SHIM_FORCE=1still claims it either way. Forced reproduction: reverted the guard to its previous[ -n "$owner" ]form, confirmed the tampered-shim test'sinstallassertion flips to succeeding-when-it-shouldn't (RED), restored the fix, confirmed it refuses (with the "predates ownership tracking" message) and only a forced install proceeds (GREEN).Also folded in, non-blocking per review but cheap to close: the owner comment now stores the shell-
%q-quoted path (matching the executable export line's own quoting) instead of the raw path, and every reader compares/displays the quoted form — a path containing a literal newline could otherwise have forged a second, fake comment line of its own.Fixed in review: bare
--update(no--cmd) forcing depended on a PR that isn't actually on this baseI originally reasoned that bare
--update's force was safe because resolving to a single existing install is as unambiguous as naming it explicitly. Review measured that premise directly against the actual base branches rather than taking it on faith:#659 (the fail-closed fix for bare
--updatewith multiple installs, #599) is not merged into eithermainorintegration/remote. On this actual base, bare--updatewith more than one install present silently picks whichever sorts first from a glob (#599, still open) — so forcing the shim reclaim unconditionally meant an install the caller never named at all could steal the shim from another one, compounding #599 with a #553-shaped consequence. My reasoning would have been correct if #659 had landed; it hadn't, on the branch that matters.Fixed by capturing whether
--cmdwas passed explicitly —CMD_WAS_EXPLICIT, set beforeCMD_NAMEgets defaulted or resolved to anything else — and forcing only then. Bare--updatestill refreshes a shim this same install already owns (the common single-install case, unaffected), but no longer reaches past one it doesn't; a conflict now surfaces the same refusal message a fresh install would. Forced reproduction: reverted to unconditional forcing, confirmed the new "bare --update does NOT force-steal" test fails (RED) — a shim owned by one install gets silently reassigned to the other — restored the fix, confirmed it's left untouched (GREEN).Fixed in review: the previous two fixes, combined, blocked the single-install upgrade they were never meant to touch
Raised in review: failing closed on an owner-unknown shim (previous section) and no longer forcing bare
--update(the section before that) are each correct alone, but together they mean nearly every real machine — whose existing shim predates ownership tracking and therefore has no owner comment — could no longer have that shim refreshed by a routineinstall.sh --updatewith no--cmd, the normal way a single-install user upgrades. The shim would silently stay stale (potentially still pointing at a pre-types/→scripts/drivers/types/-move path, the exact breakage the surrounding comment already exists to prevent) with no error and no indication anything needed attention.Fixed by adding
agmsg_only_one_install: true iff exactly one.agmsg-marked directory exists anywhere under~/.agents/skills, derived directly from this machine's actual state (not assumed from whether #659 is merged, for the same "measure the base" reason as above). An owner-unknown shim is now claimable without--cmd/--forcespecifically when this holds — with only one agmsg install anywhere, nothing else could have written it, so there is no one to take it from. With two or more installs present, the check returns false and the existing fail-closed behavior for an unrecorded owner is unchanged. Forced reproduction: reverted the allowance, confirmed the new "bare --update migrates this machine's own pre-#553 shim" test fails (RED) — the legacy shim is left stale — restored the fix, confirmed it migrates to the new owner-tracked format (GREEN).Test plan
Positive control, by hand, before writing the fix: installed a production copy (
--cmd agmsg), installed its Codex shim, then installed a second, differently-named copy (--cmd agmsg-dfr) and confirmed byte-for-byte that the shim's recordedAGMSG_CODEX_SHIM_SCRIPT_DIRswitched from the first install to the second.New tests, forced RED (via
git stashon just the production files) confirmed failing against the unfixed code, then GREEN with the fix restored:tests/test_install.bats:install: a second, differently-named install does NOT clobber the first's Codex shim (#553)— pins the shim's exact recorded line unchanged, byte for byte, after the second install.install: --update --cmd can reclaim a Codex shim owned by a different install (#553)— the documented recovery path still works.install: bare --update (no --cmd) does NOT force-steal a Codex shim owned by a different install (#553)— pins the scoping fix: whichever install an ambiguous bare--updateresolves to on this base, it must not walk away with a shim it was never explicitly told to claim.install: bare --update migrates this machine's own pre-#553 (owner-unknown) Codex shim (#553)— pins the single-install allowance: the routine upgrade path a real, single-install user actually hits still works.tests/test_codex_shim.bats(unit-level, againstcodex-shim-install.shdirectly, using two independent copies of the codex driver dir to stand in for two installs):codex shim install: refuses to repoint a shim owned by a different installcodex shim install: AGMSG_CODEX_SHIM_FORCE=1 reclaims a shim owned by a different installcodex shim status: names which install currently owns the shimcodex shim install: a plain, non-agmsg codex binary is still refused regardless of ownership wording (#553 regression guard)— the pre-existing, unrelated "don't touch a real user binary" guard stays intact and isn't described as an ownership conflict.codex shim status: a multi-line status output does not spuriously fail a piped grep -q check (#553 regression guard)— pins the SIGPIPE hazard above directly: documents that the naive piped form still fails today, and that the capture-first forminstall.shactually uses does not.codex shim status/install: a tampered shim cannot execute code via ownership parsing (#553 security regression guard)— plants a shim carrying the real marker plus a crafted line shaped like the oldeval'd export, with a command substitution that writes a sentinel file if ever executed; confirms the sentinel never appears viastatus, a plain (refused)install, or a forcedinstall. Doubles as the legacy-shim regression test: this crafted shim has no owner comment, so a plaininstallmust refuse it (with the "predates ownership tracking" message), not silently claim it.Full suite:
bash -n install.sh/bash -n codex-shim-install.shboth clean.tests/test_codex_shim.bats+tests/test_install.bats: 73/73 pass.Related
Read
#599's fix (PR #659, still open againstmain, confirmed not merged into eithermainorintegration/remote— see the bare---updatefix above) before designing this, per instruction: same file (install.sh), same failure family ("a second install is invisible to / silently interferes with the first"). #659 fixes ambiguous candidate selection for a bare--updatewith multiple installs present (which install does an unqualified--updatemean); this PR fixes ownership verification for the shim specifically, regardless of how the target install was selected. The two touch different lines/logic within the same file and don't conflict — #659 never reaches the shim-refresh block's own condition, it only changes whichSKILL_DIRgets resolved before that block runs. This PR's own safety no longer assumes #659 landed (see above) — if #659 does land first,CMD_WAS_EXPLICITstill works unchanged, since it only depends on whether the caller typed--cmd, not on how ambiguity is otherwise handled.Known, accepted overlap with #659 (flagged in review, non-blocking):
agmsg_only_one_install(codex-shim-install.sh) and #659's own bare---updatecandidate enumeration (not written yet, since #659 is unmerged) both answer the same question — how many agmsg installs exist on this machine — by independently scanning~/.agents/skillsfor.agmsgmarkers. This is duplication, not conflict: the two never call each other or share state today, and nothing in either implementation depends on the other's presence or absence. To leave a path toward unifying them later without needing changes on #659's side, the scan was split out intoagmsg_install_candidates(lists the candidate directories) withagmsg_only_one_installnow just counting its output — so once #659 lands on a shared base with this file, its enumeration could callagmsg_install_candidatesinstead of re-implementing the same scan. Full unification wasn't done here: #659's counting logic doesn't exist on this base yet, so there is nothing concrete to point it at.Fixed in review: self wasn't counted as a candidate until its own marker existed
Raised in review, independently verified against the code (
install.sh:436checks/refreshes the shim,install.sh:452touches this install's own.agmsgmarker — the marker comes after): during a fresh, second, differently-named install,agmsg_only_one_installran before that install's own marker was written. It only saw the FIRST install's marker on disk, concluded "only one install exists anywhere", and let the second install silently claim a legacy (owner-unknown) shim without--force— reintroducing #553's own bug specifically for the single-legacy-shim case the previous round's fix was meant to preserve.Fixed by counting other installs relative to self (self's own skill root, recovered from
SCRIPT_DIRby stripping the fixed.../scripts/drivers/types/codexsuffix install.sh always lays this script out under) instead of a plain total. Self now counts as a candidate whether or not its own marker is written yet — but only when self's recovered skill root structurally sits under~/.agents/skills; otherwise (e.g. this script run directly against a repo checkout, as several of this file's own tests do) self isn't trusted as an implicit member, and the check falls back to the previous exact-one-real-marker behavior. Forced reproduction: added a fresh-install-level regression test, confirmed it fails (RED) against the self-unaware version — the second install's shim step reads the first install's lone marker and silently repoints — restored the fix, confirmed GREEN. Also added direct unit coverage for the boundary (self invisible with zero/one other install, self already registered with a space in its name, an unmarked sibling correctly excluded), each constructing a realistic$0viabash -c's argv0 trick soSCRIPT_DIRresolves the way it does in a real invocation.Fixed in review: this round's own test additions broke the enforced-assertions gate
.github/scripts/check-enforced-assertions.shcounts non-last[[ ]]/(( ))/! cmdassertions in bats tests, because a false one in that position can't fail the suite on macOS bash 3.2 (#670) — silently indistinguishable from a check that was never written. This round's new/rewritten tests pushed the count 18 over the 616 baseline. Converted the flagged lines togrep -qF/refute(both plain commands, always enforced regardless of position) or left as[ ], which the checker never flags. No behavior change to any assertion; count is back to exactly 616.Known, accepted overlap with #659 (flagged in review, non-blocking)
agmsg_only_one_install(codex-shim-install.sh) and #659's own bare---updatecandidate enumeration (not written yet, since #659 is unmerged) both answer the same question — how many agmsg installs exist on this machine — by independently scanning~/.agents/skillsfor.agmsgmarkers. This is duplication, not conflict: the two never call each other or share state today, and nothing in either implementation depends on the other's presence or absence. To leave a path toward unifying them later without needing changes on #659's side, the scan was split intoagmsg_install_candidates(lists candidate directories) withagmsg_only_one_installcounting its output — so once #659 lands on a shared base with this file, its enumeration could callagmsg_install_candidatesinstead of re-implementing the same scan. Full unification wasn't done here: #659's counting logic doesn't exist on this base yet, so there is nothing concrete to point it at.base:
integration/remote. Not merging — awaiting review.