mirror of https://github.com/garrytan/gstack.git
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:
parent
026751ea20
commit
0c7ef235ed
|
|
@ -9,7 +9,7 @@
|
|||
# CI / container env where HOME may be unset.
|
||||
#
|
||||
# Chains:
|
||||
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA -> $HOME/.gstack -> .gstack
|
||||
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA (only when CLAUDE_PLUGIN_ROOT=*gstack*) -> $HOME/.gstack -> .gstack
|
||||
# PLAN_ROOT: GSTACK_PLAN_DIR -> CLAUDE_PLANS_DIR -> $HOME/.claude/plans -> .claude/plans
|
||||
# TMP_ROOT: TMPDIR -> TMP -> .gstack/tmp (and mkdir -p, best-effort)
|
||||
#
|
||||
|
|
@ -21,7 +21,11 @@ set -u
|
|||
# State root: where gstack writes projects/, sessions/, analytics/.
|
||||
if [ -n "${GSTACK_HOME:-}" ]; then
|
||||
_state_root="$GSTACK_HOME"
|
||||
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then
|
||||
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ] && echo "${CLAUDE_PLUGIN_ROOT:-}" | grep -qi "gstack"; then
|
||||
# Guard: only trust CLAUDE_PLUGIN_DATA when CLAUDE_PLUGIN_ROOT confirms we are
|
||||
# running as the gstack plugin. Without this, a CLAUDE_PLUGIN_DATA from another
|
||||
# plugin (e.g. codex) that leaked into the session env via CLAUDE_ENV_FILE would
|
||||
# be picked up, writing all gstack state into the wrong directory.
|
||||
_state_root="$CLAUDE_PLUGIN_DATA"
|
||||
elif [ -n "${HOME:-}" ]; then
|
||||
_state_root="$HOME/.gstack"
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue