Files
gstack/docs/uninstall.md
T
Garry Tanandcapy-ai[bot] 6b327148e3 Stop gstack-uninstall deleting unpushed or kept work under any folder it removes (GST-2303) (#4282)
* Stop gstack-uninstall deleting unpushed or kept work under any folder it removes (GST-2303)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep repos behind a symlinked .gstack-worktrees, remove stable-channel installs, and read worktrees on git before 2.36 in gstack-uninstall (GST-2303)

Review-Finding: p1/red-team#1 reader=red-team
Review-Finding: p1/red-team#2 reader=red-team
Review-Finding: p1/red-team#3 reader=red-team
Review-Finding: p1/specialist-batched#1 reader=testing
Review-Finding: p1/specialist-batched#2 reader=testing
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Count a FETCH_HEAD commit as pushed only when it came from one of the checkout's remotes in gstack-uninstall (GST-2303)

Review-Finding: p2/specialist-batched#1 reader=security
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Read remote URLs with git remote get-url so url.insteadOf rewrites match FETCH_HEAD in gstack-uninstall (GST-2303)

Review-Finding: fc/fix-check#1 reader=fix-check
Review-Finding: fc/fix-check#2 reader=fix-check
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Refuse to uninstall on git older than 2.36 instead of reading worktrees without -z (GST-2303)

Review-Finding: v11/outside-voice#1 reader=outside-voice
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* List the git 2.36 uninstall refusal in troubleshooting (GST-2303)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-10 09:34:35 +00:00

9.2 KiB

Uninstall

Option 1: Run the uninstall script

If gstack is installed on your machine:

~/.claude/skills/gstack/bin/gstack-uninstall

This handles skills, symlinks, harness registrations (the files gstack-harness wrote into OpenClaw, Hermes, a Dot or another harness, from their ledgers, plus the opt-in start hook, which it turns off through the harness's own CLI), global state (~/.gstack/), project-local state, browse daemons, and temp files. Use --keep-state to preserve config and analytics; it also keeps the repo's .gstack/ and .gstack-worktrees/. Use --force to skip confirmation.

A plain uninstall keeps the git-tracked files under .gstack/ (such as ship policy) and removes only the ones git ignores. It stops with exit 2, before removing anything, when a checkout under .gstack-worktrees/ has uncommitted changes, has commits no branch or tag holds, or is not a worktree this repo registered. Pass --force-worktrees to delete those checkouts anyway. Every checkout is checked, including a worktree created inside another checkout under .gstack-worktrees/. A git submodule inside a clean checkout is deleted with it only when every commit it holds is on a remote-tracking branch; otherwise it stops the uninstall. A locked worktree (git worktree lock) always stops the uninstall, even with --force-worktrees; unlock it with git worktree unlock <path> if you want it gone. If git worktree list fails, the uninstall stops too, even with --force-worktrees, and so it does on git older than 2.36, which cannot list worktrees with -z. The check runs again after you confirm, before anything is removed, so a checkout you edit while the confirmation prompt waits is kept: uninstall exits 2 and removes nothing. Run from inside the gstack install itself, a .gstack/ that holds its own git repository also stops the uninstall, because deleting the install would take it along. A .gstack/ that is its own git repository is kept, with a message saying so, because its commits may exist nowhere else. When .gstack-worktrees is a symlink, uninstall removes only the link and never follows it, so the folder it points at and the worktrees there are kept.

Before it deletes anything, uninstall also checks every folder it is about to delete: each host's gstack install (global and project-local), the gstack-made Claude per-skill folders, and ~/.gstack. If any git checkout inside one of them has uncommitted changes, has commits that neither a remote-tracking branch nor a fetch from one of its remotes (FETCH_HEAD, where a stable-channel upgrade lands) holds, or has a locked worktree, the uninstall stops with exit 2 and names the checkout. So does a folder that holds something this run keeps: with --keep-state, an install that contains ~/.gstack, your state root, or the current repo's .gstack/ or .gstack-worktrees/ (for example, when you run it from inside the gstack checkout) is refused rather than deleted with them. Push or move the work, or run uninstall from outside the install, then re-run it (troubleshooting).

Option 2: Manual removal (no local repo)

If you don't have the repo cloned (e.g. you installed via a Claude Code paste and later deleted the clone):

# 0. A delete that stops for unsaved git work: each folder goes only when no git
#    checkout inside it has uncommitted changes, commits that neither a
#    remote-tracking branch nor a fetch from one of its remotes holds, or a
#    locked worktree. A symlink is only unlinked.
# The FETCH_HEAD commits a fetch from one of checkout $1's remotes wrote.
gstack_remote_fetch_heads() (
  cd "$1" || exit 0
  git remote | while IFS= read -r n; do git remote get-url --all "$n"; done |
    sed -E 's#^([a-z+]+://)?[^@/]+@#\1#; s#/+$##; s#\.git$##' |
    awk 'NR == FNR { u[$0]; next } { n = split($0, a, " of "); if (n > 1 && (a[n] in u)) print $1 }' - "$(git rev-parse --git-path FETCH_HEAD)" 2>/dev/null
)
gstack_rm_checked() {
  local d g r s ok
  for d in "$@"; do
    if [ -L "$d" ]; then rm -f -- "$d"; continue; fi
    [ -d "$d" ] || continue
    ok=1
    while IFS= read -r -d '' g; do
      r=${g%/.git}
      s=$(git -C "$r" status --porcelain --untracked-files=all) && [ -z "$s" ] &&
        s=$(git -C "$r" rev-list -n 1 --all --not --remotes $(gstack_remote_fetch_heads "$r")) && [ -z "$s" ] &&
        git -C "$r" worktree list --porcelain -z >/dev/null 2>&1 &&
        ! git -C "$r" worktree list --porcelain -z | tr '\0' '\n' | grep -q '^locked' ||
        { echo "kept $d: $r has uncommitted changes, unpushed commits or a locked worktree" >&2; ok=0; break; }
    done < <(find "$d" -name .git -prune -print0 2>/dev/null)
    [ "$ok" = 1 ] && rm -rf -- "$d"
  done
  return 0
}

# 1. Stop browse daemons
pkill -f "gstack.*browse" 2>/dev/null || true

# 2. Remove per-skill directories whose SKILL.md points into gstack/
#    (rm -rf, not rmdir — installed dirs also contain runtime-asset links)
find ~/.claude/skills -mindepth 1 -maxdepth 1 -type d ! -name gstack 2>/dev/null |
while IFS= read -r dir; do
  link="$dir/SKILL.md"
  [ -L "$link" ] || continue
  target=$(readlink "$link" 2>/dev/null) || continue
  case "$target" in
    gstack/*|*/gstack/*)
      rm -rf "$dir"
      ;;
  esac
done
# Directories gstack created carry a .gstack-owned marker (the only signal on
# Windows, where installs are file copies with no symlink to read)
for marker in ~/.claude/skills/*/.gstack-owned; do
  [ -f "$marker" ] && rm -rf "$(dirname "$marker")"
done
# Alias skills install as copies (no symlink to detect) — remove by name
rm -rf ~/.claude/skills/_gstack-command ~/.claude/skills/connect-chrome 2>/dev/null

# 3. Remove gstack
gstack_rm_checked ~/.claude/skills/gstack

# 4. Remove global state
gstack_rm_checked ~/.gstack

# 5. Remove integrations (skip any you never installed)
gstack_rm_checked "${CODEX_HOME:-$HOME/.codex}/skills/gstack"*
gstack_rm_checked ~/.factory/skills/gstack*
gstack_rm_checked ~/.kiro/skills/gstack*
gstack_rm_checked "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/skills/gstack"* "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/skills/.gstack-skills"
gstack_rm_checked "${HERMES_HOME:-$HOME/.hermes}/skills/gstack"
gstack_rm_checked ~/.cursor/skills/gstack*
gstack_rm_checked ~/.config/opencode/skills/gstack*
gstack_rm_checked ~/.gemini/antigravity-cli/skills/gstack*
gstack_rm_checked ~/.gemini/skills/gstack*

# 6. Remove temp files
rm -f /tmp/gstack-* 2>/dev/null

# 7. Per-project cleanup (run from each project root; uses gstack_rm_checked from step 0)
gstack_rm_checked .claude/skills/gstack .agents/skills/gstack* .factory/skills/gstack*
# Browse state: removes only the files git ignores, so tracked files such as
# ship policy, and a .gstack/ that is its own git repo, stay.
git clean -fdXq -- .gstack 2>/dev/null
# Worktrees, deepest first: each goes only when it has no uncommitted changes,
# no commits a branch or tag lacks, and no checkout inside it. git refuses a
# locked one. Anything printed as kept still holds work: save it, then re-run.
root=$(git rev-parse --show-toplevel)
git worktree list --porcelain | sed -n 's/^worktree //p' | sort -r |
while IFS= read -r wt; do
  case "$wt" in "$root/.gstack-worktrees/"*) ;; *) continue ;; esac
  if [ -z "$(git -C "$wt" status --porcelain --untracked-files=all)" ] &&
     [ -z "$(git -C "$wt" log --oneline -1 HEAD --not --branches --tags)" ] &&
     [ -z "$(find "$wt" -mindepth 2 -name .git -print -quit)" ]; then
    git worktree remove "$wt" || echo "kept $wt"
  else
    echo "kept $wt: it holds work or another checkout"
  fi
