fix(gstack-paths): guard CLAUDE_PLUGIN_DATA against cross-plugin contamination (#1569)

gstack-paths previously trusted CLAUDE_PLUGIN_DATA as a fallback for
GSTACK_STATE_ROOT whenever GSTACK_HOME was unset. When another plugin
(e.g. Codex) persists its own CLAUDE_PLUGIN_DATA into the session env
via CLAUDE_ENV_FILE, gstack picked it up and wrote checkpoints,
analytics, and learnings into that plugin's directory. Anyone with the
Codex plugin installed alongside gstack hit this silently.

Fix: guard the CLAUDE_PLUGIN_DATA branch so it only fires when
CLAUDE_PLUGIN_ROOT confirms we're running as the gstack plugin (path
contains "gstack"). Skill installs fall through to \$HOME/.gstack.

Contributed by @ElliotDrel via #1570. Closes #1569.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-18 20:31:32 -07:00
parent 026751ea20
commit 0c7ef235ed
2 changed files with 26 additions and 6 deletions
+20 -4
View File
@@ -41,12 +41,28 @@ describe('gstack-paths', () => {
expect(got.GSTACK_STATE_ROOT).toBe('/tmp/explicit-state');
});
test('CLAUDE_PLUGIN_DATA wins over HOME when GSTACK_HOME unset', () => {
const got = run({
CLAUDE_PLUGIN_DATA: '/tmp/plugin-data',
test('CLAUDE_PLUGIN_DATA ignored when CLAUDE_PLUGIN_ROOT is absent or non-gstack', () => {
// Without CLAUDE_PLUGIN_ROOT, falls through to HOME path.
const noRoot = run({ CLAUDE_PLUGIN_DATA: '/tmp/plugin-data', HOME: '/tmp/home' });
expect(noRoot.GSTACK_STATE_ROOT).toBe('/tmp/home/.gstack');
// With a CLAUDE_PLUGIN_ROOT that doesn't contain "gstack" (e.g. the codex plugin),
// still falls through to HOME path — this is the cross-plugin contamination scenario.
const wrongRoot = run({
CLAUDE_PLUGIN_DATA: '/tmp/codex-data',
CLAUDE_PLUGIN_ROOT: '/tmp/openai-codex',
HOME: '/tmp/home',
});
expect(got.GSTACK_STATE_ROOT).toBe('/tmp/plugin-data');
expect(wrongRoot.GSTACK_STATE_ROOT).toBe('/tmp/home/.gstack');
});
test('CLAUDE_PLUGIN_DATA respected when CLAUDE_PLUGIN_ROOT identifies gstack', () => {
const got = run({
CLAUDE_PLUGIN_DATA: '/tmp/gstack-plugin-data',
CLAUDE_PLUGIN_ROOT: '/tmp/gstack-garrytan',
HOME: '/tmp/home',
});
expect(got.GSTACK_STATE_ROOT).toBe('/tmp/gstack-plugin-data');
});
test('HOME-derived state root when GSTACK_HOME and CLAUDE_PLUGIN_DATA unset', () => {