garrytan / garrytan/gstack

./setup run inside a project-vendored gstack silently captures the machine-global Codex skill namespace

Open
#2,879 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.