./setup run inside a project-vendored gstack silently captures the machine-global Codex skill namespace
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
## Summary
`setup` writes the Codex host registration to a **machine-global** destination, but sources it from **whichever checkout `./setup` happened to run in**. Running setup once inside a project that carries its own vendored gstack therefore repoints every Codex session on the machine at that project's copy, including in completely unrelated repos.
Combined with #1925 (upgrades only refresh the default Claude host), the capture never heals: the machine-owned Claude checkout keeps upgrading while the Codex lane stays pinned to the project copy indefinitely.
## Mechanism (read at v1.87.0.0)
```
setup:64 SOURCE_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)" # wherever setup was invoked
setup:67 CODEX_SKILLS="${CODEX_HOME:-$HOME/.codex}/skills" # machine-global
setup:2398 link_codex_skill_dirs "$SOURCE_GSTACK_DIR" "$CODEX_SKILLS"
```
`link_codex_skill_dirs` symlinks `$SOURCE_GSTACK_DIR/.agents/skills/gstack-*` into the global `$CODEX_SKILLS`. Nothing checks that `SOURCE_GSTACK_DIR` is a machine-owned checkout rather than a project-local one.
`CODEX_REPO_LOCAL` does not cover this: it is set by inspecting the *destination* (`INSTALL_SKILLS_DIR` basename `skills` with parent `.agents`), so a **global** install whose **source** is project-local takes the ordinary global path.
## Reproduction
1. Create a project from a template that vendors gstack into the project. Conductor's "new project with gstack" flow does this; any repo with a committed `.claude/skills/gstack` has the same shape.
2. Run `./setup --host codex` from that project's vendored gstack.
3. Open Codex in any other, unrelated repo.
4. Every `gstack-*` skill resolves into the first project.
```
readlink -f ~/.codex/skills/gstack-investigate/SKILL.md
/.claude/skills/gstack/.agents/skills/gstack-investigate/SKILL.md
```
On my machine this persisted for five weeks. The machine-owned Claude checkout moved from v1.60.1.0 to v1.87.0.0 while all **52** Codex skill symlinks stayed pinned to the project copy at v1.60.1.0 (plus the `~/.codex/skills/gstack` runtime root, whose `bin`, `lib`, `ETHOS.md` and `SKILL.md` entries also pointed into the project). For `gstack-investigate` alone the two generated `SKILL.md` files differ by **666 lines**.
Two properties make it hard to notice:
- **The captured skills work.** They are real gstack skills, just old, so nothing errors.
- **It is invisible from inside the project that caused it**, because from there the path looks correct. It is only visible from an unrelated repo, and only if you happen to look at a resolved skill path.
What finally surfaced it was a Codex skill reference rendering its absolute path in an unrelated project, which is not something a user would normally read.
## Suggested direction
Either of these would have prevented it:
1. **Refuse to write a machine-global host lane from a project-local source without an explicit opt-in.** If `SOURCE_GSTACK_DIR` is not the machine-owned install root, require a flag before writing `$HOME/.codex/skills`, and print which directory is about to own the global namespace. A one-line confirmation at install time would have made this impossible to do by accident.
2. **Persist installed hosts and their source roots**, then have any later setup or upgrade re-assert the recorded source, which repairs a capture automatically. This is the Fix 2 direction #1925 already converged on.
(1) prevents the capture, (2) recovers from it. (2) is tracked in #1925, so (1) looks like the unowned half. Filing this separately rather than commenting on #1925 because the entry vector is a different defect from the upgrade-refresh gap, though the two compose into the silent five-week version.
## Workaround
Re-run from the machine-owned checkout:
```
cd ~/.claude/skills/gstack && ./setup --host codex
```
This is self-healing: `create_codex_runtime_root` removes the stale runtime root and `link_codex_skill_dirs` overwrites each existing symlink. Verify with `readlink -f ~/.codex/skills/gstack-/SKILL.md`.
Note that active Codex sessions cache their skill catalog, so verification needs a fresh session.
## Environment
- macOS (Apple Silicon)
- gstack v1.87.0.0, machine-owned checkout at `~/.claude/skills/gstack`
- Codex CLI, global host lane at `~/.codex/skills`
## Related
- #1925 — upgrades only re-register Claude, so a capture like this never self-corrects
- #2347 — explicit `--host` selection not being exclusive (adjacent host-boundary shape)
- #1235 — repo-local install option that keeps state inside the project
Contributor guide
Research direction
Read setup around SOURCE_GSTACK_DIR, CODEX_SKILLS, and link_codex_skill_dirs, then compare the global and repo-local installation paths. Review #1925 and the related host-boundary issues before choosing how source roots should be protected. Done means a project-local invocation cannot silently repoint the machine-global Codex skill namespace, with the behavior verifiable through the documented readlink reproduction.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- shell
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100