* test(wireup): make gbrain-missing PATH fixture hermetic The gbrain-missing test appended the host PATH (and a hardcoded /opt/homebrew/bin) to the fixture PATH, so on any machine with a real gbrain installed the 'missing' case saw it, exited 0 instead of 2, and could never fail where the bug exists — a false green for a whole machine class. The fixture now keeps only root-owned OS dirs on the child PATH, and a new determinism check plants a host-like gbrain to prove it is unreachable. Absorbed from PR #2615 with authorship preserved; the PR-thread liveness screenshot (docs/images/gstack-pr-liveness-2255.png) is dropped — referenced by nothing in the tree. Fixes #2255 Co-authored-by: CommandCodeBot <noreply@commandcode.ai> * fix(evidence): stop bun's dotenv autoload from reaching the spawned command `bin/gstack-evidence` has a `#!/usr/bin/env bun` shebang, and bun AUTO-LOADS `.env`, `.env.<NODE_ENV>` and `.env.local` from the cwd into `process.env`. The wrapper then spawned the command with no `env` override, so every command run through it inherited those variables — and a repo `.env.local` routinely holds production credentials. Two things go wrong, and the second is worse than the leak: 1. Secrets reach a child that would not otherwise have them. `npm test` run by hand in the same shell sees none of them; the same command through the wrapper sees all of them. 2. THE COMMAND UNDER TEST BEHAVES DIFFERENTLY, so the ledger certifies a run that is not the run CI performs. Observed in a Next.js repo on 2026-08-20: four tests failed 4/4 through the wrapper and passed 5/5 without it, because app code branched on env vars only the wrapper supplied. Nearly an hour went into chasing a "flake" that was the measuring instrument. The wrapper exists to record trustworthy evidence, so silently altering the environment defeats its purpose. The fix builds the child env from `process.env` minus the keys bun injected, and detection is exact rather than heuristic: verified on bun 1.3.11, a dotenv file does NOT override a variable the shell already exported (the shell's value wins). So a key whose live value equals the dotenv file's value was injected by bun, and dropping it restores the environment the user's own shell would have given the command. A key whose live value differs is genuinely the caller's and survives. `BUN_DOTENV_FILES()` mirrors bun's precedence, including that `.env.local` is skipped when NODE_ENV is "test" — scrubbing a key bun never loaded would strip a variable the caller legitimately provided. Escape hatch: GSTACK_EVIDENCE_KEEP_DOTENV=1 keeps the old behaviour. When keys are scrubbed the wrapper warns with the KEY NAMES ONLY, so the diagnostic cannot become the leak it prevents. Tests: 6 cases, mutation-verified — removing `env: spawnEnv` reddens exactly the two leak tests and restoring it gives 30/30. Every leak test asserts the scrub warning fired, because `bun test` sets NODE_ENV=test and the first version of these tests passed vacuously against a `.env.local` bun had never loaded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Absorbed from PR #2652 with authorship preserved. Wave additions: a doc-comment on the ${VAR}-expansion limitation (bun expands refs, the reader compares raw text — those keys are left in the child env, failing open) and a regression pin for the unreadable-.env fail-open path with a functional DAC-override skip guard. Fixes #2624 * fix(setup): reap dangling skill dirs when the payload is gone cleanup_old_claude_symlinks derived its work list from the payload directory, so when the payload was gone — precisely when orphans exist — the glob matched nothing and the loop never ran; the -f guard also followed symlinks, hiding dangling SKILL.md links even with a payload present. The cleanup now scans the DESTINATION skills dir (-e/-L, so dangling symlinks are visible) and anchors SKILL.md provenance to path segments (gstack/*, */gstack/*, */.gstack/render/claude/*) instead of a bare *gstack* substring that would eat a user skill under ~/tools/gstack-fork/. The Windows real-file arm stays payload-gated: a real file has no provable owner. Absorbed from PR #2634 (2 commits squashed) with authorship preserved. The symmetric cleanup_prefixed_claude_symlinks hole is filed as a TODOS.md residual in this wave. Fixes #2204 * fix(redact): tolerate EEXIST from recursive mkdir in install-prepush-hook on bun/Windows (#2635) fs.mkdirSync(dir, { recursive: true }) is a no-op on an existing directory in Node, but bun on Windows throws EEXIST - crashing hook install on any repo whose .git/hooks already existed, leaving the repo unprotected. Add lib/fs-utils.ts mkdirpSync: swallow EEXIST only when statSync confirms the path is an existing directory; a regular file occupying the path, a stat failure, or any other errno still rethrows. Use it in installPrepushHook(). The regression test emulates the Windows bun fs semantics via a bun --preload fixture, so the exact crash path runs (and fails on the old code) on any platform, including CI Linux. Absorbed from PR #2641 with authorship preserved. Fixes #2635 * fix(bin): route remaining Windows-reachable mkdirSync sites through mkdirpSync Sweep follow-up to #2641's lib/fs-utils.ts helper: bun on Windows throws EEXIST from a recursive mkdir on an existing dir, so every unguarded recursive mkdirSync on a Windows-reachable path is a latent crash. Converted: bin/gstack-decision-log (unguarded, runs on every decision log — the second call on any machine hits the pre-existing projects dir), bin/gstack-evidence logsDir + ledger dir sites, and bin/gstack-redact-prepush's skip-log site (already try-wrapped, so its failure mode was a silent skip-log loss rather than a crash — the fix makes the log survive). The ~15 remaining gbrain/mac-lane sites are deliberately left alone. Regression: fs-utils.test.ts drives gstack-decision-log twice, the second run under the bun-Windows EEXIST preload fixture — the pre-sweep code exits 1 with EEXIST there; verified red against v1.68.3.0. * fix(setup-gbrain): warn about the ZeroEntropy sunset before Sept 4 ZeroEntropy was acquired by Notion and sunsets its hosted API on September 4, 2026. A gbrain configured with the zeroentropyai embedding recipe keeps importing pages after that date but embedding silently fails — pages land structurally with no semantic search, this repo's tracker P1 (TODOS.md NEXT PRIORITY). Nothing in gstack ever recommended ZeroEntropy (the dependency is gbrain-internal), so the gstack side is detection + advisory: the wireup helper warns when ~/.gbrain/config.json names the recipe (fail-open grep — a missing, unreadable, or other-provider config stays silent and never blocks a working setup), the setup-gbrain provider-default comments say never to select the legacy recipe for a new brain, and USING_GBRAIN_WITH_GSTACK.md gains a troubleshooting entry. The gbrain-side provider migration stays open upstream. Refs #2365 * fix(gbrain-source-wireup): first sync targets the registered source, not --repo The wireup registered a federated source by id, then ran 'gbrain sync --repo $WORKTREE' — which resolves against the brain's DEFAULT source and (on gbrain 0.46.x) rewrites that source's local_path anchor to our worktree. Net effect: the user's primary knowledge source silently repointed at the gstack brain worktree while the just-registered source got zero pages, and pages_synced still reported success. The sync now targets the registered id ('gbrain sync --source $id', the same form the repo's own troubleshooting documents). Because the script's stated floor is gbrain >= 0.18.0 and nothing proves --source exists there, support is probed via 'gbrain sync --help' first: an older gbrain keeps the wrong-but-working --repo call with an upgrade warning instead of converting it into a hard failure. The probe sits after the GSTACK_BRAIN_NO_SYNC early-exit and is unreachable in --probe mode. Regression tests (fail on v1.68.3.0): a no-skip sync case asserting the call log shows 'sync --source gstack-brain-<id>' and never 'sync --repo', and an old-gbrain fallback case (fake sync --help without --source) asserting --repo plus the upgrade warning. Fixes #2662 * fix(setup): --host slate exits informatively instead of silently installing nothing slate passed --host validation (added to the accept-list in v1.64.1.0) but never got a dispatch arm, and the all-INSTALL_*-zero fallback lives inside the auto branch — so './setup --host slate' configured nothing and exited 0, a silent no-op strictly worse than the original hard rejection. slate is now an informational arm (per docs/designs/SLATE_HOST.md it is blocked on the host-config refactor; Slate reads .claude/skills as a compatibility fallback, so the arm points at './setup --host claude'), and a defensive guard after the dispatch chain errors loudly (naming the host, the missing arm, and the valid targets, exit 1) if a future host is ever accepted without being wired. Regression tests (fail on v1.68.3.0): a dispatch-arm ratchet asserting every accept-listed install target has a matching dispatch branch — the exact drift class; a registry cross-check deriving both sides from hosts/index.ts and setup's case arms; a behavioral slate probe (exit 0, points at --host claude, never reaches the installer — on unfixed code it fell through into the installer); and a static pin on the guard's shape. Fixes #2361 * fix(make-pdf): resolve the sibling browse binary from execPath, not argv[0] In a bun-compiled binary process.argv[0] is the raw invocation string — often relative ('./pdf', 'pdf') — so dirname(argv[0]) yielded '.' and the sibling candidates (../browse/dist/browse etc.) resolved against the CWD instead of the install dir. Resolution was cwd-dependent: correct-by-luck when the fallbacks rescued it, wrong when a cwd-relative path matched. process.execPath is always the absolute binary path. The resolution step takes an injectable selfPath (defaulted) because under bun test the process path is the bun runtime and the compiled-binary shapes are otherwise unreachable. The issue's other half — pdf setup failing on newtab('about:blank') — was already fixed on main in v1.64.0.0 (browse/src/url-validation.ts exact-match allows about:blank; its comment names this exact smoke). This commit closes what remains. Regression tests (the sibling-via-selfPath case fails on v1.68.3.0 — pre-fix code ignores the seam and either resolves the global install or throws): sibling resolution from an install-shaped tree, and a decoy-browse-DIRECTORY case pinning that a directory never wins resolution. Fixes #2156 * fix(memory-ingest): store the normalized git_remote so unattributed pages hit the policy filter buildTranscriptPage wrote the normalized '_unattributed' sentinel into the page FRONTMATTER but stored the raw resolved remote ('' when unresolvable) on the page object. The policy filter fast-paths !p.git_remote, so under --include-unattributed an explicit '_unattributed → deny' (or read-only) policy never applied to exactly the pages it names — they ingested unpoliced. The stored value now matches the frontmatter. Regression test (fails on v1.68.3.0): seeds the REAL bin/gstack-gbrain-repo-policy store with '_unattributed → deny' through its own set verb, ingests an unresolvable-remote session with --include-unattributed, and asserts nothing reaches gbrain — pre-fix the '' remote bypassed the filter and the import ran. A fake echoing tiers would pass on both sides of the fix; the real helper prints 'none' for unknown keys, so only a genuinely applied deny distinguishes the two. Fixes #2353 * fix(land-and-deploy): MERGED recovery reconciles and reports remote-branch cleanup Step 4's merge commands carry --delete-branch, and the success path tells the user 'The branch has been cleaned up.' When gh exits non-zero AFTER GitHub already merged (routine in worktree layouts: gh's local cleanup runs git checkout <base> and fails), the §4a-postfail MERGED recovery re-established everything EXCEPT the branch deletion — and said nothing about it, so the discrepancy was invisible. The MERGED path now reconciles: git ls-remote --heads distinguishes branch-already-gone (exit 0, empty → 'already cleaned up', idempotent on re-runs) from branch-survived (offer confirm-first deletion, matching the section's worktree posture; -d not -D for any local branch) from check-itself-failed (non-zero exit → 'couldn't verify', skip the offer — never read a failed check as a clean branch). Template + regenerated SKILL.md + test extensions land in one commit (the md-sync assertion goes red otherwise). Regression assertions (fail on v1.68.3.0: no delete-branch reconciliation existed in test/ at all) pin the ls-remote check, the confirm-first delete, and the absent-vs-failed distinction. Fixes #2656 * fix(scripts): stop heredoc bodies deadlocking under Homebrew bash `./setup --help` can hang forever on macOS, printing nothing, with no way to tell it apart from a slow install. Eleven scripts carry the same latent hang, `setup` itself being the one every user hits first. bash 5.2+ delivers a heredoc body of 64KiB or less through a pipe: the forked child writes the entire body before exec, and nothing reads the other end until the command starts. Under macOS pipe-KVA pressure the kernel hands a fresh pipe a 512-byte buffer instead of the usual 16-64KiB, so any body of 512 bytes or more blocks write() permanently. The capacity check bash would need to notice (F_GETPIPE_SZ) is Linux-only, so it never fires here. It is pressure-dependent, which is why it reads as "worked on my machine" — the same script runs fine all day and then wedges. Homebrew bash is what `#!/usr/bin/env bash` resolves to on a Mac with brew on PATH, which is most of them. Apple's /bin/bash 3.2 predates the pipe path and is unaffected, so the bug is invisible to anyone testing with the system shell. The fix is `BASH_COMPAT=50` in each affected script, which restores the pre-5.2 tempfile path: $ bash -c 'probe() { [ -p /dev/stdin ] && echo PIPE || echo TEMPFILE; } probe <<EOF $(printf "x%.0s" $(seq 1 1000)) EOF' PIPE $ BASH_COMPAT=50 bash -c '...same...' TEMPFILE - Not a `#!/bin/bash` shebang swap: that pins the script to whatever bash lives at /bin (3.2 on macOS, absent on some Linux distributions) and is bypassed entirely by `bash script.sh` call sites. The variable survives both. - Not exported, so child processes keep their own compat level. - Placed below any `--help` sed range that reads $0, so usage output is unchanged (verified on all eleven). - Every guarded script is bash-3.2-clean — no associative arrays, case conversion, or mapfile — so compat level 50 costs them nothing. test/heredoc-pipe-deadlock.test.ts scans every tracked shell script for a heredoc body in the 512B-64KiB window and fails without the guard, and proves the mechanism at runtime on bash 5.2+ by asserting the body moves from PIPE to TEMPFILE. On older bash the runtime half is skipped, since the pipe path does not exist there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Absorbed from PR #2640 with authorship preserved. Wave adaptations: the pipe-probe test skips on minimal-/dev environments without /dev/stdin (it would report OTHER for an unobservable fd), and one caveat verified during review: on bash 4.3/4.4 (e.g. Git Bash), assigning BASH_COMPAT=50 prints a non-fatal 'invalid value' warning to stderr — those bashes are already on tempfiles, so the guard is a no-op there; windows-setup-e2e exercises this empirically. * docs: TODOS.md v1.69 wave close-out Move the slate P4 entry and the ZeroEntropy P1's gstack-side half to Completed (v1.69.0.0); reframe the ZeroEntropy NEXT PRIORITY entry around the remaining gbrain-side work; file the wave's four residuals with rationale — the prefixed-cleanup symmetric conversion, the #2163 legacy-slug checkpoint heal, the invited #2657 --reconcile contribution, and the table-driven setup host dispatch behind the new cross-check ratchet. * chore: bump version and changelog (v1.69.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Som Samantray <som.samantray@gmail.com> Co-authored-by: CommandCodeBot <noreply@commandcode.ai> Co-authored-by: Connex Client Access <paul@paulkortman.com> Co-authored-by: y$un_ <forrest.sun527@gmail.com> Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com> Co-authored-by: Benjamin D. Smith <benjamin.smith@binarysword.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
31 KiB
Using GBrain with GStack
Your coding agent, with a memory it actually keeps.
GBrain is a persistent knowledge base designed for AI agents. It stores what your agent learns, what you've decided, what worked and what didn't, and lets the agent search all of it on demand. GStack gives you a one-command path from zero to "gbrain is running, and my agent can call it" — with paths for try-it-local, share-with-your-team, and everything between.
This is the full monty: every scenario, every flag, every helper bin, every troubleshooting step. For the quick pitch, see the README's GBrain section. For error codes and sync-specific issues, see docs/gbrain-sync.md.
The one-command install
/setup-gbrain
That's it. The skill detects your current state, asks three questions at most, and walks you through install, init, MCP registration for Claude Code, and per-repo trust policy. On a clean Mac with nothing installed it finishes in under five minutes. On a Mac where something's already set up it takes seconds (it detects the existing state and skips done work).
What you get after setup
Once /setup-gbrain finishes, your coding agent has two retrieval surfaces it didn't have before:
- Semantic code search across this repo.
gbrain search "browser security canary"returns ranked file regions, not exact-match grep hits.gbrain code-def,code-refs,code-callers,code-calleeswalk the call graph by symbol — useful when you don't know which file holds the implementation but you know what it does. The agent prefers these over Grep when the question is semantic; CLAUDE.md gets a## GBrain Search Guidanceblock that teaches it the routing rules. - Cross-session memory. Plans, retros, decisions, and learnings from past sessions live in
~/.gstack/and (if you opted in to artifacts sync) get pushed to a private git repo that gbrain indexes.gbrain search "what did we decide about auth?"actually finds the prior CEO plan instead of you re-describing context every session.
If you also enabled remote MCP (Path 4 below), brain queries route to a shared brain server that other machines can write to — your laptop, your desktop, and a teammate's machine all see the same memory.
The four paths
You pick one when the skill asks "Where should your brain live?"
Path 1: Supabase, you already have a connection string
Best for: you (or a teammate's cloud agent) already provisioned a Supabase brain and you want this local machine to use the same data.
What happens: Paste the Session Pooler URL (Settings → Database → Connection Pooler → Session → copy URI, port 6543). The skill reads it with echo off, shows you a redacted preview (aws-0-us-east-1.pooler.supabase.com:6543/postgres — host visible, password masked), hands it to gbrain init via the GBRAIN_DATABASE_URL environment variable, and the URL is never written to argv or your shell history.
Trust warning: Pasting this URL gives your local Claude Code full read/write access to every page in the shared brain. If that's not the trust level you want, pick PGLite local (Path 3) instead and accept the brains are disjoint.
Path 2a: Supabase, auto-provision a new project
Best for: fresh Supabase account, you want a clean new project with zero clicking.
What happens: You paste a Supabase Personal Access Token (PAT). The skill shows you the scope disclosure first — the token grants full access to every project in your Supabase account, not just the one we're about to create. It lists your organizations, asks which one and which region (default us-east-1), generates a database password, calls POST /v1/projects, polls GET /v1/projects/{ref} every 5 seconds until the project is ACTIVE_HEALTHY (180s timeout), fetches the pooler URL, hands it to gbrain init. End-to-end: ~90 seconds.
At the end: explicit reminder to revoke the PAT at https://supabase.com/dashboard/account/tokens. The skill already discarded it from memory.
If you Ctrl-C mid-provision: The SIGINT trap prints your in-flight project ref + a resume command. You can delete the orphan at the Supabase dashboard, or run /setup-gbrain --resume-provision <ref> to pick up where you left off.
Path 2b: Supabase, create manually
Best for: you'd rather click through supabase.com yourself than paste a PAT.
What happens: The skill walks you through the four manual steps (signup → new project → wait ~2 min → copy Session Pooler URL), then takes over from Path 1's paste step. Same security treatment as Path 1.
Path 3: PGLite local
Best for: try-it-first, no account, no cloud, no sharing. Or a dedicated "this Mac's brain" that stays isolated from any cloud agent.
What happens: gbrain init --pglite. Brain lives at ~/.gbrain/brain.pglite. No network calls for the init itself. Done in 30 seconds.
Embedding model. When VOYAGE_API_KEY is set, gstack inits PGLite with voyage-code-3 (1024-dim) — Voyage's code-specialized embedding model, which beats their general-purpose voyage-4-large and OpenAI text-embedding-3-large head-to-head on this codebase's symbol queries. Without VOYAGE_API_KEY, gbrain auto-selects (OpenAI 1536-dim when OPENAI_API_KEY is present, else falls down its provider chain). Either way, the embeddings call out to the chosen provider's API during sync — set the key for the provider you want before running /sync-gbrain.
This is the best first choice if you just want to see what gbrain feels like before committing to cloud. You can always migrate later with /setup-gbrain --switch.
Path 4: Remote gbrain MCP (split-engine)
Best for: your brain runs on another machine you control (Tailscale, ngrok, internal LAN) or a teammate's server. You want the cross-machine memory benefit without standing up a local database, and you still want symbol-aware code search on this Mac.
What happens: You paste an MCP URL (e.g. https://wintermute.tail554574.ts.net:3131/mcp) and a bearer token. The skill verifies the URL over the wire, registers gbrain as an HTTP MCP in ~/.claude.json at user scope, and offers to also stand up a tiny local PGLite for code search (~30 seconds, ~120 MB disk).
If you accept the local PGLite, you end up in split-engine mode:
- Brain/context queries (
mcp__gbrain__search,mcp__gbrain__query,mcp__gbrain__get_page) route to the remote MCP. Plans, retros, learnings, cross-machine memory — all on the shared server. - Code queries (
gbrain code-def,code-refs,code-callers,code-callees,gbrain searchfor code) route to the local PGLite via the.gbrain-sourcepin in each worktree. Indexed locally, fast, never leaves the machine.
The two engines are independent. Wiping the local PGLite doesn't touch the remote brain; rotating the remote MCP bearer doesn't affect local code search. This is also the right configuration if your remote brain admin can't (or shouldn't) index every developer's checkout — local code stays local.
MCP registration for Claude Code
By default the skill asks "Give Claude Code a typed tool surface for gbrain?" If you say yes, it runs:
claude mcp add gbrain -- gbrain serve
That registers gbrain's stdio MCP server with Claude Code. Now gbrain search, gbrain put, gbrain get, etc. show up as first-class tools in every session, not bash shell-outs.
If claude is not on PATH, the skill skips MCP registration gracefully with a manual-register hint. The CLI resolver still works from any skill that shells out to gbrain — MCP is an upgrade, not a prerequisite.
Other local agents (Cursor, Codex CLI, etc.) need their own MCP registration. The skill is Claude-Code-targeted for v1; other hosts can register gbrain serve manually in their own MCP config.
Per-remote trust policy (the triad)
Every repo on your machine gets a policy decision: read-write, read-only, or deny.
- read-write — your agent can
gbrain searchfrom this repo's context AND write new pages back to the brain. Default for your own projects. - read-only — your agent can search the brain but never writes new pages from this repo's sessions. Ideal for multi-client consultants: search the shared brain, don't contaminate it with Client A's code while you're in Client B's repo.
- deny — no gbrain interaction at all. The repo is invisible to gbrain tooling.
The skill asks once per repo the first time you run a gstack skill there. After that the decision is sticky — every worktree + branch of the same git remote shares the same policy, so you set it once and it follows you.
SSH and HTTPS remote variants collapse to the same key: https://github.com/foo/bar.git and git@github.com:foo/bar.git are the same repo.
To change a policy:
/setup-gbrain --repo # re-prompt for this repo only
# Or directly:
~/.claude/skills/gstack/bin/gstack-gbrain-repo-policy set "github.com/foo/bar" read-only
To see every policy:
~/.claude/skills/gstack/bin/gstack-gbrain-repo-policy list
Storage: ~/.gstack/gbrain-repo-policy.json, mode 0600, schema-versioned so future migrations stay deterministic.
Keeping the brain current with /sync-gbrain
/setup-gbrain is one-time onboarding. /sync-gbrain is the verb you run every time you want gbrain to see fresh changes in this repo's code.
/sync-gbrain # incremental: mtime fast-path, ~seconds on a clean tree
/sync-gbrain --full # full reindex (~25-35 minutes on a big Mac)
/sync-gbrain --code-only # only the code stage; skip memory + brain-sync
/sync-gbrain --dry-run # preview what would sync; no writes
The skill runs three stages — code, memory, brain-sync — independently. A failure in one doesn't block the others. State persists to ~/.gstack/.gbrain-sync-state.json so re-running picks up cleanly.
Stages that can send data off-machine (code sync into a possibly-remote gbrain DB, memory ingest, the brain-sync push) each write a tamper-evident receipt to the egress ledger (~/.gstack/security/egress.jsonl) before sending, fail-closed: if the receipt can't be written, the stage refuses with EGRESS_RECEIPT_FAILED instead of syncing unrecorded. Fix is usually mkdir -p ~/.gstack/security && chmod -R u+w ~/.gstack/security, then re-run. Inspect receipts with gstack-egress list.
What it does on a fresh worktree:
- Pre-flight. Checks
gbrain_local_status(the local engine's health). If the engine isbroken-dborbroken-config, the skill STOPs with a remediation menu — it refuses to silently degrade. If the local engine is missing and you're in remote-MCP mode (Path 4), the code stage SKIPs cleanly and only brain-sync runs. - Code stage. Registers the cwd as a federated source via
gbrain sources add, writes a.gbrain-sourcepin file in the repo root (kubectl-style context — every worktree gets its own pin, so Conductor sibling worktrees don't collide), runsgbrain sync --strategy code. - Memory stage. Stages your
~/.gstack/transcripts + curated memory. In local-stdio MCP mode, ingests into the local engine. In remote-http MCP mode, persists staged markdown to~/.gstack/transcripts/run-<pid>-<ts>/for the remote brain admin's pull pipeline. The ingest timeout is 30 minutes by default; raise it for a big brain withGSTACK_INGEST_TIMEOUT_MS(accepts 1 min–24h). On timeout the gbrain import checkpoint is preserved, so the next/sync-gbrainresumes instead of starting over. - Brain-sync stage. Pushes curated artifacts (plans, designs, retros) to your private artifacts repo if you have one configured.
- CLAUDE.md guidance. Capability-checks the round-trip (write a page → search → find it). If green, writes the
## GBrain Search Guidanceblock to your project's CLAUDE.md. If red, REMOVES the block — the agent should never be told to use a tool that isn't installed.
The watermark. Sync state advances by commit hash. If gbrain hits a file it can't index (5 MB hard limit per file, or a file vanished mid-sync), the watermark stays put and subsequent syncs retry. To acknowledge an unfixable failure and move past it:
gbrain sync --source <source-id> --skip-failed
Re-runnable, idempotent, safe to run from multiple terminals on the same machine (locked at ~/.gstack/.sync-gbrain.lock).
Switching engines later
Picked PGLite and now want to join a team brain? One command:
/setup-gbrain --switch
The skill runs gbrain migrate --to supabase --url "$URL" wrapped in timeout 180s. Migration is bidirectional (Supabase → PGLite also works) and lossless — pages, chunks, embeddings, links, tags, and timeline all copy. Your original brain is preserved as a backup.
If migration hangs: another gstack session may be holding a lock on the source brain. The timeout fires at 3 minutes with an actionable message. Close other workspaces and re-run.
GStack memory sync (a separate concern)
This is different from gbrain itself. Your gstack state (~/.gstack/ — learnings, plans, retros, timeline, developer profile) is machine-local by default. "GStack memory sync" optionally pushes a curated, secret-scanned subset to a private git repo so your memory follows you across machines — and, if you're running gbrain, that git repo becomes indexable there too.
Turn it on with:
gstack-artifacts-init
You'll get a one-time privacy prompt: everything allowlisted / artifacts only (plans, designs, retros, learnings — skip behavioral data like timelines) / off. Every skill run syncs the queue at start and end — no daemon, no background process.
Secret-shaped content (AWS keys, GitHub tokens, PEM blocks, JWTs, bearer tokens) is blocked from sync before it leaves your machine.
On a new machine: Copy ~/.gstack-artifacts-remote.txt over (the legacy
~/.gstack-brain-remote.txt name still works), run gstack-brain-restore, and
yesterday's learnings surface on today's laptop.
Full guide: docs/gbrain-sync.md. Error index: docs/gbrain-sync-errors.md.
/setup-gbrain offers to wire this up for you at the end of initial setup — it's one more AskUserQuestion, and it integrates with the same private-repo infrastructure.
Cleanup orphan projects
If you Ctrl-C'd mid-provision, tried three different names before settling on one, or otherwise accumulated gbrain-shaped Supabase projects you don't use, there's a subcommand for that:
/setup-gbrain --cleanup-orphans
The skill re-collects a PAT (one-time, discarded after), lists every project in your Supabase account whose name starts with gbrain and whose ref doesn't match your active ~/.gbrain/config.json pooler URL. For each orphan it asks per-project: "Delete orphan project <ref> (<name>, created <date>)?" — no batching, no "delete all" shortcut. The active brain is never offered for deletion.
Command + flag reference
/setup-gbrain entry modes
| Invocation | What it does |
|---|---|
/setup-gbrain |
Full flow: detect state, pick path, install, init, MCP, policy, optional memory-sync |
/setup-gbrain --repo |
Flip the per-remote trust policy for the current repo only |
/setup-gbrain --switch |
Migrate engine (PGLite ↔ Supabase) without re-running the other steps |
/setup-gbrain --resume-provision <ref> |
Resume a path-2a auto-provision that was interrupted during polling |
/setup-gbrain --cleanup-orphans |
List + per-project delete of orphan Supabase projects |
Bin helpers (for scripting)
| Bin | Purpose |
|---|---|
gstack-gbrain-detect |
Emit current state as JSON: gbrain on PATH, version, config engine, doctor status, sync mode |
gstack-gbrain-install |
Detect-first installer (probes ~/git/gbrain, ~/gbrain, then fresh clone). Has --dry-run and --validate-only flags. PATH-shadow check exits 3 with remediation menu. |
gstack-gbrain-lib.sh |
Sourced, not executed. Provides read_secret_to_env VARNAME "prompt" [--echo-redacted "<sed-expr>"] |
gstack-gbrain-supabase-verify |
Structural URL check. Rejects direct-connection URLs (db.*.supabase.co:5432) with exit 3 |
gstack-gbrain-supabase-provision |
Management API wrapper. Subcommands: list-orgs, create, wait, pooler-url, list-orphans, delete-project. All require SUPABASE_ACCESS_TOKEN in env. create and pooler-url also require DB_PASS. --json mode available on every subcommand. |
gstack-gbrain-repo-policy |
Per-remote trust triad. Subcommands: get, set, list, normalize |
gstack-gbrain-source-wireup |
Registers your ~/.gstack/ brain repo with gbrain as a federated source via gbrain sources add + git worktree, then runs an initial gbrain sync. Idempotent. Replaces the dead consumers.json + /ingest-repo HTTP wireup from v1.12.x. Flags: --strict, --source-id <id>, --no-pull, --uninstall, --probe. |
gbrain CLI (upstream tool)
Gbrain itself ships with these that gstack wraps:
| Command | Purpose |
|---|---|
gbrain init --pglite |
Initialize a local PGLite brain |
gbrain init --non-interactive |
Initialize via env (GBRAIN_DATABASE_URL or DATABASE_URL). Never pass a URL as argv — it'll leak to shell history. |
gbrain doctor --json |
Health check. Returns `{status: "ok" |
gbrain migrate --to supabase --url ... |
Move a PGLite brain to Supabase (lossless, preserves source as backup) |
gbrain migrate --to pglite |
Reverse migration |
gbrain search "query" |
Search the brain |
gbrain put "<slug>" --content "<markdown-with-frontmatter>" |
Write a page (title/tags go in YAML frontmatter inside --content) |
gbrain get "<slug>" |
Fetch a page |
gbrain serve |
Start the MCP stdio server (used by claude mcp add) |
Config files + state
| Path | What lives there |
|---|---|
~/.gbrain/config.json |
Engine (pglite/postgres), database URL or path, API keys. Mode 0600. Written by gbrain init. |
~/.gstack/gbrain-repo-policy.json |
Per-remote trust triad. Schema v2. Mode 0600. |
~/.gstack/.setup-gbrain.lock.d |
Concurrent-run lock (atomic mkdir). Released on normal exit + SIGINT. |
~/.gstack/.brain-queue.d/ |
Pending sync records for gstack memory sync — maildir-style spool, one file per record. A legacy .brain-queue.jsonl from older releases migrates automatically on the next drain. |
~/.gstack/.brain-last-push |
Timestamp of last sync push (for /health scoring) |
~/.gstack-artifacts-remote.txt |
URL of your gstack memory sync remote (safe to copy between machines; legacy name ~/.gstack-brain-remote.txt still read) |
~/.gstack/.setup-gbrain-inflight.json |
Reserved for future --resume-provision persisted state |
Environment variables
| Var | Where it's read | What it does |
|---|---|---|
SUPABASE_ACCESS_TOKEN |
gstack-gbrain-supabase-provision |
PAT for Management API calls. Discarded after each setup run. |
DB_PASS |
gstack-gbrain-supabase-provision (create, pooler-url) |
Generated DB password. Never in argv. |
GBRAIN_DATABASE_URL |
gbrain init, gbrain doctor, etc. |
Postgres connection string (Supabase pooler URL for us). Env takes precedence over ~/.gbrain/config.json. |
DATABASE_URL |
gbrain init (fallback) |
Same semantics as GBRAIN_DATABASE_URL; checked second. |
SUPABASE_API_BASE |
gstack-gbrain-supabase-provision |
Override the Management API host. Used by tests to point at a mock server. |
GBRAIN_INSTALL_DIR |
gstack-gbrain-install |
Override default install path (~/gbrain) |
GSTACK_HOME |
every bin helper | Override ~/.gstack state dir. Heavy test use. |
VOYAGE_API_KEY |
gbrain embed subprocess; gstack PGLite init |
When set, gstack inits PGLite with voyage-code-3 (1024-dim), Voyage's code-specialized embedding model. Beats voyage-4-large and OpenAI text-embedding-3-large head-to-head on this codebase's symbol queries. See CHANGELOG v1.43.1.0 for the A/B numbers. |
OPENAI_API_KEY |
gbrain embed subprocess |
Used for embeddings during gbrain sync / /sync-gbrain when VOYAGE_API_KEY is not set (gbrain's auto-selected fallback, text-embedding-3-large 1536-dim). Without either key, pages are imported structurally (symbol tables, chunks) but semantic search degrades — you'll see [gbrain] embedding failed for code file ... in the sync log. |
ANTHROPIC_API_KEY |
claude-agent-sdk, paid evals |
Required for bun run test:evals and any direct query() call against Claude. |
GSTACK_OPENAI_API_KEY |
lib/conductor-env-shim.ts |
Conductor-injected fallback. Promoted to OPENAI_API_KEY when the canonical name is empty. |
GSTACK_ANTHROPIC_API_KEY |
lib/conductor-env-shim.ts |
Same pattern as above for Anthropic. |
Conductor + GSTACK_* env vars
If you run gstack inside a Conductor workspace, Conductor explicitly strips ANTHROPIC_API_KEY and OPENAI_API_KEY from the workspace env. Setting them in ~/.zshrc or .env won't help — the strip happens after env inheritance. To get a usable API key into a workspace, set GSTACK_ANTHROPIC_API_KEY and GSTACK_OPENAI_API_KEY in Conductor's workspace env config instead. Conductor passes those through untouched.
lib/conductor-env-shim.ts bridges the gap on the gstack side: when imported as a side effect (import "../lib/conductor-env-shim";), it promotes GSTACK_FOO_API_KEY to FOO_API_KEY for any subprocess that doesn't see the canonical name. The shim is already wired into:
bin/gstack-gbrain-sync.ts— so/sync-gbrainpicks up OpenAI for embeddingsbin/gstack-model-benchmark— so--judgeruns work without manual env mappingscripts/preflight-agent-sdk.ts— so paid-eval auth probes worktest/helpers/e2e-helpers.ts— sobun run test:evalsfinds Anthropic
If you add a new TS entry point that hits a paid API or needs gbrain embeddings, add the same one-line import at the top. See CONTRIBUTING.md "Conductor workspaces" for the contributor checklist.
bin/gstack-codex-probe is bash and doesn't read these directly — it relies on ~/.codex/ auth managed by the Codex CLI.
Security model
One rule for every secret this skill touches: env var only, never argv, never logged, never written to disk by us. The only persistent storage is gbrain's own ~/.gbrain/config.json at mode 0600, which is gbrain's discipline, not ours.
Enforced in code:
- CI grep test in
test/skill-validation.test.tsfails the build if$SUPABASE_ACCESS_TOKENor$GBRAIN_DATABASE_URLappears in an argv position - CI grep test fails if
--insecure,-k, orNODE_TLS_REJECT_UNAUTHORIZED=0appear inbin/gstack-gbrain-supabase-provision set +xat the top of the provision helper prevents debug tracing from leaking PAT- Telemetry payload contains only enumerated categorical values (scenario, install result, MCP opt-in, trust tier) — never free-form strings that could contain secrets
Enforced via tests:
test/secret-sink-harness.test.tsruns every secret-handling bin with a seeded secret and asserts the seed never appears in any captured channel (stdout, stderr, files under$HOME, telemetry JSONL). Four match rules per seed: exact, URL-decoded, first-12-char prefix, base64.- Positive controls in the same test file deliberately leak seeds in every covered channel and assert the harness catches each one. Without the positive controls, a harness that silently under-reports would look identical to a working harness.
What you can still leak (the honest limits of v1):
- If you paste a secret into a normal chat message outside
read -s, it's in the conversation transcript and any host-side logging - The leak harness doesn't dump subprocess environment — a bin that
env >> ~/.logwould evade detection (no bin in v1 does this; grep tests prevent it) - Your shell's own
HISTFILEbehavior is your shell's, not ours — we never pass secrets to argv so they don't land there via our code, but nothing stops you from pasting one into a rawcurlcommand yourself
Troubleshooting
"PATH SHADOWING DETECTED" during install
Another gbrain binary is earlier in PATH than the one the installer just linked. The installer's version check caught it. Fix one of:
rm $(which gbrain)if you don't need the other one- Prepend
~/.bun/binto PATH in your shell rc so the linked binary wins - Set
GBRAIN_INSTALL_DIRto the shadowing binary's install directory and re-run
Then re-run /setup-gbrain.
"rejected direct-connection URL"
You pasted a db.<ref>.supabase.co:5432 URL. Those are IPv6-only and fail in most environments. Use the Session Pooler URL instead: Supabase dashboard → Settings → Database → Connection Pooler → Session → copy URI (port 6543).
Auto-provision times out at 180s
The Supabase project is still initializing. Your ref was printed in the exit message. Wait a minute, then:
/setup-gbrain --resume-provision <ref>
The skill re-collects a PAT, skips project creation, resumes polling.
"Another /setup-gbrain instance is running"
You have a stale lock directory. If you're sure no other instance is actually running:
rm -rf ~/.gstack/.setup-gbrain.lock.d
Then re-run.
"No cross-model tension" on policy file
You edited ~/.gstack/gbrain-repo-policy.json by hand with legacy allow values? No problem. On the next read, gstack auto-migrates allow → read-write and adds _schema_version: 2. One log line on stderr, idempotent, deterministic.
gbrain doctor says "warnings"
/health treats that as yellow, not red. Check gbrain doctor --json | jq .checks to see which sub-checks are warning. Typical causes: resolver MECE overlap (skill names clashing) or DB connection not yet configured.
/sync-gbrain reports OK but gbrain search returns nothing semantic
Embeddings probably failed during import. Symbol queries (code-def, code-refs) still work because they don't need embeddings, but gbrain search "<terms>" falls back to a degraded BM25 path. Look in the sync output for lines like:
[gbrain] embedding failed for code file <name>: OpenAI embedding requires OPENAI_API_KEY
The fix is to put a provider API key in the process env before re-running. VOYAGE_API_KEY is preferred for code (gstack defaults PGLite to voyage-code-3 when set); otherwise OPENAI_API_KEY falls back to text-embedding-3-large. On a bare Mac shell, source the key from ~/.zshrc before calling. In Conductor, the lib/conductor-env-shim.ts shim promotes GSTACK_ANTHROPIC_API_KEY / GSTACK_OPENAI_API_KEY to their canonical names automatically; for VOYAGE_API_KEY, set it directly in your Conductor workspace env. Re-run /sync-gbrain --code-only to backfill embeddings on already-imported pages.
gbrain sync blocked at a commit hash — FILE_TOO_LARGE
A file in your tree exceeds gbrain's 5 MB hard limit (MAX_FILE_SIZE in gbrain/src/core/import-file.ts). Common culprits: response replay caches, captured screenshots, large JSON fixtures. Gbrain doesn't honor .gitignore-style exclude lists for code sync; the only knob is acknowledging the failure:
gbrain sync --source <source-id> --skip-failed
Watermark advances past the offending commit. The same file fails again if it changes; re-skip when that happens.
ZeroEntropy embeddings stop working after September 4, 2026
ZeroEntropy was acquired by Notion and sunsets its hosted API on September 4, 2026 (new signups already disabled). A gbrain configured with the zeroentropyai embedding recipe keeps importing pages after that date, but embedding silently fails — pages land structurally with no semantic search. The wireup helper warns when your ~/.gbrain/config.json names the recipe; migrate to another provider (Voyage via VOYAGE_API_KEY, or OpenAI via OPENAI_API_KEY) before the deadline. Details, self-hosting caveats, and migration discussion: garrytan/gstack#2365.
Switching PGLite → Supabase hangs
Another gstack session in a sibling Conductor workspace may be holding a lock on your local PGLite file via its preamble's gstack-brain-sync call. Close other workspaces, re-run /setup-gbrain --switch. The timeout is bounded at 180s so you'll never actually wait forever.
Why this design
Why per-remote trust triad and not binary allow/deny? Multi-client consultants need search without write-back. A freelance dev working on Client A in the morning and Client B in the afternoon can't let A's code insights leak into a brain Client B can search. Read-only solves that cleanly.
Why not bundle gbrain into gstack? Gbrain is a separate, actively-developed project with its own release cadence, schema migrations, and MCP surface. Bundling would mean gstack has to gate gbrain updates, which slows gbrain improvements from reaching users. Separate-but-integrated lets each ship on its own cadence.
Why gbrain init --non-interactive via env var and not a flag? Connection strings contain database passwords. Passing them as argv lands the password in ps, shell history, and process listings. Env-var handoff keeps the secret in process memory only. Gbrain supports both GBRAIN_DATABASE_URL and DATABASE_URL; we use the former to avoid collisions with non-gbrain tooling.
Why fail-hard on PATH shadowing instead of warn-and-continue? A shadowed gbrain means every subsequent command calls a different binary than the one we just installed. That's a silent version-drift bug that surfaces as mysterious feature gaps weeks later. Setup skills have one job — set up a working environment. Refusing to install into a broken one is the setup-skill-correct behavior.
Why not auto-import every repo? Privacy + noise. An auto-import preamble hook that ingests every repo you touch would: (a) leak work code into a shared brain without consent, and (b) clog search with throwaway repos. The per-remote policy makes ingestion an explicit, per-repo decision. /setup-gbrain doesn't install any auto-import hook today — but the policy store is forward-compatible for one later.
Related skills + next steps
/health— includes a GBrain dimension (doctor status, sync queue depth, last-push age) in its 0-10 composite score. The dimension is omitted when gbrain isn't installed; running/healthon a non-gbrain machine doesn't penalize that choice./gstack-upgrade— keeps gstack itself up to date. Does NOT upgrade gbrain independently. gbrain installs at the latest HEAD by default; to refresh it,git pullin your gbrain clone (default~/gbrain) and re-run/setup-gbrain. Pin a specific commit withgstack-gbrain-install --pinned-commit <sha>if you need reproducibility. Installs below the minimum tested version are refused./retro— weekly retrospective pulls learnings and plans from your gbrain when memory sync is on, letting the retro reference cross-machine history.
Run /setup-gbrain and see what sticks.