CodeVetter · Repo Unpack · Risk map
garrytan/gstack
The biggest blast radius is the browse daemon: it executes JS in pages, imports real browser cookies, and can be exposed to the internet via ngrok — mitigated by bearer tokens, scoped sub-tokens, a physically separated tunnel listener, and a deny-default CDP allowlist. Prompt injection is the second major surface, handled by content envelopes, DOM filters, and a sidecar ONNX classifier (but the L5 canary is currently unwired). Other hotspots: the 3.3K-line server.ts, the 3.2K-line bash setup that mutates user home dirs, cookie decryption paths with a documented v20 debugging-port non-goal, fail-open egress sinks, and generated-file drift that relies on sync tests to stay honest.
- The daemon root token is a random UUID stored 0600 in .gstack/browse.json; all mutating endpoints require Bearer auth, but the local listener also serves public surfaces (/cookie-picker, /welcome, /connect probe) — any local process that can read the state file owns the browser.
- pair-agent's ngrok tunnel is the only internet-facing surface; the mitigation is physical port separation — a second 127.0.0.1 listener bound lazily on /tunnel/start serves only /connect + /command with scoped tokens and a command allowlist; root tokens on it get 403 and all denials are rate-capped-logged to ~/.gstack/security/attempts.jsonl.
- Documented non-goal (#1136): cookie-import-browser launches Chrome with --remote-debugging-port=<random>; on Windows with App-Bound Encryption v20 a same-user process can connect to that port and exfiltrate decrypted cookies — an elevation path vs reading the SQLite DB directly.
- Prompt-injection defense is layered: L1-L3 content filters in browse/src/content-security.ts run on every page-content command; L4 is a 22MB int8 BERT ONNX classifier (TestSavantAI) that must run in the security sidecar subprocess (browse/src/security-sidecar-entry.ts) because onnxruntime-node can't dlopen from a bun-compiled binary; L6 combineVerdict requires two classifiers >= WARN to BLOCK on user-authored content.
- The L5 canary-token layer (browse/src/security.ts) currently has no production injector — the chat-path agent that used it was ripped — and L4b transcript classifier was deleted as dead code; defense-in-depth claims in docs exceed what's wired today.
- GSTACK_SECURITY_OFF=1 is a real kill switch for the classifier (L1-L3 filters still run); model cache is 112MB at ~/.gstack/models/testsavant-small/ and the attack log rotates at 10MB x 5 generations — operators auditing security posture need to know the ML layer is optional and locally modelled.
- server.ts is a 3383-line module owning auth, token registry init, both listeners, SSE endpoints, cookie-picker routes, inspector CDP, terminal-agent lifecycle, proxy config, and sanitization — a single blast-radius hotspot where a dispatch bug can cross trust boundaries; several invariants exist only as comment + static-grep tripwire tests. (inferred)
- The sidebar spawns an interactive `claude` PTY (browse/src/terminal-agent.ts) bridged over WebSocket authenticated via Sec-WebSocket-Protocol; any injection into that channel reaches a live agent shell — docs require reading SIDEBAR_MESSAGE_FLOW.md before touching it.
- Egress receipts are forensic, not preventive: fail-open sinks (design OpenAI calls, update-check, dashboards, git-class ops) can send unrecorded when the ledger write fails, and verifyLedger cannot detect tail truncation or whole-file re-fabrication — the threat model explicitly excludes same-user attackers.
- setup is 3262 lines of bash running with umask 077 that writes into ~/.claude, ~/.codex, ~/.factory, ~/.config/opencode, ~/.cursor, ~/.kiro, builds binaries, and manages symlinks — correctness depends on helpers like _link_or_copy and the ownership gate, enforced by static tests rather than types.
- Generated SKILL.md files are committed artifacts; resolving a merge conflict by accepting either side silently drops the other's template changes — the documented fix is resolve .tmpl + re-run gen-skill-docs, but nothing mechanical prevents the bad merge resolution.
- Paid evals spawn real vendor CLIs with real API keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY in eval-slices) — cost ~$4.35/run and depend on network/model nondeterminism; hermetic-env mitigates credential leakage but the lane is inherently flaky and secret-gated so fork PRs get no paid signal.
- Supabase community-pulse aggregates opt-in cross-user telemetry; combined with bin/gstack-telemetry-log, update-check, and the Memorable recall bridge, there are multiple low-visibility phone-home paths each gated by the egress-receipt wiring scanner with enumerated SCANNER_EXEMPT entries.
- playwright-core is patched via bun patchedDependencies (patches/playwright-core@1.62.1.patch, windowsHide) — a supply-chain-sensitive pinned patch that must be re-verified on every Playwright bump.