# Risk map — garrytan/gstack

> Canonical page: https://codevetter.com/unpack/gstack/risk-map

# Risk map — garrytan/gstack

Scanned commit 35dd014c580037d1c009776d8b0584587c1bd42c (2026-09-21). Generated by codevetter codevetter 1.12.0 on 2026-09-22.

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. — [ARCHITECTURE.md#L172-176](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/ARCHITECTURE.md#L172-L176) [browse/src/cli.ts#L121-138](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/browse/src/cli.ts#L121-L138)
- 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. — [ARCHITECTURE.md#L135-166](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/ARCHITECTURE.md#L135-L166) [browse/src/tunnel-denial-log.ts](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/browse/src/tunnel-denial-log.ts) [browse/src/server.ts](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/browse/src/server.ts)
- 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. — [ARCHITECTURE.md#L170](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/ARCHITECTURE.md#L170)
- 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. — [ARCHITECTURE.md#L219-235](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/ARCHITECTURE.md#L219-L235) [browse/src/security-classifier.ts](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/browse/src/security-classifier.ts) [browse/src/security-sidecar-entry.ts](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/browse/src/security-sidecar-entry.ts)
- 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. — [ARCHITECTURE.md#L227-233](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/ARCHITECTURE.md#L227-L233)
- 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. — [ARCHITECTURE.md#L233-235](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/ARCHITECTURE.md#L233-L235)
- 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) — [browse/src/server.ts#L16-140](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/browse/src/server.ts#L16-L140) [docs/BROWSER_INTERNALS.md#L22-38](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/docs/BROWSER_INTERNALS.md#L22-L38)
- 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. — [docs/BROWSER_INTERNALS.md#L11-20](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/docs/BROWSER_INTERNALS.md#L11-L20) [docs/BROWSER_INTERNALS.md#L40-53](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/docs/BROWSER_INTERNALS.md#L40-L53) [browse/src/terminal-agent.ts](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/browse/src/terminal-agent.ts)
- 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. — [lib/egress-receipt.ts#L5-8](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/lib/egress-receipt.ts#L5-L8) [ARCHITECTURE.md#L200-202](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/ARCHITECTURE.md#L200-L202)
- 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. — [setup#L1-14](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/setup#L1-L14) [setup#L87-120](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/setup#L87-L120)
- 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. — [CLAUDE.md](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/CLAUDE.md)
- 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. — [.github/workflows/evals.yml#L194-203](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/.github/workflows/evals.yml#L194-L203) [test/helpers/hermetic-env.ts#L44-70](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/test/helpers/hermetic-env.ts#L44-L70) [package.json#L29-34](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/package.json#L29-L34)
- 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. — [ARCHITECTURE.md#L237](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/ARCHITECTURE.md#L237) [supabase/functions/community-pulse](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/supabase/functions/community-pulse) [test/egress-receipt-wiring.test.ts](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/test/egress-receipt-wiring.test.ts) [bin/gstack-memorable](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/bin/gstack-memorable)
- 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. — [package.json#L93-95](https://github.com/garrytan/gstack/blob/35dd014c580037d1c009776d8b0584587c1bd42c/package.json#L93-L95)

Every claim links to its cited files at the pinned commit. Citations identify source evidence; they do not establish runtime correctness. Independent CodeVetter scan — not reviewed by upstream maintainers.

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