done
find .gstack-worktrees -depth -type d -empty -delete 2>/dev/null || true

Anything gstack_rm_checked prints as kept still holds work: push or move it, then run that step again. Never rm -rf .gstack-worktrees by hand: a checkout there can hold uncommitted work, commits no branch has, or another worktree nested inside it, and rm -rf deletes all of it. Anything the loop above keeps, check and remove yourself.

Manual removal leaves gstack's hook entries behind in ~/.claude/settings.json (the uninstall script removes all of them for you, including entries whose _gstack_source tag was stripped). Edit that file and delete every hook whose command path points into .claude/skills/gstack/: the SessionStart auto-update hook, the AskUserQuestion PreToolUse/PostToolUse hooks, and the Stop hooks (session timeline, plus verify-gate if you opted in). Left in place, they error on every matching event once the install directory is gone.

If you turned on the OpenClaw or Hermes start hook, manual removal also leaves its gstack-start plugin turned on. Turn it off with openclaw config unset plugins.entries.gstack-start or hermes plugins disable gstack-start, then delete ${OPENCLAW_STATE_DIR:-~/.openclaw}/extensions/gstack-start or ${HERMES_HOME:-~/.hermes}/plugins/gstack-start.

Clean up CLAUDE.md

The uninstall script does not edit CLAUDE.md. In each project where gstack was added, remove the ## gstack and ## Skill routing sections.

Playwright

~/Library/Caches/ms-playwright/ (macOS) is left in place because other tools may share it. Remove it if nothing else needs it.

Aside

gstack never installed Aside, so it never uninstalls it. Keep it or remove it like any other app.